资讯详情

资讯详情

MFC图形编辑器实战:从零实现绘图、选中、撤销与文件序列化

简介本资源是一份基于VC与MFC框架开发的图形编辑程序完整课程设计项目面向C初学者及高校计算机专业学生适用于《Windows程序设计》《面向对象编程》等课程实践环节。项目以Visual Studio 6.0及以上版本为开发环境采用单文档架构重点实现绘图基础功能如线条、矩形、椭圆绘制并优化视图类继承关系CDrawingView继承自CScrollView以支持滚动查看具备典型MFC工程结构与GUI交互逻辑。压缩包共115个文件含14个C源文件.cpp、15个头文件.h、48张界面与图标PNG资源、2个可执行文件.exe及配套文档.docx、.md、工程配置文件.sln、.vcxproj等整体体积54.82MB结构规范便于理解MFC消息映射、文档/视图架构与资源管理机制。已有131人学习下载提供从创建项目、类改写、绘图逻辑实现到编译调试的全流程参考特别适合夯实MFC基础、完成课程设计或毕业设计原型开发。1. 用 VC 和 MFC 从零搭起一个可画线、矩形、椭圆的图形编辑器不是 Demo是能存取 .draw 文件、支持撤销重做、鼠标拖拽实时预览的真实编辑程序很多人以为 MFC 只是“老古董”VC 6.0 的遗产写个对话框就封神。但现实是大量工业软件前端、设备配套配置工具、产线数据标注界面至今仍基于 MFC 构建——它轻量、无运行时依赖、直接调用 GDI、对 Win32 消息机制控制粒度极细。本项目标题里的【100010539】不是编号而是典型教学/实训项目代号指向一个明确目标不依赖第三方绘图库纯用 CDC、CRect、CPen、CBrush 和 CDocument/CView 架构实现图形对象的创建、选择、移动、缩放、删除与序列化。它不渲染矢量特效但要求每个图形元素可独立选中、拖拽时实时重绘轮廓、松开后才生成最终对象它不套用 Ribbon但必须用标准 MFC 文档/视图结构支撑文件保存.draw 自定义格式、CtrlZ/CtrlY 撤销重做、以及窗口大小变化时视图自动重绘。适合刚学完《Windows 编程基础》的开发者动手验证消息循环与设备上下文关系也适合需要快速交付轻量级图形配置工具的工程师复用核心绘图管理逻辑。2. 用 ClassWizard 和资源编辑器初始化 MFC 工程手动补全 CDrawDoc / CDrawView / CShape 基类体系MFC 图形编辑程序的骨架必须从文档/视图结构出发而非单文档或对话框应用。Visual Studio 2010 及以后版本虽弱化了 ClassWizard但Class View中右键Add Class → MFC Class仍可快速生成CDrawDoc继承自CDocument和CDrawView继承自CView。关键在于不能让向导自动生成序列化代码必须手动控制对象持久化逻辑否则无法支持自定义图形类型扩展。2.1 创建 CShape 抽象基类并定义图形元数据接口所有可绘制对象线、矩形、椭圆必须统一管理因此先定义CShape类位于Shape.h// Shape.h #pragma once #include afxwin.h class CShape { public: enum ShapeType { LINE, RECT, ELLIPSE }; CShape() : m_type(LINE), m_penColor(RGB(0,0,0)), m_brushColor(RGB(255,255,255)) {} virtual ~CShape() default; virtual void Draw(CDC* pDC) const 0; virtual bool HitTest(const CPoint pt) const 0; virtual void Move(const CSize offset) 0; virtual void Serialize(CArchive ar) 0; ShapeType GetType() const { return m_type; } COLORREF GetPenColor() const { return m_penColor; } COLORREF GetBrushColor() const { return m_brushColor; } protected: ShapeType m_type; COLORREF m_penColor; COLORREF m_brushColor; };提示HitTest是选择逻辑的核心——矩形需判断点是否在边框内含 4px 宽容区线段需计算点到线段距离 ≤ 5px椭圆则用归一化坐标判别。此处不实现具体算法但接口必须存在否则后续鼠标交互无法统一调度。2.2 在 CDrawDoc 中维护图形列表与撤销栈CDrawDoc不仅负责文件读写更是图形对象的唯一持有者和状态管理者。需在头文件中声明// DrawDoc.h class CDrawDoc : public CDocument { DECLARE_DYNCREATE(CDrawDoc) public: CDrawDoc(); virtual ~CDrawDoc(); // 图形容器按绘制顺序 CListCShape*, CShape* m_shapeList; // 撤销/重做栈每项为 std::vectorstd::unique_ptrCShape std::vectorstd::vectorstd::unique_ptrCShape m_undoStack; std::vectorstd::vectorstd::unique_ptrCShape m_redoStack; void AddShape(std::unique_ptrCShape pShape); void ClearAllShapes(); void SaveCurrentState(); // 调用前确保 m_shapeList 已深拷贝 void Undo(); void Redo(); protected: virtual BOOL OnNewDocument() override; virtual void Serialize(CArchive ar) override; };Serialize函数必须严格按类型 ID 数据字段顺序写入二进制流例如// DrawDoc.cpp void CDrawDoc::Serialize(CArchive ar) { if (ar.IsStoring()) { int count m_shapeList.GetCount(); ar count; POSITION pos m_shapeList.GetHeadPosition(); while (pos ! NULL) { CShape* pShape m_shapeList.GetNext(pos); ar static_castint(pShape-GetType()); pShape-Serialize(ar); // 各子类自行实现字段序列化 } } else { int count; ar count; ClearAllShapes(); for (int i 0; i count; i) { int type; ar type; std::unique_ptrCShape pShape; switch (static_castCShape::ShapeType(type)) { case CShape::LINE: pShape std::make_uniqueCLineShape(); break; case CShape::RECT: pShape std::make_uniqueCRectShape(); break; case CShape::ELLIPSE: pShape std::make_uniqueCEllipseShape(); break; default: continue; } pShape-Serialize(ar); m_shapeList.AddTail(pShape.release()); } } }注意CShape::Serialize是虚函数各子类如CRectShape必须重写只序列化自身字段如CRectShape::m_rect,m_fill不调用父类Serialize。否则会导致重复写入类型 ID 或崩溃。2.3 在 CDrawView 中接管鼠标消息与重绘逻辑CDrawView需重载OnLButtonDown,OnMouseMove,OnLButtonUp,OnPaint四个核心函数并维护临时绘制状态// DrawView.h class CDrawView : public CView { DECLARE_DYNCREATE(CDrawView) public: CDrawView(); virtual ~CDrawView(); enum DrawMode { NONE, DRAWING_LINE, DRAWING_RECT, DRAWING_ELLIPSE }; DrawMode m_drawMode; CPoint m_startPt, m_curPt; std::unique_ptrCShape m_tempShape; protected: virtual void OnDraw(CDC* pDC) override; virtual void OnInitialUpdate() override; afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnMouseMove(UINT nFlags, CPoint point); afx_msg void OnLButtonUp(UINT nFlags, CPoint point); afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags); DECLARE_MESSAGE_MAP() };OnLButtonDown启动绘制模式OnMouseMove更新m_curPt并触发Invalidate()强制重绘OnLButtonUp将临时图形转为正式对象并加入文档// DrawView.cpp void CDrawView::OnLButtonDown(UINT nFlags, CPoint point) { CDrawDoc* pDoc GetDocument(); ASSERT_VALID(pDoc); m_startPt point; m_curPt point; switch (m_drawMode) { case DRAWING_LINE: m_tempShape std::make_uniqueCLineShape(); break; case DRAWING_RECT: m_tempShape std::make_uniqueCRectShape(); break; case DRAWING_ELLIPSE: m_tempShape std::make_uniqueCEllipseShape(); break; default: return; } CView::OnLButtonDown(nFlags, point); } void CDrawView::OnMouseMove(UINT nFlags, CPoint point) { if ((nFlags MK_LBUTTON) 0 || m_drawMode NONE) return; m_curPt point; Invalidate(); // 触发 OnPaint绘制临时预览 CView::OnMouseMove(nFlags, point); } void CDrawView::OnLButtonUp(UINT nFlags, CPoint point) { if (m_drawMode NONE || !m_tempShape) return; // 设置临时图形的最终坐标 m_tempShape-SetRect(CRect(m_startPt, m_curPt)); // 转交文档管理 CDrawDoc* pDoc GetDocument(); pDoc-AddShape(std::move(m_tempShape)); pDoc-SaveCurrentState(); // 记录本次操作到 undo 栈 m_drawMode NONE; CView::OnLButtonUp(nFlags, point); }提示Invalidate()不带参数会重绘整个客户区对简单图形编辑器足够高效若图形数量超 200 个应改用InvalidateRect(rect)限定重绘区域避免闪烁。3. 实现 CLineShape / CRectShape / CEllipseShape 三个具体类重点处理 HitTest 与序列化字段图形对象的差异集中在几何表示、绘制方式和命中检测逻辑。三者共性是都用CRect描述位置范围线段用起点终点构造包围矩形但序列化字段必须精简——只存必要数据不存派生属性。3.1 CLineShape两点定义线段HitTest 用点线距离公式// LineShape.h class CLineShape : public CShape { public: CLineShape() { m_type LINE; } CLineShape(CPoint start, CPoint end) : m_start(start), m_end(end) { m_type LINE; } void Draw(CDC* pDC) const override { CPen pen(PS_SOLID, 1, m_penColor); CPen* pOldPen pDC-SelectObject(pen); pDC-MoveTo(m_start); pDC-LineTo(m_end); pDC-SelectObject(pOldPen); } bool HitTest(const CPoint pt) const override { // 点到线段距离 5px 即视为命中 CPoint v m_end - m_start; CPoint w pt - m_start; double c1 w.x * v.x w.y * v.y; double c2 v.x * v.x v.y * v.y; if (c1 0) return sqrt((pt.x-m_start.x)*(pt.x-m_start.x)(pt.y-m_start.y)*(pt.y-m_start.y)) 5; if (c2 c1) return sqrt((pt.x-m_end.x)*(pt.x-m_end.x)(pt.y-m_end.y)*(pt.y-m_end.y)) 5; double b c1 / c2; CPoint pb(m_start.x b*v.x, m_start.y b*v.y); return sqrt((pt.x-pb.x)*(pt.x-pb.x)(pt.y-pb.y)*(pt.y-pb.y)) 5; } void Move(const CSize offset) override { m_start offset; m_end offset; } void Serialize(CArchive ar) override { if (ar.IsStoring()) { ar m_start.x m_start.y m_end.x m_end.y; } else { ar m_start.x m_start.y m_end.x m_end.y; } } private: CPoint m_start, m_end; };3.2 CRectShape支持填充与边框HitTest 判定点是否在矩形内或边框上// RectShape.h class CRectShape : public CShape { public: CRectShape() { m_type RECT; m_fill TRUE; } CRectShape(const CRect rect) : m_rect(rect) { m_type RECT; m_fill TRUE; } void Draw(CDC* pDC) const override { CPen pen(PS_SOLID, 1, m_penColor); CBrush brush(m_fill ? m_brushColor : RGB(255,255,255)); CPen* pOldPen pDC-SelectObject(pen); CBrush* pOldBrush pDC-SelectObject(brush); if (m_fill) { pDC-Rectangle(m_rect); } else { pDC-Rectangle(m_rect); } pDC-SelectObject(pOldPen); pDC-SelectObject(pOldBrush); } bool HitTest(const CPoint pt) const override { // 先判中心区域填充时 if (m_fill m_rect.PtInRect(pt)) return true; // 再判边框4px 宽容区 CRect border m_rect; border.InflateRect(2, 2); if (!border.PtInRect(pt)) return false; border.DeflateRect(2, 2); if (border.PtInRect(pt)) return false; // 排除内部 return true; // 在膨胀后但不在收缩后的区域即边框上 } void Move(const CSize offset) override { m_rect.OffsetRect(offset); } void Serialize(CArchive ar) override { if (ar.IsStoring()) { ar m_rect.left m_rect.top m_rect.right m_rect.bottom m_fill; } else { ar m_rect.left m_rect.top m_rect.right m_rect.bottom m_fill; } } private: CRect m_rect; BOOL m_fill; };3.3 CEllipseShapeHitTest 用椭圆方程归一化判定序列化只存外接矩形椭圆本质是其外接矩形加“是否填充”标志无需存焦点或长轴参数。HitTest 将点映射到单位圆空间// EllipseShape.h class CEllipseShape : public CShape { public: CEllipseShape() { m_type ELLIPSE; m_fill TRUE; } CEllipseShape(const CRect rect) : m_rect(rect) { m_type ELLIPSE; m_fill TRUE; } void Draw(CDC* pDC) const override { CPen pen(PS_SOLID, 1, m_penColor); CBrush brush(m_fill ? m_brushColor : RGB(255,255,255)); CPen* pOldPen pDC-SelectObject(pen); CBrush* pOldBrush pDC-SelectObject(brush); if (m_fill) { pDC-Ellipse(m_rect); } else { pDC-Ellipse(m_rect); } pDC-SelectObject(pOldPen); pDC-SelectObject(pOldBrush); } bool HitTest(const CPoint pt) const override { if (!m_rect.PtInRect(pt)) return false; // 归一化将点映射到以矩形中心为原点、半宽半高为 1 的坐标系 double cx (m_rect.left m_rect.right) / 2.0; double cy (m_rect.top m_rect.bottom) / 2.0; double rx (m_rect.right - m_rect.left) / 2.0; double ry (m_rect.bottom - m_rect.top) / 2.0; if (rx 1e-6 || ry 1e-6) return false; double nx (pt.x - cx) / rx; double ny (pt.y - cy) / ry; double distSq nx*nx ny*ny; // 点在椭圆内含边界或在 2px 宽容带内 if (distSq 1.0) return true; if (distSq 1.0 distSq 1.0 0.02) { // 容忍 2% 偏差约 2px return true; } return false; } void Move(const CSize offset) override { m_rect.OffsetRect(offset); } void Serialize(CArchive ar) override { if (ar.IsStoring()) { ar m_rect.left m_rect.top m_rect.right m_rect.bottom m_fill; } else { ar m_rect.left m_rect.top m_rect.right m_rect.bottom m_fill; } } private: CRect m_rect; BOOL m_fill; };注意HitTest中椭圆的宽容区用相对误差1.0 0.02而非绝对像素因椭圆缩放时像素误差会变化。实际项目中可改为sqrt((nx*rx)^2 (ny*ry)^2)计算欧氏距离再比对但上述方案已满足教学与多数工程需求。4. 实现撤销重做与文件保存用二进制 .draw 格式规避 XML 解析开销MFC 序列化默认使用CArchive二进制流比 XML 或 JSON 更紧凑、解析更快。.draw文件无头部校验首 4 字节为图形数量后续每图形以 4 字节类型 ID 开头再跟具体字段。这种设计使CShape::Serialize成为唯一数据契约。4.1 CDrawDoc::SaveCurrentState() 的深拷贝实现撤销栈存储的是图形对象快照必须逐个new并Copy不能只存指针// DrawDoc.cpp void CDrawDoc::SaveCurrentState() { std::vectorstd::unique_ptrCShape snapshot; POSITION pos m_shapeList.GetHeadPosition(); while (pos ! NULL) { CShape* pSrc m_shapeList.GetNext(pos); std::unique_ptrCShape pClone; switch (pSrc-GetType()) { case CShape::LINE: pClone std::make_uniqueCLineShape(*static_castCLineShape*(pSrc)); break; case CShape::RECT: pClone std::make_uniqueCRectShape(*static_castCRectShape*(pSrc)); break; case CShape::ELLIPSE: pClone std::make_uniqueCEllipseShape(*static_castCEllipseShape*(pSrc)); break; } if (pClone) snapshot.push_back(std::move(pClone)); } m_undoStack.push_back(std::move(snapshot)); m_redoStack.clear(); // 新操作清空 redo }对应地Undo()和Redo()需替换m_shapeList内容void CDrawDoc::Undo() { if (m_undoStack.size() 2) return; // 至少保留初始空状态 // 将当前状态压入 redo 栈 std::vectorstd::unique_ptrCShape current; POSITION pos m_shapeList.GetHeadPosition(); while (pos ! NULL) { current.push_back(std::unique_ptrCShape(m_shapeList.RemoveHead())); } m_redoStack.push_back(std::move(current)); // 恢复上一状态 auto lastState m_undoStack.back(); ClearAllShapes(); for (auto pShape : lastState) { m_shapeList.AddTail(pShape.release()); } m_undoStack.pop_back(); UpdateAllViews(NULL); } void CDrawDoc::Redo() { if (m_redoStack.empty()) return; auto nextState m_redoStack.back(); ClearAllShapes(); for (auto pShape : nextState) { m_shapeList.AddTail(pShape.release()); } m_redoStack.pop_back(); UpdateAllViews(NULL); }4.2 在 CDrawView::OnDraw 中优化重绘性能双缓冲防闪烁GDI 直接绘图易闪烁尤其拖拽时频繁Invalidate()。启用双缓冲只需三步在CDrawView::OnInitialUpdate()中创建兼容 DC 和位图void CDrawView::OnInitialUpdate() { CView::OnInitialUpdate(); CClientDC dc(this); CRect rect; GetClientRect(rect); m_memDC.CreateCompatibleDC(dc); m_bitmap.CreateCompatibleBitmap(dc, rect.Width(), rect.Height()); m_memDC.SelectObject(m_bitmap); }在OnDraw中先绘到内存 DC再 BitBlt 到屏幕void CDrawView::OnDraw(CDC* pDC) { CRect rect; GetClientRect(rect); // 清空内存 DC m_memDC.FillSolidRect(rect, RGB(255,255,255)); // 绘制所有图形 CDrawDoc* pDoc GetDocument(); POSITION pos pDoc-m_shapeList.GetHeadPosition(); while (pos ! NULL) { CShape* pShape pDoc-m_shapeList.GetNext(pos); pShape-Draw(m_memDC); } // 绘制临时图形如正在拖拽的矩形预览 if (m_tempShape m_drawMode ! NONE) { m_tempShape-Draw(m_memDC); } // 一次性输出到屏幕 pDC-BitBlt(0, 0, rect.Width(), rect.Height(), m_memDC, 0, 0, SRCCOPY); }在OnSize中重建内存位图以适配新尺寸void CDrawView::OnSize(UINT nType, int cx, int cy) { CView::OnSize(nType, cx, cy); if (m_memDC.m_hDC ! NULL) { m_memDC.DeleteDC(); m_bitmap.DeleteObject(); CClientDC dc(this); m_memDC.CreateCompatibleDC(dc); m_bitmap.CreateCompatibleBitmap(dc, cx, cy); m_memDC.SelectObject(m_bitmap); } }提示双缓冲增加内存占用但对千级以下图形完全可接受。若需极致性能可对m_shapeList建立空间索引如四叉树但本项目规模下无需。5. 调试与验证用 Spy 观察消息流用 Process Monitor 检查 .draw 文件写入真实开发中90% 的 MFC 图形问题源于消息未正确路由或 CDC 使用不当。以下方法可快速定位5.1 用 Spy 验证鼠标消息是否被 CDrawView 拦截启动 SpyVS 安装目录下spyxx.exeFind Window → CtrlF选中程序主窗口展开Messages树勾选WM_LBUTTONDOWN,WM_MOUSEMOVE,WM_LBUTTONUP。操作时观察若消息显示为sent to: CDrawView说明消息路由正常若显示sent to: CMainFrame或CStatusBar说明视图未获得焦点或PreTranslateMessage被拦截若WM_MOUSEMOVE频率低于 30Hz检查是否在OnMouseMove中执行了耗时操作如CDC::TextOut。5.2 用 Process Monitor 追踪 .draw 文件写入行为下载 Sysinternals Process Monitor设置过滤器Process Namecontainsyourapp.exeOperationisWriteFileorCreateFilePathends with.draw执行“另存为”操作后观察是否出现NAME NOT FOUND→ 检查CDocument::DoSave中路径拼接是否含非法字符是否有FAST IO DISALLOWED→ 正常MFC 默认禁用 Fast I/OWriteFile后紧跟CloseFile→ 若无说明CArchive析构异常检查Serialize中是否有未捕获的CException。5.3 验证撤销栈深度与图形状态一致性在CDrawDoc::Undo()开头插入断点监视m_undoStack.size()和m_shapeList.GetCount()初始状态m_undoStack.size() 1空文档快照m_shapeList.GetCount() 0绘制 3 个图形后m_undoStack.size() 2初始当前m_shapeList.GetCount() 3执行一次Undo()后m_undoStack.size() 1m_shapeList.GetCount() 0。若m_shapeList.GetCount()不变说明ClearAllShapes()未清空链表常见原因是POSITION遍历时调用了RemoveHead()但未delete对象导致内存泄漏且链表节点残留。5.4 测试不同 DPI 缩放下的显示一致性MFC 控件自适应屏幕分辨率Windows 10/11 默认启用 DPI 缩放MFC 程序需声明PerMonitorHighDPIAware。在manifest文件中添加application xmlnsurn:schemas-microsoft-com:asm.v3 windowsSettings dpiAware xmlnshttp://schemas.microsoft.com/SMI/2005/WindowsSettingstrue/pm/dpiAware /windowsSettings /application并在InitInstance()中调用AfxEnableControlContainer(); // 添加此行 ::SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);然后在 125% 缩放屏幕上运行用尺子工具测量矩形边长是否与CRect值一致。若失真检查OnDraw中是否用GetDeviceCaps(LOGPIXELSX)计算了逻辑单位本项目因直接使用CRect像素值故无需额外缩放——这正是 MFC 原生 GDI 的优势像素即像素。注意CRect成员始终为逻辑像素CDC::DPtoLP/LPtoDP仅在混合使用 MM_LOENGLISH 等映射模式时需要。本项目全程使用默认MM_TEXT故所有坐标运算可直接用整数。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →