尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

计算机图形学MFC源码实现:光栅化到裁剪算法的像素级原理与工程实践

计算机图形学MFC源码实现:光栅化到裁剪算法的像素级原理与工程实践 简介一份面向计算机图形学学习者的 MFC/C 源码合集源于孔令德老孔的教学实例覆盖坐标变换、Bresenham/DDA 直线绘制、扫描线与梯形多边形填充、正/斜投影与透视投影、Phong 光照模型、OpenGL/Direct3D 渲染、贝塞尔曲线与 NURBS 曲面、碰撞检测等核心主题。压缩包约 140MB共 1522 个文件其中以 h/cpp 源码为主另有大量 ico 图标、bmp 位图、rc 资源描述文件、dsp/dsw 工程文件等既是 Windows 图形编程的完整工程参考也便于按示例逐个调试学习。已有 2583 人学习下载。通过分析并运行这批代码读者不仅能理解图形学基础概念的落地实现还能掌握 MFC 框架下的界面组织与交互写法可直接作为课程设计或图形应用的开发起点。1. 计算机图形学源码MFC实现是实验作业也是理解光栅化最快的入口如果你在检索“计算机图形学源码MFC实现”相关的内容大概率是三种情况之一课程实验要求用 VC 写直线、圆、填充、裁剪算法并提交源码接手了师兄留下的 MFC 工程想弄清楚 OnDraw 里那堆 CDC 调用或者只是想要一份能在 Windows 上直接编译跑通的图形学示例。这个方向解决的核心问题是把课本上的光栅化和裁剪算法用 GDI 设备上下文在窗口里真正画出来。它适合正在做图形学实验、需要交可运行源码和截图的学生也适合想快速验证算法落点的工程师。注意这里讨论的不是 OpenGL 渲染管线而是像素级可控的经典图形学实现每一步都能看到像素是怎么落下去的。2. 先把MFC绘图环境搭起来从AppWizard到CDC的像素落点2.1 为什么图形学源码选MFCCDC、GDI与OnDraw这套组合能做什么MFC 里的绘图核心是设备上下文 CDC。你可以把它理解成一张画布的操作句柄SetPixel、MoveTo、LineTo、Rectangle 全都要通过它来调用。在 CView 派生类里重写 OnDraw(CDC* pDC)窗口每次收到 WM_PAINT 消息时都会传入一个已经绑定了客户区的 pDC这意味着窗口被遮挡后重绘、最小化后恢复画面都会自动重建不需要自己保存整张位图。控制台程序做不到这一点你只能用字符画模拟像素看不到真实的栅格效果。Qt 也可以做图形学实验但很多院校的计算机图形学实验至今仍指定 MFC像深圳大学计算机图形学这类课程里就常见“用 MFC 实现直线/圆/裁剪算法”的题目查资料、问学长都更方便。相比之下MFC 的优势是系统 API 直接暴露像素操作不需要自己封装窗口和消息循环缺点是跨平台能力为零但做实验并不在乎这个。方案图形接口适合场景复杂度控制台程序无只能用字符模拟验证算法数学推导低但不直观MFC 单文档CDC/GDI 像素级操作课程实验、源码演示中交互完整Qt WidgetsQPainter 绘制跨平台可视化中高环境配置麻烦一个常见误解是“MFC 画直线不是有 LineTo 吗为什么还要自己写 DDA、Bresenham”因为实验的目的就是让你观察光栅化算法一步步算出像素坐标而 LineTo 是 GDI 封装好的黑盒底层如何取点你完全看不到。自己实现算法再用 SetPixel 落点才能对照课本验证每一步推导。2.2 新建单文档工程并接好第一个画点函数我一般会在 Visual Studio 里新建“MFC 应用”应用程序类型选“单个文档”项目样式保持“MFC 标准”高级功能里把打印和 ActiveX 取消勾选这样生成的工程结构最干净。视图类名按向导默认即可下文假设工程名为 GraphDemo视图类为 CGraphView。// CGraphView.h 类内增加一个画点函数声明 public: void DrawPixel(CDC* pDC, int x, int y, COLORREF clr); // CGraphView.cpp 实现 void CGraphView::DrawPixel(CDC* pDC, int x, int y, COLORREF clr) { pDC-SetPixel(x, y, clr); // GDI 最底层的像素写入操作 }SetPixel 的返回值是前一个像素颜色这里直接忽略。它的三个参数分别是设备坐标 x、y 和 COLORREF 颜色坐标单位是像素原点在客户区左上角x 向右增大y 向下增大。这个坐标系和数学里 y 向上的习惯不一样是后面最容易翻车的地方。接着在 OnDraw 里先画一条对角线验证环境void CGraphView::OnDraw(CDC* pDC) { CGraphDoc* pDoc GetDocument(); ASSERT_VALID(pDoc); if (!pDoc) return; for (int i 0; i 200; i) { DrawPixel(pDC, i, i, RGB(255, 0, 0)); // 红色对角线 } }OnDraw 里循环 200 次把 (0,0) 到 (199,199) 的像素点亮。编译运行如果看到一条红色对角线说明绘图链路已经通了。此时要注意OnDraw 里不要写死算法后面所有算法都封装成独立函数OnDraw 只做分发。2.3 菜单命令与Invalidate把“选算法-点鼠标-看结果”串起来直接在 OnDraw 里写死代码没法交互。先把菜单做出来资源视图里双击菜单编辑器在顶层加一个“实验”菜单下面挂“绘制对角线”ID 设为 ID_TEST_LINE。然后给视图类添加命令处理函数void CGraphView::OnTestLine() { m_bTestLine TRUE; // 成员变量OnDraw 里靠它判断 Invalidate(FALSE); // FALSE 表示不擦除背景降低闪烁 }m_bTestLine 是 BOOL 型成员变量在类构造函数里初始化为 FALSE。Invalidate 会让 Windows 向窗口发送 WM_PAINT 消息最终走到 OnDraw。FALSE 参数很关键如果这里用了 Invalidate(TRUE)窗口会先擦掉整个背景再重绘算法演示时屏幕会闪得厉害。OnDraw 里改成按标志位绘制if (m_bTestLine) { for (int i 0; i 200; i) { DrawPixel(pDC, i, i, RGB(0, 0, 255)); } }菜单命令通过消息映射宏 ON_COMMAND(ID_TEST_LINE, CGraphView::OnTestLine) 和命令函数绑定这是 MFC 消息映射的标准套路。到这里你已经有了一个“菜单触发绘制”的最小框架。下一步就是把真正的图形学算法填进这个框架里。3. 在OnDraw里实现直线与圆DDA、Bresenham和中点画圆3.1 DDA直线从浮点步进看光栅化的本质DDA 的思路很简单从起点到终点沿着距离更长的轴每步走一个像素另一个轴按斜率累加。先算出 dx 和 dy取两者绝对值的较大者作为步数 steps这样能保证每一步至少前进一个像素不会出现断点。void CGraphView::DrawLineDDA(CDC* pDC, int x0, int y0, int x1, int y1, COLORREF clr) { int dx x1 - x0; int dy y1 - y0; int steps abs(dx) abs(dy) ? abs(dx) : abs(dy); if (steps 0) return; // 两点重合直接跳过 float xIncrement (float)dx / steps; // 沿 x 方向的步长 float yIncrement (float)dy / steps; // 沿 y 方向的步长 float x x0; float y y0; for (int i 0; i steps; i) { pDC-SetPixel((int)(x 0.5f), (int)(y 0.5f), clr); x xIncrement; y yIncrement; } }加 0.5f 再强转 int等价于四舍五入到最近的整数像素。如果直接取整坐标 1.7 和 1.2 都落在 x1会让直线显得更粗糙。这个算法在几百万像素的范围内浮点误差可以忽略但它的计算涉及浮点乘加比纯整数慢DDA 的意义在于让原理直观真正追求性能要看 Bresenham。3.2 Bresenham直线整数增量Release版也不翻车Bresenham 的核心是维护一个误差项 err每走一步根据误差决定下一个像素是横向走还是纵向走。下面这个版本不需要把直线分为斜率小于 1 和大于 1 两种情况sx 和 sy 根据端点方向自动取 1 或 -1任意方向的直线都能画。void CGraphView::DrawLineBresenham(CDC* pDC, int x0, int y0, int x1, int y1, COLORREF clr) { int dx abs(x1 - x0); int dy abs(y1 - y0); int sx x0 x1 ? 1 : -1; int sy y0 y1 ? 1 : -1; int err dx - dy; // 误差项决定下一步走 x 还是走 y while (x0 ! x1 || y0 ! y1) { pDC-SetPixel(x0, y0, clr); int e2 2 * err; if (e2 -dy) { err - dy; x0 sx; } // 沿 x 方向走 if (e2 dx) { err dx; y0 sy; } // 沿 y 方向走 } pDC-SetPixel(x1, y1, clr); // while 退出前没画终点这里补上 }初始 err dx - dy 的含义是当前点在理想直线附近的位置偏差。e2 2 * err 是预判下一步落在哪个像素更靠近理想直线。整个过程只有整数加减和比较没有任何浮点运算也就没有 DDA 那种累积误差。循环退出时 x0 x1 且 y0 y1终点像素没来得及画所以循环后补一次 SetPixel。这是很多资料里容易漏掉的一行。3.3 中点画圆用对称性少算八分之七画圆的经典算法是中点画圆法。它只计算从 (0, r) 开始到第一象限 45 度方向的八分之一圆弧然后利用八个对称点一次画出整个圆。决策参数 d 的初始值是 1 - r后续根据 d 的正负选择正右方像素还是右下方像素。void CGraphView::DrawCircleMidPoint(CDC* pDC, int cx, int cy, int r, COLORREF clr) { if (r 0) return; int x 0; int y r; int d 1 - r; // 决策参数初始值 while (x y) { // 八个对称点一次循环画 8 个像素 pDC-SetPixel(cx x, cy y, clr); pDC-SetPixel(cx - x, cy y, clr); pDC-SetPixel(cx x, cy - y, clr); pDC-SetPixel(cx - x, cy - y, clr); pDC-SetPixel(cx y, cy x, clr); pDC-SetPixel(cx - y, cy x, clr); pDC-SetPixel(cx y, cy - x, clr); pDC-SetPixel(cx - y, cy - x, clr); if (d 0) d 2 * x 3; // 选正右方像素 else { d 2 * (x - y) 5; // 选右下方像素 y--; } x; } }d 的更新公式里2 * x 3 和 2 * (x - y) 5 是决策参数在两种选择下的增量推导过程在大多数图形学教材里都有这里不展开。注意圆心 (cx, cy) 用的是屏幕坐标画圆时同样存在 y 轴方向的问题但圆的对称性不受影响只要圆心位置正确就能显示。3.4 用菜单选算法、用鼠标给端点让OnDraw按需绘制算法函数写好后需要把它们挂到菜单和鼠标事件上。先在视图类里加成员变量CPoint m_ptStart、m_ptEnd 记录线段端点UINT m_nDrawMode 代表当前模式0 表示无操作1 表示画直线2 表示画圆。菜单命令里设置模式并切换直线算法void CGraphView::OnMenuDda() { m_nDrawMode 1; m_nLineAlgo 0; // 0 表示 DDA } void CGraphView::OnMenuBresenham() { m_nDrawMode 1; m_nLineAlgo 1; // 1 表示 Bresenham }鼠标按下和抬起时记录坐标void CGraphView::OnLButtonDown(UINT nFlags, CPoint point) { if (m_nDrawMode 1) { m_ptStart point; SetCapture(); // 捕获鼠标防止拖出窗口时丢失事件 } else if (m_nDrawMode 2) { m_ptStart point; // 圆心 } CView::OnLButtonDown(nFlags, point); } void CGraphView::OnLButtonUp(UINT nFlags, CPoint point) { if (m_nDrawMode 1) { m_ptEnd point; ReleaseCapture(); Invalidate(FALSE); // 松手后触发重绘 } else if (m_nDrawMode 2) { int dx point.x - m_ptStart.x; int dy point.y - m_ptStart.y; m_nRadius (int)sqrt((double)(dx * dx dy * dy)); Invalidate(FALSE); } CView::OnLButtonUp(nFlags, point); }OnDraw 里按模式分发绘制调用if (m_nDrawMode 1) { if (m_nLineAlgo 0) DrawLineDDA(pDC, m_ptStart.x, m_ptStart.y, m_ptEnd.x, m_ptEnd.y, RGB(255, 0, 0)); else DrawLineBresenham(pDC, m_ptStart.x, m_ptStart.y, m_ptEnd.x, m_ptEnd.y, RGB(0, 0, 255)); } else if (m_nDrawMode 2) { DrawCircleMidPoint(pDC, m_ptStart.x, m_ptStart.y, m_nRadius, RGB(0, 128, 0)); }注意半径计算用了 sqrt需要包含 头文件。如果鼠标拖动的距离很短半径可能只有几个像素圆看起来像一个小点这是正常现象。坐标点是设备坐标直接传给算法函数暂时还没做坐标变换这一步放在后面第 5 章再处理。4. 多边形填充与线段裁剪图形学实验的进阶题4.1 用鼠标点出多边形并保存顶点数据填充算法的输入是一组多边形顶点。在菜单里加一项“绘制多边形”把 m_nDrawMode 设为 3然后在 OnLButtonDown 里添加顶点void CGraphView::OnLButtonDown(UINT nFlags, CPoint point) { if (m_nDrawMode 3) // 多边形采集模式 { m_polygonPts.push_back(point); // 把顶点暂存起来 Invalidate(FALSE); // 重新绘制显示已点的顶点 return; } CView::OnLButtonDown(nFlags, point); } void CGraphView::OnRButtonDown(UINT nFlags, CPoint point) { if (m_nDrawMode 3 m_polygonPts.size() 3) { m_nDrawMode 4; // 切换到填充模式 Invalidate(FALSE); // OnDraw 里执行扫描线填充 return; } CView::OnRButtonDown(nFlags, point); }m_polygonPts 是视图类成员变量类型为 std::vector 需要在 CGraphView.h 里包含 。MFC 工程用标准模板库没有问题C11 及以后版本默认就支持。如果不想用 vector也可以换成 CArrayCPoint, CPoint但 vector 更通用。为了确认顶点位置在 OnDraw 里把每个顶点画成黑色小十字for (size_t i 0; i m_polygonPts.size(); i) { int x m_polygonPts[i].x; int y m_polygonPts[i].y; pDC-SetPixel(x, y, RGB(0, 0, 0)); pDC-MoveTo(x - 2, y); pDC-LineTo(x 2, y); pDC-MoveTo(x, y - 2); pDC-LineTo(x, y 2); }这段代码同时展示了 MoveTo/LineTo 的用法MoveTo 把画笔移动到起点LineTo 画线到终点。画出来的只是辅助标记不属于扫描线算法本身别把它当成填充结果。4.2 扫描线填充求交、排序、配对三步走扫描线填充的思路是从多边形的 ymin 到 ymax逐条扫描线计算它和多边形各边的交点 x把所有交点按 x 排序然后两两配对用 SetPixel 把这对交点之间的所有像素点亮。下面是最直观的实现void CGraphView::FillPolygonScanLine(CDC* pDC, const std::vectorCPoint pts, COLORREF clr) { if (pts.size() 3) return; int ymin pts[0].y, ymax pts[0].y; for (size_t i 1; i pts.size(); i) { if (pts[i].y ymin) ymin pts[i].y; if (pts[i].y ymax) ymax pts[i].y; } for (int y ymin; y ymax; y) { std::vectorint xs; // 当前扫描线与各边的交点 for (size_t i 0; i pts.size(); i) { const CPoint p1 pts[i]; const CPoint p2 pts[(i 1) % pts.size()]; // 下闭上开只统计真正跨过这条扫描线的边 if ((p1.y y p2.y y) || (p2.y y p1.y y)) { int x p1.x (y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y); xs.push_back(x); } } std::sort(xs.begin(), xs.end()); // 交点两两配对偶数到奇数之间填充 for (size_t i 0; i 1 xs.size(); i 2) { for (int x xs[i]; x xs[i 1]; x) pDC-SetPixel(x, y, clr); } } }这段代码有几个需要说明的细节。第一条件 (p1.y y p2.y y) || (p2.y y p1.y y) 是“下闭上开”扫描线碰到多边形顶点时只把顶点计到一条边上避免一个顶点被两条边重复计数否则配对会错乱。第二p1.y p2.y 的水平边不会进入这个条件所以不存在除零风险。第三每个交点用整数除法算 x 时会带来一格左右的误差对于教学实验足够追求像素级精确可以改用浮点或者固定点数运算。这个朴素版本的问题在于每次扫描线都要遍历所有边时间复杂度是 O(height * edges)复杂多边形会明显变慢。优化方向是活性边表 AET只维护与当前扫描线相交的边并更新交点效率高一两个数量级。但 AET 实现复杂得多作为实验作业先跑通朴素版写出 AET 可以作为选做加分项。4.3 Cohen-Sutherland编码裁剪矩形窗口与线段求交裁剪要解决“把窗口外的部分切掉”的问题。Cohen-Sutherland 算法的核心是用 4 位编码表示点相对于窗口的九宫格位置左、右、上、下各占一位。把一条线段的两个端点编码后如果两个编码都没有任何一位为 1说明都落在窗口内完全保留如果两个编码按位与不为 0说明都在窗口同一侧完全丢弃。在屏幕坐标系里y 轴向下ymin 是窗口上边界ymax 是下边界所以 BOTTOM 对应屏幕上方的区域TOP 对应屏幕下方。这和课本上数学坐标的命名正好相反写代码时务必注意。#define INSIDE 0 #define LEFT 1 #define RIGHT 2 #define BOTTOM 4 #define TOP 8 int CGraphView::ComputeOutCode(int x, int y, int xmin, int ymin, int xmax, int ymax) { int code INSIDE; if (x xmin) code | LEFT; if (x xmax) code | RIGHT; if (y ymin) code | BOTTOM; // 屏幕坐标 y 向下min 在上方 if (y ymax) code | TOP; // max 在下方 return code; }主函数循环计算新交点把窗口外的部分替换掉bool CGraphView::ClipLineCohenSutherland( int x1, int y1, int x2, int y2, int xmin, int ymin, int xmax, int ymax) { int outCode1 ComputeOutCode(x1, y1, xmin, ymin, xmax, ymax); int outCode2 ComputeOutCode(x2, y2, xmin, ymin, xmax, ymax); while (true) { if ((outCode1 | outCode2) 0) return true; // 完全在窗口内 if ((outCode1 outCode2) ! 0) return false; // 完全在窗口外 int outCode outCode1 ? outCode1 : outCode2; // 取一个在窗口外的端点 int x 0, y 0; // 与上边界求交屏幕坐标下上边是 ymax if (outCode TOP) { x x1 (x2 - x1) * (ymax - y1) / (y2 - y1); y ymax; } else if (outCode BOTTOM) { x x1 (x2 - x1) * (ymin - y1) / (y2 - y1); y ymin; } else if (outCode RIGHT) { y y1 (y2 - y1) * (xmax - x1) / (x2 - x1); x xmax; } else if (outCode LEFT) { y y1 (y2 - y1) * (xmin - x1) / (x2 - x1); x xmin; } if (outCode outCode1) { x1 x; y1 y; outCode1 ComputeOutCode(x1, y1, xmin, ymin, xmax, ymax); } else { x2 x; y2 y; outCode2 ComputeOutCode(x2, y2, xmin, ymin, xmax, ymax); } } }调用方式是在 OnDraw 里先画一个窗口矩形然后取一条横跨窗口的线段传入裁剪函数最后用 MoveTo/LineTo 把裁剪后的线段画出来。验证时要注意如果线段完全在窗口外裁剪返回 false什么也不要画如果完全在窗口内返回 true 但坐标没有变化。分母为零的问题在这里不会发生水平线要么完全在窗口内要么两个端点的上下编码同时为 0 或同时非 0 并在第一步被提前返回不会进入除法分支。5. MFC图形学源码的5个高频踩坑点从模板丢失到坐标翻转5.1 VS里没有MFC模板组件没装全现象新建项目对话框里搜不到“MFC 应用”只能建控制台程序打开网上下载的 MFC 源码工程提示“此项目类型不受支持”。原因安装 Visual Studio 时工作负载“使用 C 的桌面开发”没有被勾选或者选了但没勾下方“适用于最新 v143 生成工具的 C MFC”组件。这个问题在离线安装 mfc 时尤其常见很多人只装了主 IDE没有装 MFC 库。解决打开 Visual Studio Installer点“修改”切到“单个组件”搜索“MFC”勾选“适用于最新 v143 生成工具的 C MFC (x86 和 x64)”然后点修改。如果用的是离线 ISO同样要在安装器里补勾这个组件不重新装完MFC 工程的 Intellisense 都会报错。5.2 Debug正常Release画不出图GDI对象泄漏现象Debug 模式一切正常切换到 Release 后画面空白或者画到一半程序异常退出。打开任务管理器看到进程的 GDI 对象数量持续上涨直到接近一万后崩溃。原因创建了画笔、画刷、位图、内存 DC但没有及时释放。Release 优化下内存分配策略更激进资源耗尽后 GDI 调用直接失败SetPixel 没有效果现象就是“画不出图”。解决每次创建 GDI 对象后画完必须 SelectObject 还原旧对象并 DeleteObject 清理。以画笔为例CPen redPen(PS_SOLID, 1, RGB(255, 0, 0)); CPen* pOldPen pDC-SelectObject(redPen); pDC-MoveTo(x1, y1); pDC-LineTo(x2, y2); pDC-SelectObject(pOldPen); // 还原对于 CBitmap 和 CreateCompatibleDC结束后分别调用 DeleteObject 和 DeleteDC。画图帧数很高的动画里最好把常用的画笔、画刷缓存成视图类成员变量只创建一次避免每帧重复创建销毁。5.3 画的圆上下颠倒屏幕坐标与数学坐标的y轴差异现象用世界坐标算好的三角形顶点画出来是倒的圆和椭圆的位置整体偏移看起来跑到左上角去了。原因数学坐标系 y 轴向上屏幕坐标 y 轴向下。如果直接把算法里算出的世界坐标当成屏幕坐标传给 SetPixel顶点越往下 y 值反而越小图形就像在镜子里一样翻转。解决定义世界坐标到屏幕坐标的换算函数void CGraphView::WorldToScreen(double worldX, double worldY, int screenX, int screenY) { screenX (int)(worldX m_offsetX); screenY (int)(m_offsetY - worldY); // 核心就是这里取负号抵消向下坐标轴 }m_offsetX、m_offsetY 是让世界坐标原点落在客户区合适位置的偏移量通常在 OnSize 或 OnInitialUpdate 里根据客户区大小计算。所有算法函数仍然按数学坐标输入只在 SetPixel 之前调用一次 WorldToScreen这样 DDA、Bresenham、中点画圆的代码都不受影响。我第一次写图形学源码时没做这个转换整条直线都跑到了窗口上方后来养成习惯先在客户区画一条 y0 的参考线再验证算法输出。5.4 CImage::Save保存BMP失败GDI未初始化现象调用 CImage::Save(_T(snapshot.bmp)) 保存图片程序直接抛出 CImageException或者文件根本没有生成。原因CImage::Save 底层走 GDI 的编码器但 MFC 向导生成的工程默认不会调用 GdiplusStartup运行时 GDI 处于未初始化状态保存操作自然失败。解决在 App 类的 InitInstance 里初始化 GDI在 ExitInstance 里关闭。需要包含 atlimage.h 并链接 gdiplus.libULONG_PTR gdiplusToken; Gdiplus::GdiplusStartupInput gdiplusInput; Gdiplus::GdiplusStartup(gdiplusToken, gdiplusInput, NULL); // mfc 显示 bmp 图片前也要先过这关ExitInstance 里对应调用 Gdiplus::GdiplusShutdown(gdiplusToken)。gdiplusToken 存成 App 类成员变量。如果已经做了这一步保存 BMP 前还可以用 CFile::GetStatus 检查目标文件是否存在避免覆盖之前的实验截图。5.5 静态链接MFC时对话框创建失败atlmfc dumpcont.cpp报错现象项目属性里把“MFC 的使用”改成“在静态库中使用 MFC”后程序启动时对话框创建失败输出窗口出现类似 f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\dumpcont.cpp(23) : ATLTRACE 的调试信息然后程序退出。原因静态链接 MFC 是一整套复杂的构建配置涉及宏定义、库文件名、字符集匹配。不同 VS 版本的静态库路径不同一旦 Debug/Release 配置里 _DEBUG 或 _UNICODE 宏不匹配MFC 内部初始化就会走到 dumpcont.cpp 的异常分支。解决最省事的做法是把“MFC 的使用”改回“在共享 DLL 中使用 MFC”然后重新生成解决方案。如果实验要求静态链接需要确认 VS 安装器里确实装了“适用于最近工具集的 C MFC (x86 和 x64)”并且 Debug 和 Release 下都选同一个配置清空解决方案后重新编译。只是交实验的话共享 DLL 完全够用部署前带上 mfc140u.dll 等运行库即可。6. 让这套源码能交作业也能扩展BMP导出、定时器动画与验证6.1 把绘制结果保存成BMP文件实验报告要插图最直接的办法是把客户区内容导出成 BMP。用 CImage 创建与客户区等大的位图把窗口 DC 的内容 BitBlt 到内存 DC再把内存 DC 拷进 CImagevoid CGraphView::OnSaveBmp() { CRect rc; GetClientRect(rc); CImage img; img.Create(rc.Width(), rc.Height(), 24); // 24 位真彩色 CDC* pWndDC GetDC(); CDC memDC; memDC.CreateCompatibleDC(pWndDC); CBitmap bmp; bmp.CreateCompatibleBitmap(pWndDC, rc.Width(), rc.Height()); CBitmap* pOldBmp memDC.SelectObject(bmp); memDC.BitBlt(0, 0, rc.Width(), rc.Height(), pWndDC, 0, 0, SRCCOPY); img.GetDC()-BitBlt(0, 0, rc.Width(), rc.Height(), memDC, 0, 0, SRCCOPY); img.ReleaseDC(); memDC.SelectObject(pOldBmp); bmp.DeleteObject(); ReleaseDC(pWndDC); img.Save(_T(snapshot.bmp)); // GDI 没初始化的话会崩溃见 5.4 }img.Save 会按文件扩展名自动选择编码器保存 JPG 也可以实验报告插图建议用 PNG体积小且无压缩损失。这里先落盘后续要做“mfc 显示 bmp 图片”就把 CImage 的内容 Draw 到 CStatic 控件上原理一致。6.2 用定时器让线段动起来图形学实验通常会有旋转或平移的演示题。设置一个定时器每 50 毫秒触发一次重绘并在 OnDraw 里根据角度动态计算坐标void CGraphView::OnStartAnimation() { m_angle 0.0; SetTimer(1, 50, NULL); // 每 50ms 触发一次 WM_TIMER } void CGraphView::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1) { m_angle 0.05; // 角度步进 Invalidate(FALSE); // 不要用 TRUE擦背景会闪 } CView::OnTimer(nIDEvent); } void CGraphView::OnDestroy() { KillTimer(1); // 忘记关定时器会导致窗口关闭后仍在跑 CView::OnDestroy(); }OnDraw 里用 sin/cos 计算旋转后的坐标调用 Bresenham 直线画一条绕中心旋转的线段。20 FPS 的帧率足够观察光栅化过程更大的角度步进会让动画显得跳跃。6.3 算法对不对用一张像素表验证写算法最容易出现的错觉是“看起来差不多应该没错”。验证 DDA 和 Bresenham 正确性的简单方法是让它们画一条从 (0,0) 到 (5,2) 的短线然后把实际输出像素列出来对比步骤理想像素坐标1(0, 0)2(1, 0)3(2, 1)4(3, 1)5(4, 2)6(5, 2)DDA 和 Bresenham 在这个例子上输出应该完全一致。如果发现少了一点或多一点先在算法函数入口加 OutputDebugString 或 TRACE 打印每次 SetPixel 的坐标逐行核对是哪一步偏离了中点判断。我最早做这类实验时直线总是少画终点最后发现是循环退出条件提前一个像素补上终点 SetPixel 后一切正常。这套源码的价值不在于画多复杂的图形而在于每个像素都是自己算出来的。把坐标变换、GDI 资源管理、算法验证这三件事做好后续接多边形裁剪、Bezier 曲线都能在这套框架里继续扩展。希望帮到你。本文还有配套的精品资源点击获取
返回列表