☰
MFC中CToolBar自定义图标与文字的实现与避坑指南
2026/10/8 8:33:09 网站建设 项目流程

简介:面向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 与 SetButtonText

3.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_AUTOSIZE:

int 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. 自定义图片的进阶玩法:从位图换到 CImageList

4.1 换掉整张位图:用自己的图标序列代替默认按钮图

资源编辑器里的 BMP 位图虽然方便,但碰到 PNG 素材、动态生成图标、高 DPI 缩放,就力不从心了。更现代的做法是使用 CImageList 作为工具栏图片来源。Windows 工具栏本身支持 ImageList,MFC 的 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 声明为框架类成员,随窗口生命周期走。尺寸不匹配也会出问题:按钮大小 24x24,ImageList 图标却是 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 塞进 ImageList:

CImage 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、101),ON_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 精确控制,这套路稳得住。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询