简介:本资源是一份面向C++初学者与MFC入门开发者的图形绘制实践项目,聚焦于使用MFC在单文档界面(SDI)中实现基于TXT数据驱动的柱状图可视化。核心围绕GDI绘图机制展开,重点演示CBrush画刷的创建、选入设备上下文及矩形填充应用,并结合CFile类完成外部数据读取与数值解析,覆盖从文件IO到坐标计算、动态绘图的完整链路。压缩包共36个文件,含6个头文件(.h)定义类结构、5个源码文件(.cpp)实现视图逻辑与绘图主流程、2个文本文件(.txt)提供示例数据与说明,另有可执行程序(.exe)、资源文件(.ico/.bmp/.rc)及VC6编译中间产物,整体大小2.28MB,目录组织体现典型MFC SDI工程规范。目前已有600人学习下载,读者可直接运行exe查看效果,复用源码快速搭建数据可视化模块,并通过Debug目录下的pdb、ilk等文件辅助调试理解MFC消息循环与绘图生命周期。
1. MFC 柱状图不是“画个矩形就完事”:底层 GDI 绘图链路、画刷资源生命周期与坐标系偏移的三重校准
你写好CRect,调用pDC->Rectangle(),编译通过,运行却只看到一片灰白——不是代码错了,是 MFC 的绘图逻辑根本没启动。这不是 Win32 API 那种“拿到 HDC 就能狂刷”的裸奔模式,MFC 把CDC、CPaintDC、CBrush、CFont全部封装成 C++ 对象,而画刷(CBrush)的创建时机、作用域和 DeleteObject 调用时机,直接决定柱状图是否可见、颜色是否正确、甚至程序会不会在退出时崩溃。这个资源包不是教你怎么拖控件,而是从OnPaint()入口开始,逐帧拆解 GDI 句柄如何被CDC管理、CBrush如何绑定到设备上下文、客户区坐标如何映射到逻辑坐标——它解决的是“为什么我画了但看不见”“为什么颜色总变蓝”“为什么窗口缩放后柱子错位”这三类高频翻车现场。适合正在维护遗留 MFC 工业监控界面、需要嵌入轻量级统计图表、又不敢贸然引入第三方库的工程师;也适合刚学完 Win32 GDI 想过渡到 MFC 封装层的同学——它不讲消息循环原理,只告诉你OnDraw()里哪一行漏了SelectObject就会黑匣子式失效。
2. 从 OnPaint 到 CDC:MFC 绘图链路的四层责任划分与对象生命周期控制
MFC 的绘图不是“调用即生效”,而是一条由框架自动调度、开发者必须显式接管的链路。理解每一环谁负责创建、谁负责释放、谁持有句柄,是避免资源泄漏和绘图异常的前提。本节不讲抽象概念,只聚焦实际代码中CBrush、CPen、CDC这三个核心对象在OnPaint()中的真实协作关系。
2.1 CPaintDC:唯一合法的 OnPaint 绘图入口,且不可手动 delete
OnPaint()是 MFC 框架在收到WM_PAINT消息后自动调用的虚函数。此时你绝不能自己 new 一个CDC*或调用GetDC(),因为这会绕过框架对设备上下文的统一管理,导致双缓冲失效、客户区擦除异常、甚至 GDI 句柄耗尽。正确做法是使用CPaintDC—— 它是CDC的派生类,构造时自动调用BeginPaint(),析构时自动调用EndPaint(),确保绘图前客户区已擦除、绘图后系统知道该刷新哪一块。
void CMyView::OnPaint() { CPaintDC dc(this); // 自动关联当前窗口,自动 BeginPaint/EndPaint // 此处 dc 对象有效,可安全调用绘图函数 // dc 析构时自动清理,无需手动 delete 或 ReleaseDC }提示:
CPaintDC只能在OnPaint()中使用。若需在其他函数(如OnLButtonDown)中临时绘图,请用CClientDC,但必须确保其作用域严格受限,且不能跨函数传递指针。
2.2 CBrush:画刷不是“设置颜色就完事”,它必须 SelectObject 到 DC 才生效
CBrush封装 GDI 的HBRUSH句柄。常见误区是以为CBrush brush(RGB(255,0,0))创建后,后续所有Rectangle()就自动用红色填充——完全错误。GDI 是状态机模型:CDC内部持有一个当前画刷句柄,所有填充操作(Rectangle,RoundRect,FillRect)都使用这个“当前”画刷。CBrush对象本身只是句柄容器,必须通过SelectObject()显式将其“选入”CDC,才能成为当前画刷。
void CMyView::OnPaint() { CPaintDC dc(this); // 错误示范:创建画刷但未选入 DC → 填充仍用默认白色画刷 CBrush redBrush(RGB(255, 0, 0)); dc.Rectangle(CRect(10, 10, 100, 50)); // 仍是白色矩形 // 正确流程:创建 → 选入 → 绘图 → (可选)恢复原画刷 CBrush blueBrush(RGB(0, 0, 255)); CBrush* pOldBrush = (CBrush*)dc.SelectObject(&blueBrush); // 返回旧画刷指针 dc.Rectangle(CRect(120, 10, 210, 50)); // 此时才是蓝色 dc.SelectObject(pOldBrush); // 恢复原画刷,保持 DC 状态干净 }参数说明:
SelectObject()返回值是之前选入的 GDI 对象指针(此处为CBrush*),务必保存并在绘图后恢复。否则后续TextOut()等操作可能因画刷不匹配而异常。MFC 文档明确要求:每次SelectObject()后,必须对应一次恢复操作。
2.3 逻辑坐标 vs 设备坐标:SetMapMode 与 SetWindowExtEx 的协同校准
MFC 默认使用MM_TEXT映射模式:逻辑单位 = 像素,Y 轴向下为正。但柱状图常需适配不同 DPI、打印输出或缩放需求,此时必须切换映射模式。SetMapMode(MM_LOMETRIC)会让 1 逻辑单位 = 0.1mm,Y 轴向上为正——这会导致你传入的CRect(0,0,100,50)实际画在屏幕外(因为 Y=0 是顶部,Y=50 是向上 5mm,即负方向)。解决方案是组合使用SetMapMode和SetWindowExtEx/SetViewportExtEx:
void CMyView::OnPaint() { CPaintDC dc(this); // 方案一:保持 MM_TEXT,手动处理 DPI 缩放(推荐用于简单 UI) CSize size; dc.GetOutputSize(size); // 获取客户区像素尺寸 int nDPI = dc.GetDeviceCaps(LOGPIXELSY); // 获取 Y 方向 DPI double scale = nDPI / 96.0; // 相对于标准 96 DPI 的缩放比 CRect rect(10, 10, 100, 50); rect.right = (long)(rect.right * scale); rect.bottom = (long)(rect.bottom * scale); dc.Rectangle(rect); // 方案二:切换到 MM_ANISOTROPIC,自定义逻辑单位(适合打印/高精度) dc.SetMapMode(MM_ANISOTROPIC); dc.SetWindowExt(1000, 1000); // 逻辑坐标:宽高各 1000 单位 dc.SetViewportExt(size.cx, -size.cy); // 设备坐标:映射到客户区,Y 取负实现 Y 向上 // 此时 CRect(0,0,500,300) 表示占逻辑宽度一半、高度 30% }关键逻辑:
SetWindowExt定义逻辑坐标范围,SetViewportExt定义设备坐标范围。两者的比值决定缩放比例。SetViewportExt的 Y 值设为负数,是让逻辑坐标的 Y 向上增长时,设备坐标 Y 向下移动(符合屏幕习惯)。
2.4 CFont 与文本对齐:柱状图标签的基线定位与 GetTextExtentPoint32 精确测宽
柱状图离不开数据标签(如“Q1: 120”)。TextOut()的 (x,y) 是字符串左下角坐标,而非左上角。若未设置字体或未测量文本尺寸,标签极易与柱子重叠或截断。必须用CFont创建字体,并用GetTextExtentPoint32获取精确宽高:
void CMyView::OnPaint() { CPaintDC dc(this); // 创建字体:指定高度为 -12(逻辑单位),加粗,宋体 CFont font; font.CreateFont(-12, 0, 0, 0, FW_BOLD, FALSE, FALSE, 0, GB2312_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, _T("SimSun")); CFont* pOldFont = (CFont*)dc.SelectObject(&font); CString strLabel = _T("Q1: 120"); CSize textSize; dc.GetTextExtentPoint32(strLabel, strLabel.GetLength(), &textSize); // 标签居中于柱子顶部:x = 柱子中心 - 文本宽度/2, y = 柱子顶部 - 2(留白) CRect barRect(10, 100, 100, 200); // 柱子区域 int xLabel = barRect.left + (barRect.Width() - textSize.cx) / 2; int yLabel = barRect.top - 2; // 注意:y 是左下角,所以减 2 是向上偏移 dc.TextOut(xLabel, yLabel, strLabel); dc.SelectObject(pOldFont); font.DeleteObject(); // 字体对象需显式删除 }注意:
CFont::CreateFont的第一个参数为负值表示按逻辑单位(非像素)指定高度,更适应 DPI 缩放。DeleteObject()必须调用,否则字体句柄泄漏。
3. 柱状图绘制实战:数据结构设计、动态坐标计算与多色画刷池管理
画柱状图表面是画几个矩形,实则是数据驱动的坐标变换工程。本节提供一套可直接复用的轻量级实现,包含数据容器、坐标映射器、画刷缓存三部分,避免每次绘图都重复创建/销毁 GDI 对象。
3.1 数据结构:CBarData 类封装单柱属性与 CBarChart 类管理全局配置
不建议用裸std::vector<int>存数据,因为柱子需携带颜色、标签、tooltip 等元信息。我们定义CBarData结构体,并用CBarChart统一管理:
// BarData.h struct CBarData { CString m_strLabel; // 柱子标签,如 "Jan" double m_dValue; // 数值,用于计算高度 COLORREF m_crColor; // 填充色 bool m_bIsHighlighted; // 是否高亮(悬停时) CBarData(LPCTSTR lpszLabel = _T(""), double dVal = 0.0, COLORREF cr = RGB(70, 130, 180), bool bHigh = false) : m_strLabel(lpszLabel), m_dValue(dVal), m_crColor(cr), m_bIsHighlighted(bHigh) {} }; // BarChart.h class CBarChart { public: std::vector<CBarData> m_data; CRect m_rcMargin; // 图表边距:left,top,right,bottom int m_nBarWidth; // 单柱宽度(像素) int m_nBarGap; // 柱间间隙(像素) double m_dMaxValue; // 数据最大值,用于归一化高度 bool m_bShowValueLabel; // 是否显示数值标签 CBarChart() : m_nBarWidth(40), m_nBarGap(10), m_dMaxValue(100.0), m_bShowValueLabel(true) { m_rcMargin.SetRect(40, 20, 20, 30); // 左、上、右、下 } void SetData(const std::vector<CBarData>& data) { m_data = data; if (!data.empty()) { m_dMaxValue = 0.0; for (const auto& bar : data) { if (bar.m_dValue > m_dMaxValue) m_dMaxValue = bar.m_dValue; } if (m_dMaxValue == 0.0) m_dMaxValue = 1.0; // 防零除 } } };设计理由:
m_dMaxValue在SetData()中动态计算,避免硬编码最大值导致小数据无法显示。m_rcMargin用CRect而非四个独立变量,便于后续InflateRect调整。
3.2 坐标计算器:CBarCoordMapper 将逻辑值转为客户区像素坐标
坐标转换是柱状图最易出错环节。我们封装CBarCoordMapper,将数据值、柱索引映射为CRect:
// BarCoordMapper.h class CBarCoordMapper { private: CRect m_rcClient; // 客户区矩形(OnPaint 中传入) CRect m_rcPlotArea; // 绘图区域 = 客户区 - 边距 CBarChart* m_pChart; public: CBarCoordMapper(const CRect& rcClient, CBarChart* pChart) : m_rcClient(rcClient), m_pChart(pChart) { // 计算绘图区域:扣除边距 m_rcPlotArea = m_rcClient; m_rcPlotArea.DeflateRect(m_pChart->m_rcMargin); } // 获取第 i 根柱子的矩形区域 CRect GetBarRect(int i) const { if (i < 0 || i >= (int)m_pChart->m_data.size()) return CRect(0,0,0,0); int nBarCount = (int)m_pChart->m_data.size(); int totalWidth = nBarCount * m_pChart->m_nBarWidth + (nBarCount - 1) * m_pChart->m_nBarGap; int startX = m_rcPlotArea.left + (m_rcPlotArea.Width() - totalWidth) / 2; int xLeft = startX + i * (m_pChart->m_nBarWidth + m_pChart->m_nBarGap); int xRight = xLeft + m_pChart->m_nBarWidth; // 高度:按比例缩放,底部对齐绘图区域底部 double ratio = m_pChart->m_data[i].m_dValue / m_pChart->m_dMaxValue; int barHeight = (int)(m_rcPlotArea.Height() * ratio); int yTop = m_rcPlotArea.bottom - barHeight; return CRect(xLeft, yTop, xRight, m_rcPlotArea.bottom); } // 获取柱子顶部标签的 TextOut 坐标(左下角) CPoint GetLabelPoint(int i) const { CRect rcBar = GetBarRect(i); if (rcBar.IsRectEmpty()) return CPoint(0,0); CString strLabel = m_pChart->m_data[i].m_strLabel; CSize sz; // 此处需传入 CDC 指针获取字体尺寸,故延迟到 OnPaint 中调用 // 本函数只返回 x,y 位置,不负责绘图 int x = rcBar.left + (rcBar.Width() - 100) / 2; // 100 为预估宽度,实际需测量 int y = rcBar.top - 2; return CPoint(x, y); } };关键点:
GetBarRect()中startX计算采用(PlotArea.Width() - totalWidth)/2实现水平居中,而非固定偏移。yTop用m_rcPlotArea.bottom - barHeight确保所有柱子底部对齐,这是柱状图视觉一致性的基础。
3.3 画刷池:CBrushPool 避免频繁 CreateObject/DeleteObject
频繁创建/销毁CBrush是性能瓶颈,尤其在OnPaint()高频调用时。CBrushPool用std::map<COLORREF, CBrush*>缓存已创建的画刷,GetBrush()返回指针,ReleaseAll()在视图销毁时统一清理:
// BrushPool.h class CBrushPool { private: std::map<COLORREF, CBrush*> m_mapBrushes; public: CBrush* GetBrush(COLORREF cr) { auto it = m_mapBrushes.find(cr); if (it != m_mapBrushes.end()) { return it->second; } CBrush* pBrush = new CBrush(cr); m_mapBrushes[cr] = pBrush; return pBrush; } void ReleaseAll() { for (auto& pair : m_mapBrushes) { if (pair.second) { pair.second->DeleteObject(); delete pair.second; } } m_mapBrushes.clear(); } }; // 在 CMyView 头文件中声明成员 class CMyView : public CView { // ... private: CBrushPool m_brushPool; CBarChart m_chart; };血泪经验:
CBrushPool必须是视图类的成员变量,而非OnPaint()局部变量。否则每次绘图都新建池子,缓存失效。ReleaseAll()应在OnDestroy()或析构函数中调用。
3.4 OnPaint 完整合成:调用坐标器、画刷池、字体,绘制完整图表
现在把所有模块组装进OnPaint():
void CMyView::OnPaint() { CPaintDC dc(this); CRect rcClient; GetClientRect(&rcClient); // 1. 初始化坐标映射器 CBarCoordMapper coordMapper(rcClient, &m_chart); // 2. 创建字体 CFont font; font.CreateFont(-12, 0, 0, 0, FW_NORMAL, FALSE, FALSE, 0, GB2312_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, _T("SimSun")); CFont* pOldFont = (CFont*)dc.SelectObject(&font); // 3. 绘制所有柱子 int nCount = (int)m_chart.m_data.size(); for (int i = 0; i < nCount; ++i) { CRect rcBar = coordMapper.GetBarRect(i); if (rcBar.IsRectEmpty()) continue; // 获取画刷(来自池) COLORREF cr = m_chart.m_data[i].m_crColor; if (m_chart.m_data[i].m_bIsHighlighted) { cr = RGB(255, 140, 0); // 高亮色 } CBrush* pBrush = m_brushPool.GetBrush(cr); CBrush* pOldBrush = (CBrush*)dc.SelectObject(pBrush); // 绘制柱子 dc.Rectangle(rcBar); // 绘制标签(如果启用) if (m_chart.m_bShowValueLabel) { CString strLabel; strLabel.Format(_T("%.0f"), m_chart.m_data[i].m_dValue); CSize sz; dc.GetTextExtentPoint32(strLabel, strLabel.GetLength(), &sz); CPoint ptLabel = coordMapper.GetLabelPoint(i); ptLabel.x += (rcBar.Width() - sz.cx) / 2; // 真实居中 ptLabel.y -= 2; // 微调 dc.TextOut(ptLabel.x, ptLabel.y, strLabel); } dc.SelectObject(pOldBrush); } // 4. 清理 dc.SelectObject(pOldFont); font.DeleteObject(); }逻辑说明:
GetLabelPoint()返回的是粗略位置,真实居中需结合GetTextExtentPoint32测量后二次计算x。pOldBrush每次循环后恢复,保证下一柱子用新画刷,且 DC 状态干净。
4. 避坑指南:OnPaint 中五类高频翻车现场与根因修复方案
MFC 绘图的坑往往不报错,只表现为“看不见”“颜色错”“闪屏”“崩溃”。以下是我在某工业 HMI 项目中踩过的五个真实问题,每一条都附带现象、根因分析和可立即验证的修复代码。
4.1 现象:柱子画出来是纯白色,无论CBrush设什么颜色
原因:SelectObject()返回值被忽略,且未恢复原画刷,导致后续Rectangle()使用了系统默认白色画刷(HOLLOW_BRUSH)。更隐蔽的情况是:CBrush构造失败(如RGB(256,0,0)超出范围),CreateBrushIndirect返回NULL,SelectObject(NULL)不报错但无效。
解决:强制检查SelectObject返回值,并始终恢复;CBrush创建后检查GetSafeHandle()是否非NULL。
CBrush brush(RGB(255,0,0)); if (brush.GetSafeHandle() == NULL) { AfxMessageBox(_T("CBrush 创建失败!检查 RGB 参数范围")); return; } CBrush* pOld = (CBrush*)dc.SelectObject(&brush); if (pOld == NULL) { AfxMessageBox(_T("SelectObject 失败!DC 可能已损坏")); return; } dc.Rectangle(rcBar); dc.SelectObject(pOld); // 必须恢复4.2 现象:窗口最大化后柱子错位、标签飞到屏幕外
原因:OnPaint()中硬编码了坐标计算(如CRect(10,10,100,50)),未基于GetClientRect()动态获取客户区尺寸。当窗口大小变化,CRect不变,绘图区域与客户区失配。
解决:所有坐标计算必须以GetClientRect(&rcClient)为起点,通过CBarCoordMapper重新计算。
排查技巧:在
OnPaint()开头加TRACE(_T("Client: %d,%d,%d,%d\n"), rcClient.left, rcClient.top, rcClient.right, rcClient.bottom);,拖动窗口看日志是否实时更新。
4.3 现象:快速拖动窗口时出现严重闪烁,柱子一闪而过
原因:MFC 默认OnEraseBkgnd()会用背景色擦除整个客户区,再OnPaint()重绘,造成“先白后图”的闪烁。CPaintDC本身不解决此问题。
解决:重载OnEraseBkgnd()返回TRUE,禁止默认擦除;改用双缓冲:先在内存 DC 绘制,再BitBlt到屏幕。
BOOL CMyView::OnEraseBkgnd(CDC* /*pDC*/) { return TRUE; // 禁止默认擦除 } void CMyView::OnPaint() { CPaintDC dc(this); CRect rcClient; GetClientRect(&rcClient); // 创建内存 DC 和位图 CDC memDC; memDC.CreateCompatibleDC(&dc); CBitmap bitmap; bitmap.CreateCompatibleBitmap(&dc, rcClient.Width(), rcClient.Height()); CBitmap* pOldBitmap = memDC.SelectObject(&bitmap); // 在内存 DC 中绘图(复用前面的绘图逻辑) memDC.FillSolidRect(&rcClient, RGB(255,255,255)); // 白色背景 // ... 此处调用 coordMapper、draw bars 等(传 memDC 而非 dc) // 一次性 BitBlt 到屏幕 dc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), &memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldBitmap); bitmap.DeleteObject(); memDC.DeleteDC(); }4.4 现象:程序退出时崩溃,调用栈指向CBrush::~CBrush()
原因:CBrush对象在OnPaint()中作为局部变量创建,但未调用DeleteObject()。MFC 的CBrush析构函数会自动调用DeleteObject(),但前提是该对象曾成功CreateBrushIndirect()。若CBrush构造时参数非法(如RGB(-1,0,0)),内部句柄为NULL,析构时DeleteObject(NULL)触发断言失败。
解决:永远用CBrushPool管理画刷,或确保CBrush在OnPaint()外创建(如视图成员),并在OnDestroy()中显式DeleteObject()。
注意:
CBrush的DeleteObject()可安全调用多次,if (GetSafeHandle()) DeleteObject();是冗余的。
4.5 现象:中文标签显示为方块(□□□),英文正常
原因:CreateFont()中字符集参数错误。GB2312_CHARSET仅支持简体中文,若系统无对应字体或OUT_DEFAULT_PRECIS匹配失败,回退到默认字体(如 Terminal),不支持中文。
解决:显式指定中文字体名,并用DEFAULT_CHARSET让系统自动选择:
font.CreateFont(-12, 0, 0, 0, FW_NORMAL, FALSE, FALSE, 0, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, _T("Microsoft YaHei")); // 微软雅黑验证方法:用
EnumFontFamilies列出系统所有中文字体,确认目标字体存在。
5. 进阶技巧:响应鼠标事件实现柱子高亮、导出为 BMP 文件、以及 DPI 感知的自适应缩放
绘图完成只是开始。真正落地的工业界面需要交互与导出能力。本节提供三个经过某产线监控系统实测的技巧,全部基于原生 MFC,不依赖第三方库。
5.1 鼠标悬停高亮:OnMouseMove + HitTest 实现精准柱子识别
OnMouseMove持续触发,需避免频繁重绘。策略是:仅当鼠标进入/离开新柱子时,标记m_nHoverIndex并InvalidateRect()重绘相关区域。
// CMyView.h 中添加 private: int m_nHoverIndex; // 当前悬停的柱子索引,-1 表示无 CRect m_rcLastHoverRect; // 上次悬停柱子的矩形,用于局部重绘 // CMyView.cpp void CMyView::OnMouseMove(UINT nFlags, CPoint point) { CRect rcClient; GetClientRect(&rcClient); // 将鼠标点转换为客户区坐标(OnMouseMove 的 point 已是客户区坐标) CBarCoordMapper coordMapper(rcClient, &m_chart); int nNewHover = -1; CRect rcNewHover; for (int i = 0; i < (int)m_chart.m_data.size(); ++i) { CRect rcBar = coordMapper.GetBarRect(i); if (rcBar.PtInRect(point)) { nNewHover = i; rcNewHover = rcBar; break; } } // 若悬停柱子改变,则局部重绘旧区域 + 新区域 if (nNewHover != m_nHoverIndex) { // 重绘旧悬停区域(清除高亮) if (m_nHoverIndex != -1) { CRect rcOld = coordMapper.GetBarRect(m_nHoverIndex); InvalidateRect(&rcOld, FALSE); } // 重绘新悬停区域(添加高亮) if (nNewHover != -1) { InvalidateRect(&rcNewHover, FALSE); } m_nHoverIndex = nNewHover; m_rcLastHoverRect = rcNewHover; } CView::OnMouseMove(nFlags, point); } // 修改 OnPaint:根据 m_nHoverIndex 设置高亮色 void CMyView::OnPaint() { // ... 前面的绘图逻辑不变 for (int i = 0; i < nCount; ++i) { COLORREF cr = m_chart.m_data[i].m_crColor; if (i == m_nHoverIndex) { cr = RGB(255, 140, 0); // 橙色高亮 } // ... 后续 SelectObject & Rectangle } }性能要点:
InvalidateRect(&rc, FALSE)的FALSE参数表示不擦除背景,由OnPaint()统一处理,避免闪烁。PtInRect()是 O(1) 操作,100 根柱子内无性能压力。
5.2 导出为 BMP:CreateDIBSection + GetDIBits 实现无损截图
CImage或Gdiplus会引入额外依赖。原生方案是创建 DIB(Device Independent Bitmap),用GetDIBits从屏幕捕获:
BOOL CMyView::ExportToBMP(LPCTSTR lpszFilePath) { CRect rcClient; GetClientRect(&rcClient); int width = rcClient.Width(); int height = rcClient.Height(); // 1. 创建兼容 DC 和 DIB CDC dcScreen; dcScreen.Attach(::GetDC(NULL)); CDC dcMem; dcMem.CreateCompatibleDC(&dcScreen); BITMAPINFO bmi = {0}; bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth = width; bmi.bmiHeader.biHeight = -height; // top-down DIB bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 24; bmi.bmiHeader.biCompression = BI_RGB; BYTE* pBits; HBITMAP hBitmap = CreateDIBSection(dcMem.GetSafeHdc(), &bmi, DIB_RGB_COLORS, (VOID**)&pBits, NULL, 0); if (!hBitmap) { ::ReleaseDC(NULL, dcScreen.GetSafeHdc()); return FALSE; } HGDIOBJ hOldObj = dcMem.SelectObject(hBitmap); // 2. 绘图到内存位图(复用 OnPaint 逻辑,但传 dcMem) CBarCoordMapper coordMapper(rcClient, &m_chart); // ... 此处粘贴 OnPaint 中的绘图代码,将 dc 替换为 dcMem // 3. 保存为 BMP 文件 CFile file; if (!file.Open(lpszFilePath, CFile::modeCreate | CFile::modeWrite)) { dcMem.SelectObject(hOldObj); DeleteObject(hBitmap); ::ReleaseDC(NULL, dcScreen.GetSafeHdc()); return FALSE; } BITMAPFILEHEADER bmf = {0}; bmf.bfType = 0x4d42; // 'BM' bmf.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + width * height * 3; bmf.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER); file.Write(&bmf, sizeof(bmf)); file.Write(&bmi.bmiHeader, sizeof(bmi.bmiHeader)); file.Write(pBits, width * height * 3); file.Close(); // 清理 dcMem.SelectObject(hOldObj); DeleteObject(hBitmap); ::ReleaseDC(NULL, dcScreen.GetSafeHdc()); return TRUE; }关键参数:
biHeight设为负值表示 top-down DIB,GetDIBits可直接读取;bfOffBits必须准确,否则图片打不开。
5.3 DPI 感知:OnSettingChange + GetDpiForWindow 实现缩放自适应
Windows 10+ 支持 per-monitor DPI。OnSettingChange()在 DPI 变化时触发,需重新计算m_nBarWidth、m_nBarGap等像素值:
void CMyView::OnSettingChange(UINT uFlags, LPCTSTR lpszSection) { CView::OnSettingChange(uFlags, lpszSection); if (uFlags == SPI_SETLOGICALDPIOVERRIDE || uFlags == SPI_SETDPI) { // 获取当前 DPI int dpi = GetDpiForWindow(m_hWnd); double scale = dpi / 96.0; // 按比例缩放柱子尺寸 m_chart.m_nBarWidth = (int)(40 * scale); m_chart.m_nBarGap = (int)(10 * scale); // 强制重绘 Invalidate(); } }注意:
GetDpiForWindow需 Windows 10 1607+,旧系统可 fallback 到GetDeviceCaps(LOGPIXELSX)。
从那以后我每次写OnPaint(),第一件事就是打开CPaintDC的文档页,确认SelectObject的返回值处理和DeleteObject的调用时机;第二件事是把GetClientRect()的结果打日志,盯着它随窗口缩放实时变化。这两个动作成了我的肌肉记忆——不是为了炫技,而是因为曾经有三天时间,我卡在一根柱子的颜色上,最后发现是CBrush构造时RGB(256,0,0)溢出,句柄为NULL,而 MFC 的CBrush析构函数在delete时试图DeleteObject(NULL),触发了
本文还有配套的精品资源,点击获取