MFC CToolBar 自定义工具栏:图片文字混排与高DPI适配实战
简介这份资源面向使用MFC进行Windows界面开发的初学者与中级开发者聚焦CToolBar工具栏的自定义实现解决按钮图片与文字同时显示、工具栏停靠与浮动等常见需求。压缩包共24个文件约324KB包含7个h头文件、5个cpp源文件以及ico图标、rc资源脚本、vcxproj工程与sln解决方案等另附工具栏图片素材与代码说明文档可直接导入现有项目参考。资源以TimeClient示例工程为主线演示了在InitInstance中创建并初始化工具栏、通过LoadBitmap加载位图、用SetButtonInfo设置按钮样式与图像索引以及重写OnDockBar、调用DockControlBar和FloatToolBar控制停靠与浮动状态的完整流程并涉及按钮点击事件的响应处理。目前已有1684人学习下载适合希望快速掌握MFC工具栏定制技巧、对照可运行代码查漏补缺的开发者。1. 从一张糊掉的工具栏截图说起MFC CToolBar 到底能改成什么样接手过一个老 MFC 项目主框架上那排工具栏按钮全是 16 色糊图产品经理看了一眼说“这玩意儿像上个世纪的”。我当时想不就是换个图吗结果翻车了——CToolBar 默认走的是 4bit 位图资源直接塞 PNG 进去根本不认。后来花了两天才把图片和文字混排、高 DPI 不糊、按钮状态切换这套东西理顺。这个 Demo 就是那次的产物一个基于 MFC 的 CToolBar 自定义工具栏示例支持给按钮挂自定义图片、图片和文字同时显示、运行时切换按钮状态。适合正在维护 MFC 老项目、需要把工具栏从“能用”改成“能看”的开发者。如果你也在跟 CToolBar 的位图资源较劲这份代码能省你不少时间。2. 工程结构与环境搭建从零把 Demo 跑起来2.1 目录结构与文件职责拿到源码先别急着编译花两分钟把目录看一遍后面改起来心里有数。这个 Demo 的结构是典型的 MFC 单文档工程核心文件就那几个CToolBarDemo/ ├── CToolBarDemo.sln # VS 解决方案文件 ├── CToolBarDemo/ │ ├── MainFrm.h / .cpp # 主框架窗口工具栏在这里创建 │ ├── CToolBarDemoView.h/.cpp # 视图类演示用 │ ├── CToolBarDemo.h/.cpp # 应用程序类 │ ├── CustomToolBar.h/.cpp # 自定义工具栏封装核心 │ ├── res/ │ │ ├── Toolbar.bmp # 工具栏位图资源 │ │ ├── Toolbar256.bmp # 高彩色位图 │ │ └── icons/ # 单张按钮图片 │ └── resource.h # 资源 ID 定义CustomToolBar是重点它继承自CToolBar把图片加载、文字设置、状态切换都封进去了。MainFrm.cpp里负责创建和挂载res目录放图片资源。搞清这个分工改的时候就知道该动哪个文件。2.2 编译环境与依赖确认这个 Demo 对环境的依赖不复杂但版本对不上会直接编译报错。我一般按这个清单核对项目要求说明Visual Studio2015 及以上2010 也能开但高 DPI 支持差MFC 组件需勾选安装VS 安装器里“C MFC”默认不装字符集Unicode工程属性里确认多字节会有乱码Windows SDK8.1 及以上影响部分绘图 API 可用性装 VS 的时候最容易踩的坑就是没勾 MFC。打开解决方案如果提示找不到afxwin.h八成是 MFC 组件没装去 VS 安装器里补上“使用 C 的桌面开发”下的 MFC 选项就行。2.3 首次编译与运行验证环境确认后直接打开.sln选Debug|x64或Debug|Win32编译。第一次编译大概率会遇到两个问题一是资源文件路径不对二是预编译头报错。前者检查res目录是否完整后者在工程属性里把“预编译头”改成“不使用预编译头”先跑通再逐个文件加回来。编译通过后运行主窗口顶部应该出现一排带图标的工具栏按钮。如果按钮是空白方块说明位图资源没加载成功去MainFrm.cpp里看LoadBitmap的返回值。跑通这一步后面改代码才有意义。提示建议先用 Debug 配置跑一遍Release 下有些断言会被优化掉排查问题不方便。3. 核心模块拆解CToolBar 自定义图片与文字的完整链路3.1 自定义工具栏类的继承与初始化CustomToolBar继承CToolBar重写了创建和绘制相关的几个方法。核心思路是不用 MFC 默认的 4bit 位图机制改成自己管理图片列表按按钮索引取图。// CustomToolBar.h class CCustomToolBar : public CToolBar { DECLARE_DYNAMIC(CCustomToolBar) public: CCustomToolBar(); virtual ~CCustomToolBar(); // 加载单张按钮图片nIndex 对应按钮位置 BOOL SetButtonImage(int nIndex, UINT nResID, LPCTSTR lpszType _T(PNG)); // 设置按钮文字支持图片文字混排 BOOL SetButtonText(int nIndex, LPCTSTR lpszText); // 切换按钮按下/弹起状态 void SetButtonPressed(int nIndex, BOOL bPressed); protected: CImageList m_imgList; // 管理按钮图片 CMapint, int, CString, CString m_mapText; // 按钮索引到文字的映射 virtual void OnUpdateCmdUI(...); afx_msg void OnPaint(); DECLARE_MESSAGE_MAP() };这里用CImageList而不是CBitmap是因为CImageList天然支持多张图、透明色和不同尺寸管理起来省事。m_mapText存按钮索引和文字的对应关系绘制时按索引查。SetButtonImage的lpszType参数默认PNG意味着你可以直接加载 PNG 资源不用先转成 BMP。3.2 图片资源的加载与按钮绑定加载图片是这套逻辑里最容易出问题的地方。MFC 默认的LoadBitmap只认 BMP而且对色深有要求。这里改成从资源里加载 PNG再转成CImageList能用的格式。BOOL CCustomToolBar::SetButtonImage(int nIndex, UINT nResID, LPCTSTR lpszType) { // 从资源加载 PNGFindResource 定位资源块 HRSRC hRes ::FindResource(AfxGetResourceHandle(), MAKEINTRESOURCE(nResID), lpszType); if (hRes NULL) return FALSE; HGLOBAL hMem ::LoadResource(AfxGetResourceHandle(), hRes); DWORD dwSize ::SizeofResource(AfxGetResourceHandle(), hRes); LPVOID pData ::LockResource(hMem); // 用 CImage 从内存流加载支持 PNG/JPG CImage image; IStream* pStream ::SHCreateMemStream((BYTE*)pData, dwSize); if (pStream NULL) return FALSE; image.Load(pStream); pStream-Release(); // 转成 HBITMAP 加入 ImageListILC_COLOR32 保证高彩色 HBITMAP hBmp image.Detach(); if (m_imgList.GetSafeHandle() NULL) m_imgList.Create(24, 24, ILC_COLOR32 | ILC_MASK, 0, 4); m_imgList.Add(CBitmap::FromHandle(hBmp), RGB(255, 0, 255)); ::DeleteObject(hBmp); // 把 ImageList 挂到工具栏SetButtons 需提前调用 SetImageList(m_imgList); return TRUE; }逐段看FindResource按资源 ID 和类型定位类型传PNG就能找到 PNG 资源SHCreateMemStream把内存数据包成流CImage::Load直接吃流省去临时文件ILC_COLOR32是关键不加这个高彩色图会失真RGB(255,0,255)是透明色实际用 PNG 自带 alpha 时这个参数影响不大但保留着兼容旧图。最后SetImageList把图列表挂上去按钮就会按索引取图。3.3 文字与图片混排的绘制逻辑CToolBar 默认只显示图片要显示文字得改样式。MFC 提供了TBSTYLE_FLAT和TBBS_TEXT组合但混排效果一般所以这里重写OnPaint自己画。void CCustomToolBar::OnPaint() { CPaintDC dc(this); CRect rectClient; GetClientRect(rectClient); // 遍历所有按钮逐个绘制 int nCount GetCount(); for (int i 0; i nCount; i) { UINT nID GetItemID(i); if (nID 0 || nID ID_SEPARATOR) continue; CRect rectBtn; GetItemRect(i, rectBtn); // 画按钮背景按下状态用不同颜色 BOOL bPressed (GetButtonStyle(i) TBBS_PRESSED) ! 0; dc.FillSolidRect(rectBtn, bPressed ? RGB(200, 200, 200) : RGB(240, 240, 240)); // 画图片从 ImageList 取对应索引 if (m_imgList.GetSafeHandle() ! NULL) { CPoint ptImg(rectBtn.left 4, rectBtn.top 4); m_imgList.Draw(dc, i, ptImg, ILD_TRANSPARENT); } // 画文字从 map 里查 CString strText; if (m_mapText.Lookup(i, strText) !strText.IsEmpty()) { dc.SetBkMode(TRANSPARENT); dc.SetTextColor(bPressed ? RGB(0, 0, 0) : RGB(60, 60, 60)); CRect rectText rectBtn; rectText.top 28; // 文字在图片下方 dc.DrawText(strText, rectText, DT_CENTER | DT_SINGLELINE); } } }GetItemRect拿到每个按钮的矩形区域FillSolidRect画背景m_imgList.Draw画图DrawText画文字。文字位置用rectText.top 28往下挪避免和图片重叠。按下状态通过TBBS_PRESSED判断背景和文字颜色都做区分。这段代码的好处是完全可控想改间距、颜色、字体直接调参数就行不用跟 MFC 默认样式较劲。3.4 按钮状态切换与消息响应工具栏按钮要能响应点击并切换状态得在消息映射里处理。这里用ON_COMMAND_RANGE批量处理比逐个写ON_COMMAND清爽。// CustomToolBar.cpp 消息映射 BEGIN_MESSAGE_MAP(CCustomToolBar, CToolBar) ON_WM_PAINT() ON_COMMAND_RANGE(ID_BTN_FIRST, ID_BTN_LAST, CCustomToolBar::OnButtonClick) ON_UPDATE_COMMAND_UI_RANGE(ID_BTN_FIRST, ID_BTN_LAST, CCustomToolBar::OnUpdateButton) END_MESSAGE_MAP() void CCustomToolBar::OnButtonClick(UINT nID) { // 找到按钮索引 int nIndex CommandToIndex(nID); if (nIndex 0) return; // 切换按下状态 UINT nStyle GetButtonStyle(nIndex); nStyle ^ TBBS_PRESSED; SetButtonStyle(nIndex, nStyle); // 触发重绘 InvalidateRect(NULL); UpdateWindow(); // 通知父窗口实际项目里这里发自定义消息 GetParent()-PostMessage(WM_COMMAND, nID, 0); } void CCustomToolBar::OnUpdateButton(CCmdUI* pCmdUI) { // 根据按钮状态更新 UI比如启用/禁用 int nIndex CommandToIndex(pCmdUI-m_nID); if (nIndex 0) { BOOL bPressed (GetButtonStyle(nIndex) TBBS_PRESSED) ! 0; pCmdUI-SetCheck(bPressed ? 1 : 0); } }ON_COMMAND_RANGE把一段 ID 范围映射到同一个处理函数CommandToIndex反查索引SetButtonStyle改状态InvalidateRect触发重绘。OnUpdateButton负责 UI 同步比如按钮按下时菜单项也打勾。实际项目里点击后通常要发自定义消息给主框架这里用PostMessage模拟改成WM_USER n更规范。4. 避坑与常见问题排查那些让我加班到凌晨的细节4.1 图片显示为空白或黑块现象按钮位置有框但图片是空白或者纯黑。原因多半是位图色深不对或者ILC_COLOR32没加。MFC 默认按 4bit 处理你塞 24bit 图进去就乱。解决确认m_imgList.Create时带了ILC_COLOR32PNG 资源用CImage加载而不是LoadBitmap。如果还是黑块检查透明色参数换成RGB(255,0,255)试试。4.2 高 DPI 下工具栏模糊现象在 125% 或 150% 缩放的显示器上工具栏图片糊成一片。原因MFC 默认不处理 DPI 缩放图片按 96 DPI 绘制系统拉伸导致模糊。解决在MainFrm构造函数里调SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)然后按 DPI 比例重新加载图片尺寸。简单做法是准备两套图24x24 和 32x32运行时按GetDpiForWindow选。4.3 按钮文字不显示或位置偏移现象图片出来了文字死活不显示或者跑到按钮外面。原因m_mapText没填或者rectText计算时没考虑按钮实际高度。解决确认SetButtonText在SetButtonImage之后调用rectText.top的偏移量根据按钮高度动态算别写死 28。按钮高度用GetItemRect拿文字区域高度 按钮高度 - 图片高度 - 间距。4.4 工具栏停靠后重绘异常现象工具栏拖出来浮动再停靠回去按钮图片错位或消失。原因停靠状态变化时CImageList被释放或重建索引对不上。解决在OnBarStyleChange或OnLButtonUp里重新SetImageList确保图列表句柄有效。更稳的做法是把m_imgList的生命周期绑到工具栏对象上别在局部作用域创建。4.5 资源 ID 冲突导致按钮错乱现象按钮点击响应错乱点 A 触发 B。原因ON_COMMAND_RANGE的 ID 范围和菜单或其他控件的 ID 重叠。解决在resource.h里给工具栏按钮单独划一段 ID比如ID_BTN_FIRST 32771到ID_BTN_LAST 32790确保和其他命令不撞。改完记得清理重新编译资源 ID 有时候会缓存。5. 二次开发改造从 Demo 到项目实战的几步5.1 把工具栏封装成可复用组件Demo 里的CustomToolBar已经封了一层但要塞进实际项目还得再抽象。我一般会加一个配置结构体把按钮的图片、文字、ID、初始状态都放进去初始化时一次性传入。struct ToolBarItemConfig { UINT nCmdID; // 命令 ID UINT nImageResID; // 图片资源 ID CString strText; // 按钮文字 BOOL bInitiallyPressed; // 初始按下状态 }; // 批量初始化接口 void CCustomToolBar::InitFromConfig(const std::vectorToolBarItemConfig configs) { SetButtons(NULL, 0); // 先清空 for (size_t i 0; i configs.size(); i) { SetButtonInfo(i, configs[i].nCmdID, TBBS_BUTTON, i); SetButtonImage(i, configs[i].nImageResID); SetButtonText(i, configs[i].strText); if (configs[i].bInitiallyPressed) SetButtonPressed(i, TRUE); } }这样换项目时只改配置数组不用动工具栏类本身。配置可以从 XML 或 JSON 读甚至做成可视化编辑器生成省得每次手写资源 ID。5.2 支持运行时动态换肤实际项目里经常要求换主题工具栏图片和颜色都得跟着变。思路是把图片资源和颜色方案都做成可切换的。图片按主题名分目录颜色定义成结构体。struct ToolBarTheme { COLORREF clrBackground; COLORREF clrPressed; COLORREF clrText; CString strImageFolder; // 图片目录按主题区分 }; void CCustomToolBar::ApplyTheme(const ToolBarTheme theme) { m_theme theme; // 重新加载图片按主题目录 for (int i 0; i GetCount(); i) { CString strPath; strPath.Format(_T(%s\\btn_%d.png), theme.strImageFolder, i); // 从文件加载替换原有图片 LoadImageFromFile(i, strPath); } InvalidateRect(NULL); // 触发重绘 }ApplyTheme遍历按钮重新加载图片m_theme存当前主题OnPaint里用m_theme的颜色。换肤时调一次ApplyTheme就行不用重启程序。5.3 验证改造效果的几个检查点改完别急着提交按这几个点过一遍图片在高 DPI 下是否清晰文字和图片是否对齐按钮按下状态是否正常切换工具栏浮动停靠后是否错位资源 ID 是否冲突。我习惯在OnPaint里加个调试开关打开后画辅助线能直观看到每个按钮的矩形区域和文字位置调间距时特别有用。#ifdef _DEBUG // 调试模式画边框方便定位 dc.Draw3dRect(rectBtn, RGB(255, 0, 0), RGB(255, 0, 0)); dc.Draw3dRect(rectText, RGB(0, 255, 0), RGB(0, 255, 0)); #endif红色框是按钮区域绿色框是文字区域一眼就能看出哪里偏了。这个习惯是从一次调间距调到崩溃之后养成的从那以后我每次改工具栏布局都强制走一遍这个调试开关省得反复编译试。希望帮到你。本文还有配套的精品资源点击获取