news 2026/10/15 18:10:10

MFC柱状图绘制实战:用CBrush和GDI实现无第三方依赖

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MFC柱状图绘制实战:用CBrush和GDI实现无第三方依赖

简介:面向MFC初学者、Windows图形编程学习者以及需要完成课程设计或日常绘制统计图表的开发者,这份资源聚焦如何利用MFC在单文档界面中结合CBrush画刷绘制柱状图,并从TXT文档读取数据动态更新图表,完整展示了GDI绘图的核心流程。压缩包共36个文件,大小2.28MB,内含C++源文件、头文件、可执行程序以及工程配置、资源与调试辅助文件,既有可直接运行的exe,也保留有完整工程源码、资源脚本和调试信息,便于二次编译修改。已有599人学习下载。通过研读代码可掌握CPaintDC的设备上下文构建、画刷创建与选入、矩形填充绘制、CFile文本读取等关键技巧;同时包内提供的TXT数据文件可供换数测试,并支持自行替换数据验证动态更新效果;工程目录结构清晰,便于对照练习,是理解MFC消息映射、视图刷新机制与设备上下文协作方式的入门实践范例。

1. 用 MFC 画柱状图:别再用第三方库,一个画刷就能搞定

很多人一接到「画个柱状图」的需求,第一反应是去下载图表控件或者集成第三方绘图库,结果光配置环境就折腾半天,最后还因为库的版本冲突翻车。实际上,在 MFC 里画简单的柱状图,核心就是利用CClientDC结合CBrush画刷,再加上CString做文字标注,几十行代码就能完成一个能看的静态柱状图。这份资源适合刚接触 MFC 绘图的初学者,也适合需要在对话框里快速展示几组数据的开发者——不需要额外依赖,不碰 GDI+,纯 GDI 就能画完整个流程。

2. CBrush 画刷与设备描述表:绘图的基本功

2.1 CBrush 的三种创建方式与选型理由

柱状图的「柱体」本质上是矩形填充区域,而填充就离不开CBrush画刷。用 MFC 画柱状图,常见的坑在于画刷的创建方式选错了,导致颜色不生效或者内存泄漏。

CBrush有三种经典创建方式:

// 方式一:创建纯色画刷 CBrush brushSolid(RGB(66, 133, 244)); // 蓝色画刷 // 方式二:创建带阴影线的画刷 CBrush brushHatch(HS_DIAGCROSS, RGB(255, 0, 0)); // 红色交叉线 // 方式三:从位图创建画刷 CBitmap bitmap; bitmap.LoadBitmap(IDB_BRUSH_PATTERN); CBrush brushBitmap(&bitmap);

纯色画刷最适合柱状图的柱体,色彩直接、视觉效果明确;阴影线画刷通常用于对比图中的特殊标记,比如实际值和计划值;从位图创建的画刷应用场景少,一般只在需要纹理填充时才用。我的建议是:柱状图柱体用纯色画刷,辅助线或对比柱才考虑阴影画刷。

选型理由很简单:CBrush是 GDI 对象,创建后必须用DeleteObject()释放,否则在长时间运行的程序里会积累 GDI 句柄泄漏。CBrush析构函数会自动释放,但如果你是通过SelectObject()选入设备描述表,就得注意恢复旧画刷。

2.2 设备描述表获取与画刷选入的正确姿势

在 MFC 的对话框程序中,获取绘图设备描述表一般有两种做法:一是在OnPaint里通过CPaintDC获取;二是在消息处理函数里通过CClientDC获取。画柱状图推荐在OnPaint中完成,因为窗口刷新时会自动重绘。

void CMyDialog::OnPaint() { CPaintDC dc(this); // 设备描述表 CBrush brush(RGB(66, 133, 244)); CBrush* pOldBrush = dc.SelectObject(&brush); // 画矩形柱体,参数依次是:左上角x、左上角y、右下角x、右下角y dc.Rectangle(50, 200, 100, 300); // 恢复到旧画刷 dc.SelectObject(pOldBrush); // brush 离开作用域时自动析构释放资源 }

逻辑说明:dc.SelectObject(&brush)会把画刷对象选入设备描述表,返回值是之前的画刷指针,必须保存并在绘制完成后恢复。这里有个新手容易犯的错误——直接dc.SelectObject(&brush)后不恢复旧画刷,虽然CBrush析构时能释放对象,但设备描述表如果没有正确还原,后续绘制可能应用了错误的画刷。

参数说明:Rectangle的前两个参数是左上角坐标,后两个是右下角坐标。坐标原点默认在窗口客户区的左上角,X 轴向右为正,Y 轴向下为正,这一点在后续做数据映射时至关重要——真实数据的最大值要对应到 Y 轴方向的最小坐标值。如果你用CClientDC在OnLButtonDown里画图,要注意每次触发都会新绘制一层,不会自动擦除旧内容;而CPaintDC在OnPaint中只会在窗口无效区域重绘时触发一次。

3. 柱状图的完整实现流程:从坐标轴到数据映射

3.1 坐标轴设计:先定画布区域再画轴

画柱状图的第一步不是画柱子,而是规划画布区域。如果柱子画到窗口客户区之外,就会被截断,这个问题很常见。我一般先把客户区划分出上下左右四个边距,把绘图区约束在一个矩形内。

// 定义绘图区域,边距留出轴标签的空间 CRect rectClient; GetClientRect(&rectClient); int marginLeft = 60; // 左侧留白,放Y轴刻度 int marginBottom = 50; // 下方留白,放X轴标签 int marginTop = 20; // 上方留白,防止柱子顶到窗口边缘 int marginRight = 20; // 右侧留白 CRect rectPlot( rectClient.left + marginLeft, rectClient.top + marginTop, rectClient.right - marginRight, rectClient.bottom - marginBottom );

然后画坐标轴。X 轴画在绘图区的底边,Y 轴画在绘图区的左边:

CPen penAxis(PS_SOLID, 2, RGB(0, 0, 0)); CPen* pOldPen = dc.SelectObject(&penAxis); // X 轴 dc.MoveTo(rectPlot.left, rectPlot.bottom); dc.LineTo(rectPlot.right, rectPlot.bottom); // Y 轴 dc.MoveTo(rectPlot.left, rectPlot.top); dc.LineTo(rectPlot.left, rectPlot.bottom); // X 轴箭头 dc.MoveTo(rectPlot.right - 10, rectPlot.bottom - 5); dc.LineTo(rectPlot.right, rectPlot.bottom); dc.MoveTo(rectPlot.right - 10, rectPlot.bottom + 5); dc.LineTo(rectPlot.right, rectPlot.bottom); // Y 轴箭头 dc.MoveTo(rectPlot.left - 5, rectPlot.top + 10); dc.LineTo(rectPlot.left, rectPlot.top); dc.MoveTo(rectPlot.left + 5, rectPlot.top + 10); dc.LineTo(rectPlot.left, rectPlot.top); dc.SelectObject(pOldPen);

逻辑说明:固定画布区域可以让柱状图的绘制逻辑与窗口大小解耦,后续调整边距就能适配不同分辨率。箭头是加分项,线条用两段短斜线模拟,视觉效果接近坐标轴箭头。

参数说明:CPen的构造参数依次是线型、线宽、颜色。PS_SOLID是实线,线宽 2 像素;如果你要画虚线做参考线,可以用PS_DASH,但线宽必须是 1。箭头的大小取决于短线的长度,这里的10和5是经验值,在小窗口里可以适当缩小。

3.2 数据映射:把业务数据变成屏幕坐标

柱状图最核心的映射逻辑:给一组数据,比如{3, 7, 5, 9},要把值转换为绘图区内的像素坐标。这里的坑在于 Y 轴方向相反,最大值要映射到绘图区顶部。

double data[] = { 3, 7, 5, 9, 6, 8 }; int nCount = sizeof(data) / sizeof(double); // 找到最大值和最小值,用于归一化 double maxVal = data[0]; double minVal = data[0]; for (int i = 1; i < nCount; i++) { if (data[i] > maxVal) maxVal = data[i]; if (data[i] < minVal) minVal = data[i]; } // 绘图区宽度和高度 int plotWidth = rectPlot.Width(); int plotHeight = rectPlot.Height(); // 每组数据对应的柱宽,柱间距设为柱宽的 0.3 倍 double barWidth = plotWidth / (nCount * 1.3); double gap = barWidth * 0.3; // 数据映射到像素坐标 int barLeft = rectPlot.left + (int)(i * (barWidth + gap)); int barTop = rectPlot.bottom - (int)((data[i] - minVal) / (maxVal - minVal) * plotHeight); int barRight = barLeft + (int)barWidth; int barBottom = rectPlot.bottom;

映射公式解释:(data[i] - minVal) / (maxVal - minVal)把原始数据归一化到0~1区间,再乘以plotHeight得到像素高度。因为 Y 轴方向是向下的,所以用rectPlot.bottom减去高度,保证数值越大柱子越高。如果不做归一化,数据超出绘图区或者缩成一团都是常事。

3.3 画柱体与数据标签

柱子用Rectangle画矩形,数据标签用DrawText或者TextOut输出。

for (int i = 0; i < nCount; i++) { // 不同柱子用不同颜色,便于区分 CBrush brush(RGB(60 + i * 30, 120 + i * 20, 200)); CBrush* pOldBrush = dc.SelectObject(&brush); dc.Rectangle(barLeft, barTop, barRight, barBottom); // 画数据标签 -- 在柱子上方 CString strLabel; strLabel.Format(L"%.1f", data[i]); dc.SetBkMode(TRANSPARENT); // 背景透明,避免遮住柱子 dc.TextOut(barLeft + 5, barTop - 20, strLabel); // 画 X 轴标签 CString strName; strName.Format(L"P%d", i + 1); dc.TextOut(barLeft + (int)barWidth / 2 - 10, rectPlot.bottom + 10, strName); dc.SelectObject(pOldBrush); }

逻辑说明:用取模间隔生成渐变色,可以让相邻柱子在视觉上有区分度。SetBkMode(TRANSPARENT)很关键,如果不设置,文字背景会用默认的白色填充,盖住柱子边缘。标签位置用barTop - 20是因为文字基线在左上角,偏移量需要手动微调。

参数说明:TextOut的前两个参数是文本左上角坐标,CString::Format的用法与printf一致,L"%.1f"表示保留一位小数的宽字符格式化。如果你的工程是 ANSI 编码,去掉L前缀即可。

4. 避坑:MFC 柱状图绘制中的常见问题与排查

4.1 柱子画出去或者被截断

现象:柱子跑到了窗口外面,或者底部被对话框的边框裁掉。 原因:没有预先定义绘图区域,直接在客户区坐标上画柱子,或者边距设定不足。 解决:先拿到客户区矩形,用InflateRect或者手工边距约束出绘图区,所有柱子坐标都基于绘图区计算。如果窗口可缩放,在OnSize里保存新的绘图区,并在Invalidate()后强制重绘。

4.2 柱子颜色没变化,全是黑色

现象:画刷设置了RGB(255, 0, 0),画出来还是黑色边框加白色填充。 原因:用了DC.FrameRect或者FillRect绘制矩形,这两种函数不受当前画刷影响,而是直接用指定颜色绘制边框或填充。 解决:画柱体用Rectangle函数,它会同时使用当前画刷填充和当前画笔绘制边框。如果你想边框和填充不同色,先SelectObject画刷、SelectObject画笔,再调用Rectangle。

4.3 窗口重绘时柱状图闪烁或者消失

现象:拖动窗口时图消失,或者重绘时闪烁严重。 原因:OnPaint里没有处理WM_ERASEBKGND消息,系统先擦除背景再绘制前景,导致闪烁;图消失大多是因为绘图代码写在了其他消息函数里(如OnCreate),该函数只在窗口创建时执行一次。 解决:把全部绘制逻辑放进OnPaint;如果闪烁严重,在消息映射里拦截WM_ERASEBKGND并直接返回TRUE,或者使用双缓冲CBitmap+MemoryDC先绘制到内存再一次性贴图。

4.4 文字乱码或者标签显示问号

现象:TextOut输出的中文字符变成问号。 原因:工程字符集设置不对,项目使用了 Unicode 但代码里用了 ANSI 字符串,或者字体不支持中文。 解决:MFC 中统一使用CString搭配L前缀;在OnPaint开始处设置字体,比如CFont font; font.CreateFont(16, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"微软雅黑"),然后SelectObject进 DC。不设置字体时默认用系统字体,不同操作系统渲染效果差异很大。

4.5 GDI 句柄泄漏导致绘制越来越慢

现象:程序运行一段时间后柱状图绘制卡顿,甚至界面假死。 原因:画刷或画笔对象在循环中反复创建但没有释放,GDI 句柄数量耗尽。 解决:循环体内创建的CBrush和CPen在循环结束前会因析构自动释放,但如果用new创建了 GDI 对象,必须手动delete。建议循环内直接声明局部对象,借助栈对象生命周期管理资源。

5. 进阶技巧:用双缓冲消除闪烁,并让柱状图动态响应数据

双缓冲的实现思路是:先在内存中创建一张与绘图区等大的位图,把所有图形画到位图上,再用BitBlt一次性贴到屏幕上。这样避免了每次画矩形时窗口背景被反复擦除的闪烁。

void CMyDialog::DrawChart(CDC* pDC, CRect rectPlot, double* data, int nCount) { // 创建内存兼容 DC 和位图 CDC memDC; memDC.CreateCompatibleDC(pDC); CBitmap memBitmap; memBitmap.CreateCompatibleBitmap(pDC, rectPlot.Width(), rectPlot.Height()); CBitmap* pOldBitmap = memDC.SelectObject(&memBitmap); // 把绘图区背景刷成白色 CBrush bgBrush(RGB(255, 255, 255)); memDC.FillRect(rectPlot, &bgBrush); // 在 memDC 中执行坐标轴和柱子绘制逻辑 DrawAxes(&memDC, rectPlot); DrawBars(&memDC, rectPlot, data, nCount); // 一次性贴到屏幕 pDC->BitBlt( rectPlot.left, rectPlot.top, rectPlot.Width(), rectPlot.Height(), &memDC, 0, 0, SRCCOPY ); // 恢复并释放 GDI 对象 memDC.SelectObject(pOldBitmap); }

在这份资源里,我一般会多做一个步骤:把数据源抽成一个CArray<double>成员变量,然后提供一个SetChartData(CArray<double>& newData)方法,内部调用Invalidate()触发重绘。这样外部只要更新数据,柱状图就自动刷新,而不需要在每个按钮点击事件里重复粘贴绘制代码。

参数说明:CreateCompatibleBitmap需要原始 DC 作为参考,这样可以确保和屏幕显示格式一致;BitBlt的SRCCOPY是直接覆盖模式,不涉及透明混合。如果你的绘图区特别大,位图会占用较多内存,但一般对话框级别的柱状图几百像素宽没有压力。

实际效果验证时,我会把数据改成随机数再加大循环次数模拟动态刷新。如果这个时候画面依然丝滑,说明双缓冲生效了;如果还有肉眼可见的闪烁,多半是OnEraseBkgnd没有处理干净——直接在这个消息处理函数里return TRUE,让系统跳过背景擦除。从那以后我每次做柱状图都强制走一遍「绘图区约束 → 双缓冲贴图 → 自定义数据刷新方法」这套流程,新手阶段浪费在调闪烁上的时间几乎归零。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 18:09:58

MinGW gcc 4.6.2 Windows下老代码编译实践指南

简介&#xff1a;MinGW&#xff08;gcc 版本 4.6.2&#xff09;是一套专为 Windows 平台打造的开源开发工具集&#xff0c;面向需要在本地编译 C/C 程序却又不依赖 Visual C 等商业环境的开发者&#xff0c;解决跨平台移植与自由编译的需求。压缩包共含 1685 个文件&#xff0c…

作者头像 李华
网站建设 2026/10/15 18:09:28

【已解决】WorkBuddy 国内版与国际版共存 + 关闭自动更新 教程

WorkBuddy 国内版与国际版共存 + 关闭自动更新 教程 适用场景:同时安装 WorkBuddy(国内版)和 WorkBuddy AI(国际版),希望两者共存、不互相覆盖、不自动更新。 平台:Windows 10/11。命令行以 命令提示符(cmd) 为主,PowerShell 脚本作为可选补充。 目录 背景与目标 根因…

作者头像 李华
网站建设 2026/10/15 18:09:18

从URL到HTTPS:一次请求背后的加密与身份认证全链路解析

1. URL不只是“一个链接”&#xff0c;它是整套通信约定的起点1.1 五个基础字段&#xff1a;协议类型、主机、端口、路径与查询参数我们先回到最前端。你在浏览器地址栏里敲下的那串字符&#xff0c;平时可能叫“网址”“链接”“域名”&#xff0c;但在网络协议体系里&#xf…

作者头像 李华
网站建设 2026/10/15 18:08:57

仓颉 stdx encoding.base64 包详解:Base64 编码解码 API 与源码级实现原理

标准库网络密码学并发编程 【免费下载链接】cangjie_stdx 仓颉编程语言提供了 stdx 模块&#xff0c;该模块提供了网络、安全等领域的通用能力。 项目地址&#xff1a; https://gitcode.com/Cangjie/cangjie_stdx 点击查看 免费下载 Base64 是文本环境中传递二进制数据的事实标…

作者头像 李华
网站建设 2026/10/15 18:08:57

Zint+Zlib+Libpng预编译库集成指南:依赖链与避坑要点

简介&#xff1a;这套面向 Windows 开发者的预编译库包&#xff0c;内含 Zint、Zlib 与 Libpng 三大开源库的完整编译产物&#xff0c;专为 Visual Studio 2015 环境设计&#xff0c;提供 32 位与 64 位、Debug 和 Release 版本&#xff0c;可直接集成到项目中实现条形码/二维码…

作者头像 李华
网站建设 2026/10/15 18:08:19

手写极简视频播放器:FFmpeg4.1解码与SDL2渲染实战

在一路写完解封装、解码、格式转换这些零散知识点之后&#xff0c;其实一直缺一个东西&#xff1a;把它们串起来放出来。当时手头正好要用FFmpeg4.1&#xff0c;索性就用它写一个极简视频播放器——能读本地文件、能出画面、能出声音、能基本同步&#xff0c;代码量尽量压到最小…

作者头像 李华