☰
MFC CToolBar 工具栏自定义图片和文字显示完整指南
2026/10/12 2:39:33 网站建设 项目流程

简介:一套MFC CToolBar工具栏自定义示例资源,面向Windows桌面应用开发者,解决工具栏按钮需同时展示图片与文字,以及自由停靠、浮动等常见定制需求。压缩包共24个文件,整体约324KB,以7个头文件、5个C++源文件、图标、资源脚本和说明文档为主,并附有独立的工具栏图片素材及代码包,工程结构完整,便于直接对照使用。已有1683人浏览学习。内容围绕CToolBar的创建与初始化、按钮位图加载、文字显示、SetButtonInfo属性设置,以及DockControlBar和FloatToolBar停靠浮动机制展开,配合TimeClient示例工程与代码使用文档,可帮助开发者理解并落地MFC中外观和交互均可自定义的工具栏。资源将按钮图片素材、窗口框架代码与使用说明打包在一起,既适合刚接触MFC界面开发的初学者按例程逐步上手,也适合需要快速改造工具栏的开发者直接复用,省去自行整理位图和配置属性的时间。

1. MFC CToolBar:为什么需要同时显示图片和文字

如果你维护过稍微有点年头的 MFC 业务系统,大概率见过那种只有图标、鼠标悬停才知道含义的 CToolBar。操作人员每天守着“齿轮”“软盘”“放大镜”这些抽象图画,新人上手第一周全在记按钮位置。把文字直接显示到按钮上,是降低误操作成本、让软件更符合业务直觉的最直接手段。MFC CToolBar 工具栏自定义图片及文字,乍一看只是加一行样式和一行字符串,真正落地时却绕不开按钮位图索引、TBSTYLE_LIST 样式、工具栏控件与 MFC 包装类之间的层级关系。这篇文章就沿着这条主线,把原理、代码、参数和踩坑经验一次说清。

2. CToolBar 的按钮数据结构与文字显示机制

2.1 理解 CToolBar 与 CToolBarCtrl 的关系

MFC 的 CToolBar 本质是 Win32 工具栏控件的 C++ 包装壳。你平时调用的 LoadToolBar、SetButtonText、SetSizes 都是封装后的便捷方法,最终很多操作会落到内部的 CToolBarCtrl 上。CToolBar 负责停靠、布局、与 CFrameWnd 的配合,而 CToolBarCtrl 负责按钮数组、图像列表、按钮状态和底层消息。所以“按钮是否显示文字”这类属性,并不完全由 CToolBar 的停靠样式决定,而是要看底层工具栏控件是否具备 TBSTYLE_LIST。

有个常见的翻车点:有人直接调用m_wndToolBar.SetStyle(TBSTYLE_LIST),但 CToolBar::SetStyle 处理的是 CBRS_* 停靠样式,对按钮文本显示没有任何作用。正确做法是通过GetToolBarCtrl()获得底层控件,再设置工具栏控制样式。记住 CToolBar 不是根本,CToolBarCtrl 才是按钮数据和样式的大本营,遇到任何与按钮本身相关的“玄学”问题,先回到这一层排查。

写代码时,我们可以把 CToolBarCtrl& 的引用长久持有,但要注意工具栏可能被浮动窗口重新创建,因此更建议每次用GetToolBarCtrl()临时取引用,避免保存悬空引用导致黑匣子式崩溃。CToolBar 在浮动停靠、切换 DPI 后会重建内部窗口,这个细节容易被忽略。

2.2 TBBUTTON:按钮图片、命令和文字是怎么绑定在一起的

在底层工具栏控件里,每个按钮对应一个 TBBUTTON 结构体。几个关键字段:

  • iBitmap:按钮图片在图像列表里的索引,它决定显示哪张图。
  • idCommand:按钮被点击时发给父窗口的命令 ID,一般就是菜单项的 ID。
  • fsState:按钮状态,比如启用、隐藏、选中。
  • fsStyle:按钮风格,比如普通按钮、分隔符、复选框按钮。
  • iString:按钮文字在内部字符串池中的索引或指针。

MFC 的 SetButtonText 实际就是把一个字符串写到 iString 指定的位置,再由工具栏控件绘制出来。这个结构说明了一个重要事实:图片和文字是分离的,你完全可以让同一张图标配不同文字,也可以同一个命令 ID 在不同语言环境显示不同的文案,而不必重新加载资源。

按钮在工具栏里的物理索引和命令 ID 并不相等。资源编辑器里放了 7 个按钮,顺序上第一个按钮的索引是 0,第二个是 1,但第一个按钮可能对应 ID_FILE_NEW,第二个对应 ID_APP_ABOUT。业务代码不要写死索引,否则用户拖拽、隐藏按钮后全乱套。最稳妥的转换方式是用CommandToIndex(命令ID)取得索引,再传给 SetButtonText、SetButtonInfo 等。

TBBUTTON 的 fsStyle 还支持 TBBS_CHECKBOX 这类需要点击后保持选中态的按钮。如果这个按钮还要同时显示文字,你需要给它足够宽的按钮区域,因为复选框占用的视觉空间比普通按钮多出一小截。过去我见过把“启用”按钮做成普通按钮,导致选中状态毫无反馈的体验事故。这类细节,数据结构和样式机制能帮你提前绕开。

2.3 TBSTYLE_LIST 与 TBSTYLE_AUTOSIZE:图片和文字共存的底层开关

要让按钮同时显示图片和文字,必须给工具栏控件设置两个关键样式:

  • TBSTYLE_LIST:允许按钮以“图标+文本”模式显示,文字绘制在图标右侧。
  • TBSTYLE_AUTOSIZE:根据按钮文本长度自动计算按钮宽度,避免长文案被截断。

设置方式如下:

CToolBarCtrl& ctrl = m_wndToolBar.GetToolBarCtrl(); ctrl.SetStyle(ctrl.GetStyle() | TBSTYLE_LIST | TBSTYLE_AUTOSIZE);

这里先用GetStyle()读取控件已有样式,再用按位或合并新样式。不能直接整体赋值,否则会丢掉 TBSTYLE_FLAT、 TBSTYLE_TRANSPARENT 这类原有标志。LoadToolBar 之后才设置可以避免资源加载过程覆盖样式,但这也不是绝对要求,只要保证在绘制按钮前生效即可。

一点容易混淆:CToolBar::SetSizes 里的按钮尺寸与 TBSTYLE_AUTOSIZE 的关系。如果设置了 AUTOSIZE,按钮宽度会优先跟随文字实际像素宽度,SetSizes 中给的 sizeButton.cx 更像最小值。如果你希望所有按钮等宽且不随文字伸缩,就不要开 AUTOSIZE,而是通过 SetSizes 给定足够的宽度,并手动处理长文字。反之,如果希望短文字按钮窄、长文字按钮宽,则开 AUTOSIZE 更省心,但会带来按钮宽度参差不齐的观感。

3. 用 CToolBar 实现图片和文字共存:从零到可用

3.1 在资源编辑器中准备工具栏位图与按钮布局

标准流程是在资源编辑器里创建工具栏资源,比如 IDR_MAIN_TOOLBAR。插入按钮后,每个按钮默认只有图标,没有文字;图标来自配套位图资源。MFC 在加载时会根据按钮要显示的单张图片尺寸把位图切分,因此位图宽度必须严格等于“单张图标宽度 × 图标数量”。如果你的按钮图标是 16×16,共 4 个图标,位图宽度即为 64 像素,高度 16 像素。多一像素或少一像素,都会在运行时出现个别按钮图片错位甚至显示残边。

资源编辑器里通常可以设置“透明色”,工具栏位图会用第一个像素的颜色作为掩码色。建议不要用黑色、白色做图标底色,否则图标边缘也会被挖空。常规做法是将透明色设为鲜亮的洋红或亮绿色,保证图标本身不包含这些颜色。

按钮顺序上,分隔符也算一个按钮数组元素。分隔符用 idCommand=0 表示,它在工具栏上表现为一条竖线。你在 CommandToIndex 时拿到的是包括分隔符在内的综合索引,例如第一个命令按钮前有两个分隔符,它的索引就是 2。不要手工去给每个按钮编号,把CommandToIndex当成唯一的索引来源即可。

3.2 创建工具栏并配置文字显示的最小完整代码

在主框架的 OnCreate 里,常见做法是:

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CFrameWndEx::OnCreate(lpCreateStruct) == -1) return -1; // 1. 创建工具栏窗口 if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC)) { TRACE0("工具栏创建失败\n"); return -1; } // 2. 加载工具栏资源 if (!m_wndToolBar.LoadToolBar(IDR_MAIN_TOOLBAR)) { TRACE0("工具栏资源加载失败\n"); return -1; } // 3. 开启按钮文字显示 + 自动宽度 CToolBarCtrl& ctrl = m_wndToolBar.GetToolBarCtrl(); ctrl.SetStyle(ctrl.GetStyle() | TBSTYLE_LIST | TBSTYLE_AUTOSIZE); // 4. 给每个命令按钮写文字 SetToolbarButtonText(ID_FILE_OPEN, _T("打开文件")); SetToolbarButtonText(ID_FILE_SAVE, _T("保存文件")); SetToolbarButtonText(ID_EDIT_COPY, _T("复制")); // 5. 设置图标和按钮总尺寸 CSize sizeImage(16, 16); CSize sizeButton(60, 32); m_wndToolBar.SetSizes(sizeButton, sizeImage); return 0; } void CMainFrame::SetToolbarButtonText(UINT nCmdID, LPCTSTR lpszText) { int nIndex = m_wndToolBar.CommandToIndex(nCmdID); if (nIndex >= 0) { m_wndToolBar.SetButtonText(nIndex, lpszText); m_wndToolBar.Invalidate(); } }

步骤 3 是显示文字的核心。步骤 4 里每次 SetButtonText 后调用 Invalidate,是为了强制工具栏立刻重绘,避免 DWM 缓存下文字延迟出现。步骤 5 的 SetSizes 告诉工具栏“单个图标的裁剪尺寸”和“整个按钮的最小矩形”。常见的图标尺寸是 16×16 或 32×32,按钮尺寸需要额外容纳文字区域和左右边距。

如果按钮文字是中文字符,尽量把 sizeButton.cx 留到 58 像素以上,因为两个汉字加边距和图标间距,16 像素图标下至少需要 26 像素文字区。具体的做法是用字体计算,后文会给出更稳的公式。

3.3 运行时更换按钮图标:SetButtonInfo 与图像列表的取舍

有时候不想为了换图标改动资源文件,需要程序运行时替换按钮图片。常见路线是用 SetButtonInfo 修改 iBitmap 索引,或直接更换整个 ImageList。前者适合单张替换,后者适合整体换肤。

单张替换的可靠方法是先用 AddBitmap 把新位图加入图像列表,再把按钮的图片索引指向它:

int nNewBtnIndex = m_wndToolBar.GetToolBarCtrl().AddBitmap(1, IDB_NEW_TOOLBAR_BTN); int nButton = m_wndToolBar.CommandToIndex(ID_ACTION_START); m_wndToolBar.SetButtonInfo(nButton, ID_ACTION_START, TBBS_BUTTON, nNewBtnIndex);

AddBitmap 会根据当前图像尺寸把位图切分成多张图,返回值是新图像在列表里的起始索引。如果 IDB_NEW_TOOLBAR_BTN 中包含多张图,则返回第一张的索引,后续图片依次排列。这种方式最便于用一张位图放多个图标,不需要单独管理 CImageList 生命周期。

如果要替换按钮的图标但又不想动这个按钮的其他风格,比如它原本是 TBBS_CHECKBOX,直接调用 SetButtonInfo 会把风格覆盖成普通按钮。更细致的做法是读取当前按钮信息再修改:

CToolBarCtrl& ctrl = m_wndToolBar.GetToolBarCtrl(); TBBUTTONINFO tbi = { sizeof(TBBUTTONINFO) }; tbi.dwMask = TBIF_IMAGE | TBIF_STYLE; ctrl.GetButtonInfo(nButton, &tbi); tbi.dwMask = TBIF_IMAGE; tbi.iImage = nNewBtnIndex; ctrl.SetButtonInfo(nButton, &tbi);

这里 GetButtonInfo 先用 TBIF_IMAGE | TBIF_STYLE 把按钮当前风格和图片索引读出来,再只设置 TBIF_IMAGE,把新图片索引写进去,风格字段保持不变。逻辑上就是“读老值、改目标位、只写目标字段”。使用底层 SetButtonInfo 时,dwMask 必须包含要修改的字段,否则调用会被忽略。

整体更换图像列表则适合多按钮场景:

CImageList m_iconList; // 一定是成员变量,不能是局部变量 m_iconList.Create(32, 32, ILC_COLOR32 | ILC_MASK, 4, 4); m_iconList.Add(AfxGetApp()->LoadIcon(IDI_NEW_ICON_1)); m_iconList.Add(AfxGetApp()->LoadIcon(IDI_NEW_ICON_2)); m_wndToolBar.GetToolBarCtrl().SetImageList(&m_iconList);

此时按钮的 iBitmap 要重新设置为新列表中的编号。需要特别注意的是,m_iconList 必须被保存为窗口类的成员变量,否则工具栏控件持有的指针会指向已销毁对象,绘制时出现闪退。这种问题几乎每次都会让开发者排查大半天,属于血泪教训。

3.4 用字体测量撑开按钮宽度

工具栏文字是否被截断,直接看字体大小和字符串长度。用 SetSizes 拍脑袋给个固定值不是长久之计,靠谱手段是调用 CDC::GetTextExtent 量出字符串像素宽度,再换算成按钮宽度:

CClientDC dc(&m_wndToolBar); CFont* pOldFont = dc.SelectObject(m_wndToolBar.GetFont()); CSize textSz = dc.GetTextExtent(strButtonText); dc.SelectObject(pOldFont); SIZE sizeBtn = m_wndToolBar.GetButtonSize(); sizeBtn.cx = max(sizeBtn.cx, textSz.cx + 28); sizeBtn.cy = max(sizeBtn.cy, textSz.cy + 8); m_wndToolBar.SetSizes(sizeBtn, CSize(16, 16));

加 28 像素是为了给图标、图标与文字的间距、按钮左右边距留出空间。加 8 像素是上下边距。这段代码要在设置所有按钮文字之后执行,因为 GetButtonSize 返回的是当前按钮尺寸,你需要用所有文字里最大的那个宽度作为统一尺寸,使按钮看起来整齐。

如果工具栏被用户拖拽成浮动状态,按钮尺寸仍然受 SetSizes 限制,浮动窗口会根据停靠方向自动重排,但在垂直浮动时按钮布局会很不自然。此时文字建议不显示,或保持工具栏固定停靠。MFC 的 CBRS_SIZE_DYNAMIC 虽然允许拖动改变工具栏布局,但垂直状态下文字和图标会挤成一排,体验很糟。

4. CToolBar 自定义图片和文字的避坑清单

4.1 按钮文字不显示

现象:调用了 SetButtonText,但工具栏上依旧只有图标。

原因:没有给底层工具栏控件设置 TBSTYLE_LIST,或者设置时机不对。部分代码在 LoadToolBar 之前调用 GetToolBarCtrl().SetStyle,资源加载过程会把样式重置。

解决:在 LoadToolBar 之后执行以下逻辑:

CToolBarCtrl& ctrl = m_wndToolBar.GetToolBarCtrl(); ctrl.SetStyle(ctrl.GetStyle() | TBSTYLE_LIST);

设置后强制m_wndToolBar.Invalidate()。如果还没有文字,检查按钮索引是否合法,CommandToIndex返回 -1 就表示命令 ID 没有对应按钮。

4.2 图标和文字互相挤压、按钮宽度不足

现象:打开文字显示后,按钮挤成一团,长文字被截断,甚至文字和图标重叠。

原因:SetSizes 中 sizeButton 宽度太小,且没有 TBSTYLE_AUTOSIZE 自动撑开。另一种情况是开了 AUTOSIZE 但文字是在设置样式之前写入的,宽度没有重新计算。

解决:先写好所有按钮文本,再调用一次 AutoSize。如果要求等宽按钮,关闭 AUTOSIZE 后,用 GetTextExtent 测出最长文字,统一扩大 sizeButton.cx。如果只是个别按钮过长,可以给该按钮单独设置宽一点的尺寸,但注意 CToolBar 没有“单按钮宽度”的直接接口,统一尺寸更省心。

4.3 更换图标后其他按钮图片错位

现象:用 AddBitmap 或 SetButtonInfo 换图后,原来正常的按钮图片突然错位,显示成半张图。

原因:位图资源宽度不是单张图标尺寸的整数倍,导致 MFC 切分图标时把最后一张切歪。也可能是修改 image index 时没有保留原按钮风格,造成按钮被破坏。

解决:检查新位图的宽度。例如单张图标 16×16,位图放 4 个图标,宽度必须是 64。如果位图里只画了 3 个图标却声明 4 个索引,最后一个索引就会指向位图边界外的数据。设置前先 GetButtonInfo 读取原风格,再用 TBBUTTONINFO 只改 TBIF_IMAGE 字段,避免把 TBBS_CHECKBOX 之类风格冲掉。

4.4 高DPI下图标模糊、文字偏移

现象:在 125% 或 150% 缩放的 Windows 上,工具栏图标边缘发糊,文字和图标中心不在一条水平线上。

原因:传统 MFC 工具栏位图没有为高分辨率准备,系统只能做拉伸。CToolBarCtrl 内部按像素计算按钮矩形,缩放后字体和图片的基准线不同。

解决:如果项目允许,把工具图标改为 ICO 或 PNG,并在 ImageList 中载入大尺寸;或者在进程启动时调用 SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2) 关闭系统强制缩放。文字偏移可以用 SetSizes 加大按钮高度,让图标垂直居中区比文字区域更高,抵消系统字体基线差异。V2 感知下按钮文字会更接近预期。

4.5 动态切换文字后布局不更新

现象:程序运行中切换了多语言,SetButtonText 后文字变了,按钮宽度依然是旧尺寸,长文字被裁剪。

原因:工具栏按钮宽度没有失效,或者 AUTOSIZE 没有触发。MFC 的 SetButtonText 不会自动重算宽度,需要手动通知。

解决:在设置所有文本后依次调用:

m_wndToolBar.GetToolBarCtrl().AutoSize(); m_wndToolBar.RecalcLayout(); RecalcLayout(); // 这里调用主框架的

AutoSize 负责重算按钮宽度,RecalcLayout 负责通知主窗口重新停靠布局。如果文字变短但按钮不肯缩回去,那是 AUTOSIZE 没生效,关闭后再用 SetSizes 强制给一个统一最大宽度。

5. 进阶:用数据表驱动工具栏文字、图标和用户偏好

我想分享一个特别适合中大型项目的习惯:不要在每个 OnCreate 里写一长串 SetButtonText,而是把“命令 ID、图片索引、文字”塞进一个静态数组中,用循环统一布置。这样后期增加按钮、切换语言、调整图标都只改表结构,不碰流程代码。

struct TOOLBAR_ITEM { UINT nCmdID; UINT nIconIdx; LPCTSTR lpszText; }; static TOOLBAR_ITEM items[] = { { ID_FILE_NEW, 0, _T("新建") }, { ID_FILE_OPEN, 1, _T("打开") }, { ID_FILE_SAVE, 2, _T("保存") }, { ID_APP_EXIT, 3, _T("退出") }, };

遍历时用 CommandToIndex 把 ID 换成按钮索引,然后依次 SetButtonInfo 换图、SetButtonText 写字。这样“图片和文字对应关系”一眼就能看清楚。

在此基础上还可以给每个 item 增加一个BOOL bShowText字段,用来响应用户的“显示文字”开关。当用户只需要图标时,从样式里去掉 TBSTYLE_LIST:

CToolBarCtrl& ctrl = m_wndToolBar.GetToolBarCtrl(); ctrl.SetStyle(ctrl.GetStyle() & ~TBSTYLE_LIST); ctrl.AutoSize();

去掉样式后文字会立即消失,按钮宽度收到 AUTOSIZE 影响回到只适应图标的状态,整个工具栏看起来更紧凑。恢复时再加回 TBSTYLE_LIST,再循环写一遍按钮文字,因为样式切换不会保留旧文字绘制。

数据表驱动还有一个隐藏好处:多语言切换时,你只需要准备多份字符串表,程序启动时根据语言选择其中一份填入数组,再统一重建工具栏。这比在运行时频繁修改单个按钮文字可靠得多,因为重建后布局必然恢复,不会再出现残留宽度。

最后说句踩坑后的习惯:无论用什么方式维护 CToolBar,都先确认资源编辑器里按钮的数量和数组中条目数量一致。我曾经在数组里新增了一个按钮,却忘记在资源编辑器中插入按钮,启动时直接断言崩溃,排查了整整一下午。现在我在工具栏创建处加了一个ASSERT(_countof(items) <= toolbarButtonCount);,每次改动都能第一时间暴露。希望帮到你。

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

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

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

立即咨询