
简介面向MFC开发者的工具栏自定义实践资源围绕CToolBar类讲解如何为按钮配置位图图片与显示文字并实现工具栏的停靠与浮动。适用对象是有一定C基础、希望优化Windows桌面应用界面交互的开发者。整合了完整Visual Studio工程rar压缩包共24个文件包含7个h头文件、5个cpp源码、rc资源脚本、ico图标、sln工程与示例exe等整体仅324KB便于快速下载对照已有1682人学习/下载。TimeClient示例项目完整演示了创建并初始化工具栏、用SetButtonInfo调整按钮属性、加载位图素材、处理DockControlBar/FloatToolBar停靠与浮动等关键流程。另附工具栏图片素材和说明文档可参考图片资源编辑、按钮文字定位与点击事件响应直接复用到自己的MFC工程中对于需要掌握MFC界面定制技巧的开发者能显著减少从零调试工具栏外观和停靠行为的成本。1. 为什么要在 MFC 里折腾 CToolBar 的图片和文字接手一个老 MFC 桌面工具时最常见的需求不是新功能而是把工具栏改成图标加文字。默认的 CToolBar 只画一排小图标用户根本看不明白每个按钮是干嘛的老板一句话把字加上你就得从位图资源、按钮样式、消息映射开始捋一遍。CToolBar 是 MFC 对 Windows 通用控件工具栏的封装图片来自位图或 ImageList文字来自 SetButtonText真正把两者同时显示出来需要同时搞定资源、样式和刷新时机。这篇文章就按我自己落地过的顺序讲清楚自定义图片、显示文字、动态换图、以及那些让人抓狂的坑。2. 从资源到代码CToolBar 工具栏的创建与图片绑定2.1 用资源编辑器建工具栏位图开始前先规划图片尺寸在 Visual Studio 里新建 MFC 项目默认会带一个 IDR_MAINFRAME 的工具栏资源。资源编辑器里那个网格画布实际是一张横向排列多个图标的 BMP 位图。每个按钮占一个等宽的小格最常见的是 16x16 像素也有 24x24、32x32。开始自定义之前先想好两个问题这套工具栏要多大图标以及要用几张。我一般会把图标尺寸定在 24x24兼顾显示效果和旧屏幕。位图颜色深度建议 8 位以上真彩 32 位更好。关键是透明色MFC 默认把位图中的洋红 RGB(255,0,255) 当作透明背景你画图标时底色必须是这个颜色不然按钮四周会出现一个刺眼的色块。在资源编辑器里逐格画每个图标占一格图标之间不要留缝隙否则运行时会出现按钮错位。2.2 用代码加载工具栏CreateEx 与 LoadToolBar 的参数资源画好了接下来在框架类 OnCreate 里创建工具栏。这一步是 MFC 里再常见不过的操作但每个参数都有实际影响if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME)) { TRACE0(无法创建工具栏\n); return -1; } m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY); DockControlBar(m_wndToolBar);逻辑说明CreateEx 负责创建窗口并设置工具栏风格。TBSTYLE_FLAT 是平面样式不设的话出来的是老式凸起 3D 按钮视觉上很过时。WS_CHILD | WS_VISIBLE 确保工具栏作为子窗口显示。CBRS_TOP 让它默认停靠在顶部CBRS_GRIPPER 显示可拖动的把手CBRS_TOOLTIPS 开启悬停提示CBRS_FLYBY 则让提示同时显示在状态栏。LoadToolBar 负责从资源加载位图并读取 .rc 文件里定义的按钮结构、字符串资源。参数说明LoadToolBar 是核心它做的事情很多加载位图、创建按钮数组、设置每个按钮的命令 ID、关联字符串资源中的工具提示。如果你资源画错了比如位图宽高不是按钮宽度整数倍LoadToolBar 返回 TRUE 但显示会乱。调试时第一个看 LoadToolBar 返回值第二个看 GetToolBarCtrl().GetButtonCount() 是否符合预期。2.3 按钮图片是怎么对应到按钮的一张位图里的多个图标LoadToolBar 加载的位图里第几个图标对应哪个按钮取决于 .rc 文件里的工具栏按钮列表。如果你不想改资源文件而是想用代码完全控制按钮顺序可以用 SetButtons 重排static UINT BASED_CODE buttons[] { ID_FILE_NEW, ID_FILE_OPEN, ID_SEPARATOR, ID_FILE_SAVE, ID_APP_ABOUT, }; m_wndToolBar.SetButtons(buttons, _countof(buttons));逻辑说明SetButtons 按数组顺序创建按钮ID_SEPARATOR 是分隔条它不占用位图索引。也就是说上面数组里 ID_FILE_NEW 对应位图第 0 个图标ID_FILE_OPEN 对应第 1 个图标ID_SEPARATOR 跳过ID_FILE_SAVE 对应第 2 个图标。这样位图与按钮的对应关系是顺序错位的自定义图片时最容易被搞晕。参数说明_countof(buttons) 给出按钮数组元素个数包含分隔条。SetButtons 之后Windows 工具栏控件会依据数组顺序建立内部索引但位图索引只按非分隔按钮排列。后续用 CommandToIndex 查按钮索引用 SetButtonText 设文字用到的都是按钮索引而不是位图索引很多人在这里踩坑。还要注意SetButtons 只设置按钮结构不修改位图。如果你先 LoadToolBar 再调用 SetButtons位图依然有效图片顺序按上面规则对应。如果想彻底换图片见第 4 章的 CImageList 方式。3. 文字显示与图片文字组合TBSTYLE_TEXT 与 SetButtonText3.1 用 SetButtonText 给按钮加文字先有 ID 再有文字图标就绪后加文字第一反应是 SetButtonText。这个函数按按钮索引设文本不是按命令 ID。所以稳妥写法是先用 CommandToIndex 转换int nIndex m_wndToolBar.CommandToIndex(ID_FILE_NEW); if (nIndex 0) m_wndToolBar.SetButtonText(nIndex, _T(新建)); nIndex m_wndToolBar.CommandToIndex(ID_FILE_OPEN); if (nIndex 0) m_wndToolBar.SetButtonText(nIndex, _T(打开));逻辑说明CommandToIndex 内部遍历工具栏按钮找到命令 ID 相同的按钮返回其索引。如果找不到返回 -1。SetButtonText 只改文本不改样式。即使马上调用按钮也不一定会显示文字因为默认工具栏样式没有启用文本绘制。参数说明_T(新建) 是 Unicode 兼容写法。如果你在 ANSI 工程里用 LPCTSTR 或 CString 都行。我习惯把按钮文字统一放到一个初始化函数里避免散落。还有一个细节分隔条也能设文字但没意义你只需要 Care 到 ID_SEPARATOR 跳过即可。3.2 同时显示图片和文字TBSTYLE_TEXT 与按钮自动大小这是整个自定义里最容易失败的环节。只 SetButtonText但工具栏窗口样式没有 TBSTYLE_TEXT那么按钮文字根本不显示。设置样式的常见做法是m_wndToolBar.GetToolBarCtrl().SetStyle( m_wndToolBar.GetToolBarCtrl().GetStyle() | TBSTYLE_TEXT | TBSTYLE_LIST | TBSTYLE_AUTOSIZE);逻辑说明TBSTYLE_TEXT 是让工具栏绘制按钮文本的总开关。TBSTYLE_LIST 让文本显示在图片右侧而不是图片下方如果你按工具栏按钮属性里勾了显示文字但没加 LIST文字可能出现在图标底下把工具栏撑得很高。TBSTYLE_AUTOSIZE 让按钮宽度根据文本和图片自动计算不加的话按钮保持默认大小文字会被截断。参数说明这三个样式要一起使用。有些资料只提 TBSTYLE_TEXT结果发现文字和图片重叠就是因为没有加 LIST 或 AUTOSIZE。设置样式后应再刷新一次工具栏m_wndToolBar.RedrawWindow();此外每个按钮自身的样式也影响布局。MFC 的按钮默认样式是 TBBS_BUTTON如果要自动大小需要为每个按钮加上 TBBS_AUTOSIZEint nIdx m_wndToolBar.CommandToIndex(ID_FILE_NEW); m_wndToolBar.SetButtonStyle(nIdx, TBBS_BUTTON | TBBS_AUTOSIZE);这里 TBBS_AUTOSIZE 的作用是告诉 Windows这个按钮的宽度要按内容自适应。如果不设即使工具栏有 AUTOSIZE 风格某些旧版 MFC 下按钮还是固定宽度。我一般对每个要显示文字的按钮都调用一次。3.3 字符串资源、工具提示和按钮文字的相爱相杀MFC 的工具栏和字符串资源关系很微妙。LoadToolBar 加载时会为每个按钮 ID 查找字符串表通常一条字符串格式是状态栏提示\n工具提示。例如 ID_FILE_NEW 的字符串是新建文件\n新建。这时工具栏悬停提示会显示新建状态栏也会显示新建文件。但注意这条字符串并不会自动变成按钮上的文字。资源编辑器里工具栏按钮有文本属性你填写后VC 资源编译器会生成相应的按钮文本信息但那也是靠代码调用 SetButtonText 或内部结构实现的代码动态创建的按钮不会自动带。我建议动态按钮用一套统一逻辑先 LoadString 读提示再截断换行符设置按钮文字。这样保证按钮文字和提示一致CString strText; strText.LoadString(ID_FILE_NEW); int nSep strText.Find(_T(\n)); if (nSep 0) strText strText.Left(nSep); int nIdx m_wndToolBar.CommandToIndex(ID_FILE_NEW); m_wndToolBar.SetButtonText(nIdx, strText);这样按钮文字显示新建文件提示也是同一来源避免两处不一致。如果你只想显示简短文字就在字符串表里把换行前的内容做成新建。这段逻辑是我实际项目中一直用的省了很多维护成本。4. 自定义图片的进阶玩法从位图换到 CImageList4.1 换掉整张位图用自己的图标序列代替默认按钮图资源编辑器里的 BMP 位图虽然方便但碰到 PNG 素材、动态生成图标、高 DPI 缩放就力不从心了。更现代的做法是使用 CImageList 作为工具栏图片来源。Windows 工具栏本身支持 ImageListMFC 的 CToolBarCtrl 也提供了 SetImageList。用 ImageList 的好处是图标可以来自 ICO、PNG也可以运行时替换单个图标还能轻松做到 32 位真彩透明。具体做法是创建 CImageList把图标添加进去然后替换工具栏图片列表// m_ImgList 声明为 CMainFrame 成员变量 bool bOK m_ImgList.Create(24, 24, ILC_COLOR32 | ILC_MASK, 0, 1); if (!bOK) return; HICON hIcon ::LoadIcon(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_NEW)); m_ImgList.Add(hIcon); // 索引 0 hIcon ::LoadIcon(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_OPEN)); m_ImgList.Add(hIcon); // 索引 1 hIcon ::LoadIcon(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_SAVE)); m_ImgList.Add(hIcon); // 索引 2 m_wndToolBar.GetToolBarCtrl().SetImageList(m_ImgList);逻辑说明Create 的第一个参数是图标宽高必须与按钮尺寸一致。ILC_COLOR32 表示 32 位真彩ILC_MASK 启用透明处理。Add 依次把 HICON 添加到列表返回的索引从 0 开始。SetImageList 把传入的 CImageList 指针设为工具栏新的图片源。参数说明CImageList 对象必须持久存活不能是局部变量。我曾经把 m_ImgList 局部创建结果工具栏重绘时访问悬空指针画面直接花掉程序不定时崩溃。后来一直坚持把 CImageList 声明为框架类成员随窗口生命周期走。尺寸不匹配也会出问题按钮大小 24x24ImageList 图标却是 16x16按钮会留白或者被拉伸变形。可以先调用 GetToolBarCtrl().GetButtonSize 查看当前按钮尺寸。4.2 动态换单个按钮图标维护好索引映射用 ImageList 之后换单个图标很直观HICON hNewIcon (HICON)::LoadImage(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_NEW2), IMAGE_ICON, 24, 24, LR_DEFAULTCOLOR); if (hNewIcon) { m_ImgList.Replace(0, hNewIcon); // 替换索引 0 m_wndToolBar.GetToolBarCtrl().SetImageList(m_ImgList); ::DestroyIcon(hNewIcon); }这里坑就来了Replace 的索引是 ImageList 索引不是按钮索引。如果工具栏按钮序列里没有分隔条按钮索引和 ImageList 索引恰好一致一旦混入 ID_SEPARATOR后面的按钮索引就会比 ImageList 索引大 1、大 2。比如按钮数组是 新建、打开、分隔、保存那么保存的按钮索引是 3但它对应的 ImageList 索引是 2。以 CommandToIndex 为准去 Replace必然换错图标。我的做法是维护一张映射表int m_nImagIndexForCmd[ID_LAST - ID_FIRST 1];或者直接用数组索引关联。更简单的方式是把 ImageList 索引存到按钮的 dwData 字段里用 TB_SETBUTTONINFO 写进去读取时用 CommandToIndex 找到按钮再取。这样即使顺序变化也不出错。4.3 用 PNG 做图标从 CImage 到 HICON 的转换MFC 工具栏原生态不支持 PNG但 PNG 素材在现代 UI 里更常用。可以通过 CImage 加载 PNG再转成 HICON 塞进 ImageListCImage img; if (FAILED(img.Load(_T(reources\\\\new.png)))) return; CBitmap bmp; bmp.Attach(img.Detach()); // 把位图转换为图标 ICONINFO ii {0}; ii.fIcon TRUE; ii.hbmColor bmp; // 需要得到 HBITMAP ii.hbmMask nullptr; HICON hIcon ::CreateIconIndirect(ii); if (hIcon) { m_ImgList.Add(hIcon); ::DestroyIcon(hIcon); }注意点CImage::Load 失败时会返回 E_FAIL检查文件路径和工程工作目录。转换时位图最好已经带有透明通道PNG 的 alpha 通道会被保留CreateIconIndirect 得到的图标背景就是透明的。如果转换出来背景发黑说明位图没有 alpha 通道需要先用 BitBlt 预乘或更换素材。这一套下来工具栏图标就彻底摆脱了 BMP 的束缚。4.4 禁用按钮的显示灰化效果并不总是自动的用位图时MFC 会自动处理禁用按钮的灰度。用 CImageList 32 位图标后Windows 的灰化处理经常失效禁用状态的按钮看起来和正常状态没区别。这是因为高彩图标没有对应的无光版本系统只能对位图做半透明灰化对 PNG 的透明区域会留下灰色底。解决办法有两个方向一是准备一套灰度图标在状态切换时替换 Replace二是响应工具栏的 NM_CUSTOMDRAW 通知在 CDDS_PREPAINT 阶段用灰色滤镜绘制。NM_CUSTOMDRAW 是 Windows 通用控件的自绘机制我可以给一个简单示例BEGIN_MESSAGE_MAP(CMyToolBarCtrl, CToolBarCtrl) ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, CMyToolBarCtrl::OnCustomDraw) END_MESSAGE_MAP() void CMyToolBarCtrl::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) { NMCUSTOMDRAW* pDraw (NMCUSTOMDRAW*)pNMHDR; if (pDraw-dwDrawStage CDDS_PREPAINT) *pResult CDRF_NOTIFYITEMDRAW; else if (pDraw-dwDrawStage CDDS_ITEMPREPAINT) { DWORD dwState ((NMTBCUSTOMDRAW*)pNMHDR)-nmcd.dwItemSpec; // 根据按钮状态决定是否用灰色画笔 *pResult CDRF_NOTIFYPOSTPAINT; } else *pResult CDRF_DODEFAULT; }这里是自定义控件的方式需要从 CToolBarCtrl 派生一个类。如果你不想动派生可以处理主窗口的反射通知但代码量差不多。我实际项目中为了快速交付直接用两套图标替换效果最可控。5. 避坑CToolBar 自定义图片和文字的常见问题排查5.1 图片不显示但文字在位图尺寸与资源加载失败现象按钮上只有文字没有图标或者图标位置是一块空白。原因最常见的是 LoadToolBar 加载的位图资源没有被正确绑定。另一个常见原因是位图尺寸与按钮尺寸不一致。CToolBar 的 LoadToolBar 会根据位图中的总宽度除以按钮数计算每个图标宽度如果位图高度不是按钮高度图标会被裁掉。解决先检查 LoadToolBar 返回值。返回 FALSE说明资源 ID 写错或位图不完整。返回 TRUE 但没图用 GetToolBarCtrl().GetBitmap() 确认位图句柄是否有效再用 GetToolBarCtrl().GetButtonSize() 查看按钮宽高。比如位图是 16x16按钮尺寸却被 SetSizes 改成了 24x24图片就会藏在按钮左上角。解决办法是重新设置位图大小m_wndToolBar.SetSizes(CSize(24, 24), CSize(24, 24));第一个参数是按钮总尺寸含边距第二个参数是图标尺寸。两个参数必须与位图资源匹配别拍脑袋填。5.2 文字被截断或与图片重叠宽度没有自动计算现象设置文字后文字只露出前两个字或者文字压在图标上按钮整体很小。原因工具栏或按钮缺少 TBSTYLE_AUTOSIZE / TBBS_AUTOSIZE导致按钮宽度按默认的固定值绘制。另外一个隐藏原因SetButtonText 之后工具栏没有重新布局已经绘制出来的按钮区域不会自己变宽。解决添加样式并强制自动布局。我写过一段最小修复代码// 给每个按钮补上自动大小样式 for (int i 0; i m_wndToolBar.GetToolBarCtrl().GetButtonCount(); i) { UINT nStyle m_wndToolBar.GetButtonStyle(i); m_wndToolBar.SetButtonStyle(i, nStyle | TBBS_AUTOSIZE); } // 让工具栏重新计算整体尺寸 m_wndToolBar.GetToolBarCtrl().AutoSize(); m_wndToolBar.RedrawWindow();AutoSize 会遍历所有按钮根据文字和图标重算宽度。如果还是不够那就是字体太大可以手动设置更大按钮尺寸m_wndToolBar.GetToolBarCtrl().SetButtonSize(CSize(80, 32));我一般用 AutoSize 就够手动设宽度属于后备手段。5.3 按钮点击没反应命令 ID 和消息映射范围现象按钮能正常显示图标文字都对但单击后没有任何反应按钮按下去也不弹起。原因MFC 命令消息的传递范围要求命令 ID 在 0x8000 到 0xDFFF 之间。如果你在 Resource.h 里自己定义了低编号 ID例如 100、101ON_COMMAND 可能映射不上。此外消息映射表里可能漏写了处理函数或者把处理函数写在错误的类中。解决先在 Resource.h 里检查 ID。如果自定义 ID 接近 1000通常没问题但低于 32768 就要小心。顺手用ON_COMMAND(ID_MY_TOOL, CMainFrame::OnMyTool)在 CMainFrame 消息映射里注册。如果按钮在 View 里也有需要就别在框架里处理直接在 CView 的消息映射里写MFC 会自动按视图链传递。排查时用 Spy 或输出日志确认按钮命令确实发出。常见陷阱是 SetButtons 与 LoadToolBar 之间 ID 数组中出现了 ID_SEPARATOR导致 CommandToIndex 找不到你点击的 ID这时候 SetButtonText 也会失败但不是点击无响应的原因。5.4 工具栏背景脏、有残留透明与 DPI 问题现象拖动工具栏后按钮周围出现黑块或渐变残留在 4K 屏幕上按钮明显变小图标模糊。原因透明色处理不一致。位图里使用了洋红透明但 CImageList 用了 ILC_COLOR32透明通道没有正确设置黑色残留就会显示出来。DPI 方面MFC 默认按系统 DPI 缩放位图但 16x16 的小图标拉大后自然模糊。解决位图场景统一透明色为 RGB(192,192,192) 或洋红并确认工具栏风格包含 TBSTYLE_FLAT 和 TBSTYLE_TRANSPARENT。ImageList 场景确保 ILC_COLOR32 且 PNG 自带 alpha。DPI 问题在程序初始化时调用 SetProcessDpiAwareness并用图标原尺寸的 2 倍图SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);然后用m_wndToolBar.GetToolBarCtrl().SetButtonSize(CSize(32, 32))配合 32x32 图标。这样在高分屏上是整数放大不再糊。老项目如果突然接入 DPI 感知需要整体评估对话框字体和布局不建议只改这一处。6. 进阶技巧用 TB_SETBUTTONINFO 一次搞定文字与图片索引自定义图片和文字很多人都是先 SetButtonText 再 SetButtonStyle最后还要刷新。实际上 Windows 工具栏有一个更底层的消息 TB_SETBUTTONINFO可以一次设置文本、图片索引和按钮样式。MFC 的 CToolBarCtrl 封装了 SetButtonInfo我建议直接用它。TBBUTTONINFO tbi {0}; tbi.cbSize sizeof(TBBUTTONINFO); tbi.dwMask TBIF_TEXT | TBIF_IMAGE | TBIF_STYLE; tbi.pszText _T(新建); tbi.iImage 0; // ImageList 中的索引 tbi.fsStyle TBSTYLE_BUTTON | TBSTYLE_AUTOSIZE; m_wndToolBar.GetToolBarCtrl().SetButtonInfo( m_wndToolBar.CommandToIndex(ID_FILE_NEW), tbi);这段代码直接把按钮文字、图片索引、自动大小样式一次写入。dwMask 告诉系统哪些字段有效避免覆盖你没设置的字段。使用它之后不再需要单独调用 SetButtonText 和 SetButtonStyle刷新也更干净。这个技巧适合批量初始化按钮文字时用配合一个列有命令 ID 和文字的资源表几行循环就能把所有按钮设置完。调试工具栏还有一个习惯用 Spy 查看工具栏子窗口的样式位。打开工具栏所在窗口展开子窗口列表找到 Windows 通用控件工具栏类右键属性查看样式。如果 TBSTYLE_TEXT 没有生效样式值里看不到这一位问题大概率在前面设置样式的代码没执行或顺序不对。另外SetButtonInfo 设置完可以立即用 GetButtonInfo 读回来检查 tbi.pszText 是否正确落盘。这个验证方法能快速区分是 MFC 缓存没刷新还是底层控件真的没收到。我自己早期做定制时经常因为在局部变量里创建 CImageList 而翻车后来养成习惯所有图像容器、位图句柄都放窗口类成员里生命周期跟窗口保持一致。工具栏这种东西改图改字看似简单但涉及资源、样式、自绘三层任何一个环节没同步效果就跟你预期差很远。先把基础创建理顺再用 SetButtonInfo 精确控制这套路稳得住。希望帮到你。本文还有配套的精品资源点击获取