资讯动态

MFC动态绘制圆弧:CDC::Arc参数、橡皮筋交互与三点定弧实现

发布时间:2026/9/8 16:15:28 来源:尧图企业网站定制
简介一份面向MFC初学者的完整可运行案例演示如何在视图类中基于GDI动态绘制圆弧。资源围绕自定义CView派生类展开通过CArcView等类实现绘图逻辑覆盖重写OnDraw、使用CDC与GDI函数、鼠标消息驱动参数更新以及用ArcTo实现精确弧线等内容能帮助读者理解MFC事件驱动模型与Windows图形设备接口的实际配合方式同时提供了接口设计思路供扩展学习。压缩包共19个文件体积仅81KB以6个头文件.h与4个源文件.cpp为主体另含工程配置.vcxproj、.sln、资源文件.rc、.ico及项目说明ReadMe.txt等目录结构清晰可导入Visual Studio直接编译运行并观察效果。这一轻量级项目也便于对照阅读、逐行断点调试。目前已有924人浏览学习。通过该案例读者可掌握动态绘图中的坐标换算、窗口重绘刷新、消息处理流程等关键机制并可将圆弧参数设置与绘制逻辑迁移到其他MFC或Windows绘图项目中是课程设计、日常练习与入门GDI编程的实用参考。 在MFC里做动态绘制圆弧算是一个经典到不能再经典的图形交互案例。但经典归经典真动手实现一遍很多人会卡在几个看似不起眼的地方CDC的Arc函数参数到底怎么传为什么拖出来的弧总在乱跳橡皮筋预览怎么做到不闪不重影我把这个实例完整做了一遍工程是在VS2013下建的MFC单文档程序核心思路和代码直接分享出来适合刚入门MFC图形编程或者准备在项目里加入画图类交互功能的同学参考。1. 项目背景与整体设计思路1.1 为什么选MFC来做这个图形交互实例先交代背景。MFC虽然老但在工业上位机、教学软件、小型工具类程序里它的地位依然稳固。原因很简单向导成熟资料多团队协作成本低。而且MFC对GDI图形设备接口的封装足够方便在窗口客户区里画点、画线、画圆弧本质上都是调用CDC类的方法逻辑简单直接。我这次做的实例目标是做一个MFC单文档程序用户按住鼠标左键在客户区拖动屏幕上就能实时看到一条圆弧跟着鼠标变化松开鼠标后圆弧固定保留在画布上并且可以连续绘制多条圆弧。这个交互过程看起来简单核心其实是三个层面的问题几何计算、消息驱动的状态管理、以及实时绘制时的图形擦除与刷新。把这三块想清楚这个实例就完成了一大半。1.2 交互方案选型为什么不用对话框输入参数画一段圆弧的方式其实有很多最常见的有三种方案交互方式优点缺点A. 参数对话框用户输入圆心、半径、起止角度实现最简单参数精确交互割裂看不到实时反馈B. 代码固定绘制程序按预定参数画弧适合测试与动画演示不具备交互性C. 鼠标拖拽动态绘制按住拖动实时预览所见即所得最能体现“动态”状态管理复杂需处理实时刷新这个实例选择方案C原因很实际动态拖拽绘制的应用场景太多了小到画图工具里的弧线大到工业软件里的轨迹编辑底层逻辑都是同一个套路。走通这一遍后续再做矩形、折线、椭圆无非是换一套几何计算交互骨架是可以直接复用的。另外要提前说清楚一个概念圆弧的几何定义方式。实际工程中主要用两种三点定弧由起点、经过点、终点确定唯一圆弧。圆心半径起止角度适合参数化存储也适合计算。我这个实例的交互输入采用三点定弧的思路但在存储时转成“圆心半径起止角”这样后续做重绘、缩放、轨迹规划都比较方便。2. 画弧前必须搞懂的数学与GDI细节2.1 CDC::Arc函数的参数机制这里有个高频陷阱MFC画圆弧用的是CDC::Arc函数签名如下BOOL Arc(LPCRECT lpRect, POINT ptStart, POINT ptEnd); // 或者 BOOL Arc(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4);这里第一个参数是边界矩形后四个参数是两个点。很多人第一次用就栽在这两个点上——它们并不是圆弧的起点和终点坐标而是“方向参考点”。GDI的实际逻辑是先找到矩形中心向ptStart作一条射线射线与矩形内切圆的交点才是真正的圆弧起点同样向ptEnd作射线交点就是圆弧终点。接着GDI按逆时针方向从起点画到终点。举个例子// 边界矩形(100,100)到(300,300)中心(200,200)半径100 // ptStart(350,200)表示圆心正右方ptEnd(200,50)表示圆心正上方 pDC-Arc(100, 100, 300, 300, 350, 200, 200, 50);这段代码会画出一段从右侧起点开始逆时针经过下方、左侧最后到达上方终点的3/4圆弧。理解这一点之后就会发现Arc函数其实很聪明只要给出方向不用自己精确计算圆周上的交点坐标。2.2 三点定弧的圆心求解动态拖拽绘制时我的做法是按下鼠标记下起点A拖动过程中以当前位置为终点C同时实时计算一个经过点BA、B、C三点确定一段圆弧。要让这三个点落在同一个圆上圆心就是它们外接圆的圆心。已知三点坐标求圆心可以用垂径定理两条弦的中垂线交点就是圆心。写代码时如果按这个几何思路求要处理直线斜率不存在的情况比较啰嗦。更稳妥的办法是直接列方程因为圆心到三点距离相等展开之后得到两个线性方程解二元一次方程组即可。CPoint CalcCircleCenter(const CPoint ptA, const CPoint ptB, const CPoint ptC, BOOL bValid) { double ax ptA.x, ay ptA.y; double bx ptB.x, by ptB.y; double cx ptC.x, cy ptC.y; double d 2 * (ax * (by - cy) bx * (cy - ay) cx * (ay - by)); if (fabs(d) 1e-6) { bValid FALSE; // 三点共线或距离过近 return CPoint(0, 0); } double ux ((ax * ax ay * ay) * (by - cy) (bx * bx by * by) * (cy - ay) (cx * cx cy * cy) * (ay - by)) / d; double uy ((ax * ax ay * ay) * (cx - bx) (bx * bx by * by) * (ax - cx) (cx * cx cy * cy) * (bx - ax)) / d; bValid TRUE; return CPoint((int)ux, (int)uy); }这段代码里d接近于0时说明三点近似共线这时候不存在外接圆需要做防御处理否则会产生一个极远处的圆心画出来的弧完全失控。2.3 角度计算、弧度制与Arc的方向控制求出圆心O后半径就是圆心到任一点的距离。要调用Arc可以直接把A点、C点作为方向参考点传给函数因为它们本身就是圆周上的点方向天然正确。但不少场景还是要计算角度比如存储时要记录起止角或者之后要做轨迹插值。角度计算用atan2double angleA atan2((double)(ptA.y - center.y), (double)(ptA.x - center.x)); double angleC atan2((double)(ptC.y - center.y), (double)(ptC.x - center.x)); double radius sqrt((double)(ptA.x-center.x)*(ptA.x-center.x) (double)(ptA.y-center.y)*(ptA.y-center.y));这里有两个特别容易踩的坑。第一atan2返回的是弧度不是角度后续如果做sin/cos运算必须统一用弧度制千万别混。第二屏幕坐标系的y轴向下所以角度正方向在视觉上是顺时针的这和数学坐标系相反。如果后面做轨迹规划、角度插值要始终记得这个坐标系差异。另外还有一个方向判断问题Arc函数默认从起点逆时针画到终点但A、B、C三点确定的弧可能是顺时针走向。如果不管方向直接画可能画出来的弧没有经过B点而是走了圆的另一侧。判断方法很简单分别计算三个点的极角看B是否落在A到C的逆时针区间内不在就说明应该画顺时针弧此时把起点终点对调传给Arc即可。3. 动态交互绘制从鼠标消息到橡皮筋效果3.1 工程搭建与绘图环境准备我用VS2013创建一个MFC单文档应用程序工程名取ArcDrawDemoView类的基类选择CView。如果用VS2019或VS2022流程完全一样。创建的要点不用多说关键是后面的消息处理。在View类里需要维护这些成员变量class CArcDrawView : public CView { protected: BOOL m_bDrawing; // 是否正在拖拽绘制 CPoint m_ptDown; // 按下鼠标时的起点 CPoint m_ptCurrent; // 拖动过程中的当前点 CPoint m_ptMid; // 动态计算的经过点 ArcData m_tempArc; // 当前拖拽的临时弧 std::vectorArcData m_arcList; // 已完成的圆弧列表 };其中ArcData是自定义的结构体struct ArcData { CPoint center; // 圆心 double radius; // 半径 double startAngle; // 起始角弧度 double endAngle; // 终止角弧度 BOOL bClockwise; // 是否顺时针 };之所以要把完成的弧存下来是因为窗口一旦重绘比如被遮挡后恢复、最小化再打开客户区内容会全部丢失必须在OnDraw里从m_arcList重新绘制。这是MFC绘图程序的基本功所有图形数据都由程序自己维护窗口重绘时用数据重建画面。3.2 经过点的动态计算与临时弧更新三点定弧需要三个点但我想让用户一次拖拽就完成操作所以第三个点不单独让用户选而是在拖动过程中自动计算。计算方法如下取起点A和当前点C的中点沿AC法线方向偏移一段距离作为经过点B。这样弧线会随鼠标移动自然弯曲体验接近很多绘图软件里的“画弧线”操作。实现代码void CArcDrawView::CalcMidPoint() { double dx m_ptCurrent.x - m_ptDown.x; double dy m_ptCurrent.y - m_ptDown.y; double len sqrt(dx * dx dy * dy); if (len 1) { m_ptMid m_ptCurrent; return; } CPoint mid((m_ptDown.x m_ptCurrent.x) / 2, (m_ptDown.y m_ptCurrent.y) / 2); double offset len * 0.25; // 弓高可调 m_ptMid.x mid.x (int)(dy / len * offset); m_ptMid.y mid.y - (int)(dx / len * offset); }注意这里的法向量方向会让弧线偏向AC连线的一侧。如果要双向弯曲可以做成按键切换或者根据鼠标在AC两侧的位置动态换方向这个就看需求的灵活度了。offset取len的0.25倍是一个经验值太小弧线看起来接近直线太大会显得夸张。有了三个点先求圆心再计算起止角和方向把结果存到m_tempArc里。3.3 橡皮筋效果XOR绘图与旧弧擦除动态绘制的核心难点是怎么让预览弧“动”起来还不产生残影。最经典的做法是用GDI的异或光栅操作R2_XORPEN。简单说在XOR模式下同样的内容画两次画面就恢复原样于是我们可以在鼠标移动时“先画一遍旧弧擦掉再画新弧”。代码骨架如下void CArcDrawView::DrawTempArc(CDC* pDC) { pDC-SetROP2(R2_XORPEN); CPen pen(PS_SOLID, 2, RGB(255, 0, 0)); CPen* pOldPen pDC-SelectObject(pen); // 画新的临时弧 DrawArcData(pDC, m_tempArc); pDC-SelectObject(pOldPen); pDC-SetROP2(R2_COPYPEN); }在OnMouseMove里每次先调用一次DrawArcData把上一帧的临时弧再画一遍相当于擦除更新m_tempArc之后再调用一次DrawTempArc画新弧。这样客户区始终只保留一条预览弧松开鼠标后临时弧被正式存进列表再统一刷新。这里有一个非常关键的点擦除旧弧时用的参数必须是上一帧的m_tempArc所以更新m_tempArc之前要先画一遍旧值。如果顺序反了残影会一直留在画布上这也是很多人做橡皮筋效果时最常见的错误。OnDraw里的重绘逻辑则很直接遍历m_arcList把每条弧的几何数据转成矩形和方向点调用Arc画出来。这里不涉及动态擦除使用普通的R2_COPYPEN还原模式即可。4. 常见问题与实用排查技巧4.1 拖动图形闪烁、残影的排查思路闪烁的一个常见来源是把Windows的擦除背景和你的绘制混在了一起。如果你在OnMouseMove里调用Invalidate()窗口会先按背景色清掉整个客户区再重画所有图形在大范围刷新时肉眼就能看出闪烁。做橡皮筋预览时我建议不要在拖动过程中走Invalidate而是直接用CClientDC在客户区绘图配合XOR方式擦旧画新实测下来流畅度好很多。残影问题则几乎都是“擦除旧弧失败”。排查顺序是这样先确认上一帧的m_tempArc确实保存在成员变量里再确认更新m_tempArc之前已经完成擦除最后确认XOR模式下画笔颜色与背景色的异或结果有效。如果换用了非XOR模式就必须先恢复背景再绘制否则一定残留。4.2 窗口重绘后图形消失图形数据必须持久化很多第一次写图形程序的开发者会遇到一个问题画好的圆弧窗口一最小化再恢复就没了。原因是Windows不会自动保存客户区的绘制内容系统只保存窗口的“位图”很少的部分一旦需要重绘所有客户区内容都要由你的OnDraw重新生成。所以要养成好习惯所有图形数据存入列表OnDraw只负责“重放”。这也是我把m_arcList单独抽出来的原因。数据存储越早做后期功能扩展越省力。4.3 三点共线和角度跳变的防御前面提到求圆心的函数有共线判断这是必须的。当鼠标拖动的距离极短或者恰好拖动成一条直线时外接圆不存在程序要直接忽略这一次绘制或者给出提示而不是硬算出一个偏差极大的圆心。角度跳变是另一个容易忽略的问题。atan2返回值的范围是[-π, π]如果起点角是170度鼠标稍微一动终点角从-170度开始如果不做处理程序会认为要从170度画到-170度结果画出一小段而不是用户预期的大圆弧。解决办法是在存储和计算时做角度归一化比如统一把角度调整到[0, 2π)再判断是否要加一圈保证弧长方向正确。完整角度归一化可以参考double NormalizeAngle(double angle) { while (angle 0) angle 2 * PI; while (angle 2 * PI) angle - 2 * PI; return angle; }5. 顺着实例还能延伸出什么5.1 从绘制圆弧到圆弧轨迹规划画圆弧这个功能天然可以延伸到运动控制领域。很多设备在运动时走的是圆弧轨迹比如点胶机、雕刻机、机械臂所谓的圆弧轨迹规划就是把你画出来的圆弧离散成一系列点逐个下发到运动控制卡或执行器。离散化思路很直接根据起止角度和弧长把角度均匀切分成N份然后用圆心半径角度算出每个点的坐标。我在实际做这类项目时会直接把ArcData导出成有固定采样间隔的坐标点序列然后在界面上回放这些点模拟运动轨迹。先在MFC里把轨迹画出来调参数再回到真实设备上运行开发效率会高很多。5.2 界面增强与项目发布这个实例做完基础版之后还有很多可以增强的地方用自定义按钮来切换“三点画弧”“圆心半径画弧”加入列表控件显示已经画出的图形参数支持删除和导出如果想让画面更漂亮可以接入OpenGL做渲染MFCOpenGL的组合在工业软件里非常常见两者互不冲突OpenGL只管3D窗口MFC负责交互和界面。项目完成后还要考虑打包发布。VS自带的部署项目在新版本里用起来别扭我现在一般用Inno Setup做安装包脚本简单、绿色小巧适合给MFC程序做发布。需要注意一点如果你的程序用了动态链接的MFC库目标机器上可能缺少对应运行库建议在项目属性里选择“在静态库中使用MFC”这样生成的exe小范围拷贝就能跑。最后再分享一个我个人做这类图形交互的小经验别急着写代码先把几何计算拆清楚。动态绘制看似是鼠标消息的问题其实大部分坑都出在几何换算上——圆心计算、角度方向、坐标系的y轴翻转这些只要有一个地方想岔了界面表现就会非常诡异。调试时可以先用TRACE把圆心、半径、起止角打出来每次鼠标松开后看日志数值符合预期再继续优化交互手感能省下不少排查时间。这个例子做完以后我又把同样的交互方式扩展到了矩形和椭圆骨架改动很小。如果你们正在做类似的图形编辑器或者轨迹编辑功能希望能给大家一点参考。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价