首页/新闻资讯/正文详情

MFC柱状图绘制原理:GDI资源管理与坐标映射校准

发布时间:2026/11/2 12:53:50 来源:云帆数科 栏目:资讯中心
MFC柱状图绘制原理:GDI资源管理与坐标映射校准
简介本资源是一份面向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 到 CDCMFC 绘图链路的四层责任划分与对象生命周期控制MFC 的绘图不是“调用即生效”而是一条由框架自动调度、开发者必须显式接管的链路。理解每一环谁负责创建、谁负责释放、谁持有句柄是避免资源泄漏和绘图异常的前提。本节不讲抽象概念只聚焦实际代码中CBrush、CPen、CDC这三个核心对象在OnPaint()中的真实协作关系。2.1 CPaintDC唯一合法的 OnPaint 绘图入口且不可手动 deleteOnPaint()是 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.1mmY 轴向上为正——这会导致你传入的CRect(0,0,100,50)实际画在屏幕外因为 Y0 是顶部Y50 是向上 5mm即负方向。解决方案是组合使用SetMapMode和SetWindowExtEx/SetViewportExtExvoid 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::vectorint存数据因为柱子需携带颜色、标签、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::vectorCBarData 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::vectorCBarData 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::mapCOLORREF, CBrush*缓存已创建的画刷GetBrush()返回指针ReleaseAll()在视图销毁时统一清理// BrushPool.h class CBrushPool { private: std::mapCOLORREF, 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返回NULLSelectObject(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 导出为 BMPCreateDIBSection GetDIBits 实现无损截图CImage或Gdiplus会引入额外依赖。原生方案是创建 DIBDevice 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 DIBGetDIBits可直接读取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)触发了本文还有配套的精品资源点击获取

相关推荐

JavaScript作用域原理与实践指南
JavaScript作用域原理与实践指南

1. 为什么作用域是JavaScript的基石当我在2015年第一次接触JavaScript时,最困惑的就是为什么在函数内部定义的变量,外部无法访问。直到理解了作用域这个概念,才真正打开了JavaScript的大门。作用域就像是一个透明的玻璃罩,决定了你… · 2026/11/2 12:53:50

MFC多页对话框实战:CTabCtrl标签页切换与页面生命周期管理
MFC多页对话框实战:CTabCtrl标签页切换与页面生命周期管理

简介:本资源是一份面向MFC初学者与中级开发者的TabControl标签页切换实战项目,聚焦Windows桌面应用中多页面动态切换的核心交互实现。资源完整演示了CTabCtrl控件的初始化、子窗口关联、TCN_SELCHANGE事件响应、页面数据按需加载等关键环节,并… · 2026/11/2 12:53:50

虚拟电厂调度中的鲁棒优化与不确定性建模
虚拟电厂调度中的鲁棒优化与不确定性建模

1. 虚拟电厂调度中的不确定性挑战在能源系统向分布式、智能化转型的背景下,虚拟电厂(Virtual Power Plant, VPP)作为聚合分布式能源资源的关键技术,其调度优化面临源-荷双重不确定性的核心挑战。传统电力系统中,发电侧… · 2026/11/2 12:53:50

MySQL插入语句修改数据:ON DUPLICATE KEY UPDATE与REPLACE INTO详解
MySQL插入语句修改数据:ON DUPLICATE KEY UPDATE与REPLACE INTO详解

1. 先搞清楚一件事:插入语句到底能不能改数据很多人第一次看到"用插入语句修改字段值"这个说法,第一反应是"这不是矛盾吗"。插入是往表里加新行,修改是动已有的行,两个动作在语义上是对立的。但如果你真的在业… · 2026/11/3 4:26:48

LLM硬件加速器:KV Cache与MoE架构的专用硬件解法
LLM硬件加速器:KV Cache与MoE架构的专用硬件解法

1. 这不是“又一个AI芯片”:LLM硬件加速器的本质是重构计算范式你可能已经看过太多标题里带“AI芯片”“大模型加速”“国产替代”的新闻稿——它们往往堆砌参数、罗列算力数字、强调“支持千亿参数”,最后落点在“已量产”“达成战略合作”。但真正做过… · 2026/11/3 4:26:48

OpenCode+Harness智能体工程方法论:构建可编排、可审计的生产级智能体
OpenCode+Harness智能体工程方法论:构建可编排、可审计的生产级智能体

1. 这不是又一个“AI玩具”,而是一套可落地的智能体工程方法论OpenCode 智能体教程——光看标题,很多人第一反应是“又一个新出的AI平台宣传页”。但如果你真花30分钟拆开它背后的Harness核心架构,就会发现这根本不是教你怎么点几下鼠标调用大… · 2026/11/3 4:26:48

人形机器人技术瓶颈与商业化路径分析
人形机器人技术瓶颈与商业化路径分析

1. 人形机器人产业现状与商业困境人形机器人这个领域最近两年突然火了起来,但真正了解行业的人都知道,现阶段做这个生意简直就是在"烧钱"。我接触过几家头部企业,他们每年的研发投入都是以亿为单位计算的,但商业化落地却… · 2026/11/3 4:26:48

边缘容器服务:云边协同架构的技术突破与实践
边缘容器服务:云边协同架构的技术突破与实践

1. 边缘容器服务的技术本质与行业痛点云原生技术发展到今天,已经形成了从开发到运维的完整技术栈,但传统中心化云计算架构在物联网、工业互联网等场景下暴露出明显短板。我曾参与过某智能制造项目的云边协同改造,当车间设备产生的实时数据需要… · 2026/11/3 4:26:48

TFLite内存规划器解析:张量生命周期与Arena复用机制
TFLite内存规划器解析:张量生命周期与Arena复用机制

做端侧推理的人,早晚会在内存上栽一回。模型文件 20MB,你觉得稳了,结果一跑起来进程直接飙到 200MB 以上,换个小内存的设备就 OOM。多数人第一反应是换更小的模型、降分辨率,其实还有一个比模型体积更隐蔽的“内存黑洞… · 2026/11/3 4:25:48

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18

STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59

ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59

基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码