☰
MFC工具栏扩展实战:CToolBarEx实现下拉菜单与图标文字混排
2026/10/9 6:24:39 网站建设 项目流程

简介:这份资源围绕CToolBarEx在Visual C++中的实际应用展开,面向具备MFC基础、希望提升Windows界面交互效果的C++开发者。CToolBarEx在标准CToolBar基础上扩展出自定义按钮图像、下拉菜单、图标文字混排及禁用、隐藏、分组等状态管理能力,资源通过一个可运行的示例工程演示按钮添加、消息映射绑定、点击事件重载与按钮样式设置等关键环节,帮助读者理解如何将扩展工具栏集成到对话框或框架窗口中。压缩包为rar格式,共30个文件,约43KB,包含7个h头文件、6个cpp源文件,以及ico图标、bmp位图、rc与rc2资源脚本、dsp与dsw工程文件、txt说明文档等,覆盖从界面资源到工程配置的完整结构。目前已有93人学习下载,适合作为MFC工具栏定制的入门参考与代码模板。

1. 拆开 TB.rar 之后:一个 MFC 工具栏扩展的完整可编译样本

手上这份TB.rar不是教程 PDF,也不是零散代码片段,而是一个能直接在 Visual C++ 6.0 里打开、编译、运行的 MFC 工程快照。解压后能看到TB.dsw、TB.dsp两个工程文件,MainFrm.cpp、TBView.cpp、ToolBarEx.cpp这些实现文件,以及res目录下的Toolbar.bmp、TB.ico、Barcode.ico等资源。它解决的是一个很具体的问题:标准CToolBar只能做静态按钮,想加下拉菜单、悬停高亮、图标加文字混排,就得自己从头写。这个包里的CToolBarEx把这几件事都封装好了,适合正在维护老 MFC 项目、需要给工具栏加交互但不想重写框架的开发者。如果你手上正好有个 VC6 或 VS2010 以前的工程要改界面,这份源码值得跟着走一遍。

2. 从 CToolBar 到 CToolBarEx:继承关系与消息映射的底层逻辑

2.1 为什么标准 CToolBar 不够用

MFC 自带的CToolBar本质上是对TOOLBARCLASSNAME通用控件的薄封装。它支持SetButtons一次性定义按钮 ID 数组,也支持SetButtonInfo改样式,但有几个硬伤:第一,按钮的悬停、按下、禁用三种状态图必须预先在位图里按固定顺序排好,运行时没法动态换图;第二,工具栏按钮和下拉菜单是两套独立机制,CToolBar不提供按钮弹出菜单的原生支持;第三,想在按钮上同时画图标和文字,得自己处理TBSTYLE_LIST和TBSTYLE_EX_MIXEDBUTTONS的兼容问题,VC6 环境下尤其容易翻车。

CToolBarEx的做法不是重新造控件,而是在CToolBar基础上做三件事:重写OnPaint或拦截NM_CUSTOMDRAW来接管绘制,用CMenu挂载到按钮矩形下方实现下拉,以及维护一个内部状态数组来跟踪每个按钮的当前视觉状态。这个思路在 MFC 里很常见,好处是兼容原有消息映射,坏处是如果绘制逻辑写得不严谨,滚动或重绘时会闪。

2.2 工程文件结构与关键类职责

解压后先别急着编译,花两分钟认清文件分工:

文件职责
ToolBarEx.h/ToolBarEx.cppCToolBarEx类声明与实现,核心逻辑都在这里
MainFrm.h/MainFrm.cpp主框架窗口,负责创建工具栏并挂到CFrameWnd上
TBView.h/TBView.cpp视图类,演示按钮点击后如何更新视图内容
TBDoc.h/TBDoc.cpp文档类,VC6 单文档结构的标配
TB.rc/resource.h资源脚本与 ID 定义,按钮 ID 和菜单位图都在这里
res\Toolbar.bmp工具栏位图,通常按 16x15 或 16x16 一格排列

CToolBarEx的类声明里一般会看到AddButton、SetButtonInfo、OnToolBarBtnClick这几个成员。AddButton不是 MFC 原生函数,是这个扩展类自己加的,内部会调用CToolBar::SetButtons或直接操作TBBUTTON结构。OnToolBarBtnClick是虚函数,留给派生类重写,用来处理带下拉菜单的按钮点击。

2.3 消息映射的挂接方式

MFC 的消息映射靠宏展开成静态数组,CToolBarEx作为CToolBar的派生类,必须把父类的映射链也带上。在ToolBarEx.cpp里能看到类似这样的结构:

BEGIN_MESSAGE_MAP(CToolBarEx, CToolBar) //{{AFX_MSG_MAP(CToolBarEx) ON_WM_CREATE() ON_WM_LBUTTONDOWN() ON_WM_MOUSEMOVE() ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, OnCustomDraw) //}}AFX_MSG_MAP END_MESSAGE_MAP()

这里ON_NOTIFY_REFLECT是关键。工具栏控件的自定义绘制通知会先发给父窗口,再反射回控件自身。如果只写ON_NOTIFY而不加REFLECT,OnCustomDraw根本不会被调用,按钮画出来还是系统默认样式。这个坑我在第一次移植时踩过,调试了半天才发现消息没进来。

ON_WM_LBUTTONDOWN和ON_WM_MOUSEMOVE用来跟踪鼠标位置,判断当前悬停在哪一个按钮上,然后触发重绘。OnMouseMove里通常要调用TrackMouseEvent注册WM_MOUSELEAVE,否则鼠标移出工具栏后悬停状态不会清除,按钮会一直亮着。

2.4 按钮状态管理的内部数组

CToolBarEx内部一般会维护一个struct数组或CArray,每个元素记录按钮 ID、当前状态(正常/悬停/按下/禁用)、关联菜单指针。SetButtonInfo被重写后,除了调用基类版本,还要同步更新这个数组。如果只改基类不更新内部状态,绘制时读到的还是旧值,表现就是按钮样式改了但界面没变。

常见做法是在OnCreate里初始化数组,在AddButton里追加元素,在OnCustomDraw里根据鼠标位置查数组决定画哪张图。数组索引和TBBUTTON的索引必须一一对应,中间如果调用了SetButtons重新分配,数组也要跟着重建,否则索引错位会导致按钮画串。

3. 在 VC6 里编译运行 TB.dsw:从打开工程到按钮响应

3.1 打开工程与首次编译

双击TB.dsw,VC6 会自动加载TB.dsp。如果提示工作区已存在,选“是”覆盖。打开后先看Build菜单下的Set Active Configuration,选Win32 Debug。然后直接按 F7 编译。

第一次编译大概率会遇到两个问题:一是StdAfx.h里包含的 MFC 头路径不对,二是res\Toolbar.bmp被识别为无效位图。前者在Tools > Options > Directories里把 MFC 的include路径加到最前面;后者用 VC6 自带的资源编辑器打开TB.rc,找到IDR_MAINFRAME工具栏资源,重新导入res\Toolbar.bmp即可。

编译通过后按 Ctrl+F5 运行,应该能看到一个单文档窗口,顶部工具栏有若干按钮。如果按钮显示为空白方块,说明位图没加载成功,检查Toolbar.bmp的颜色数——VC6 对 256 色以上的位图支持不好,最好转成 16 色或 256 色。

3.2 添加自定义按钮的完整代码路径

假设要在现有工具栏上加一个“条码打印”按钮,图标用res\Barcode.ico。步骤分三步:资源里加按钮 ID,代码里加按钮定义,消息映射里加处理函数。

先在resource.h里加一个 ID:

#define ID_BUTTON_BARCODE 32771

然后在MainFrm.cpp的OnCreate里,找到创建工具栏的那段代码,在SetButtons之后追加:

// 假设 m_wndToolBar 是 CToolBarEx 类型 m_wndToolBar.AddButton(ID_BUTTON_BARCODE, 5, TBBS_BUTTON, _T("条码打印")); // 参数说明: // 第1个参数:命令 ID,与消息映射对应 // 第2个参数:位图索引,对应 Toolbar.bmp 里第 5 格图像 // 第3个参数:按钮样式,TBBS_BUTTON 表示普通按钮 // 第4个参数:按钮文字,配合 TBSTYLE_LIST 显示在图标右侧

AddButton内部会构造一个TBBUTTON结构并插入到控件里。位图索引从 0 开始,如果Toolbar.bmp里一格是 16 像素宽,第 5 格就是x = 5 * 16的位置。文字参数只有在工具栏设置了TBSTYLE_LIST和TBSTYLE_EX_MIXEDBUTTONS时才会显示,否则会被忽略。

接着在MainFrm.h的afx_msg区域加声明:

afx_msg void OnButtonBarcode();

在MainFrm.cpp的消息映射里加:

ON_COMMAND(ID_BUTTON_BARCODE, OnButtonBarcode)

最后实现处理函数:

void CMainFrame::OnButtonBarcode() { // 这里写实际业务逻辑,比如弹出条码生成对话框 AfxMessageBox(_T("条码打印按钮被点击")); }

编译运行,点击新按钮应该弹出提示框。如果没反应,检查ON_COMMAND的 ID 是否和AddButton的第一个参数一致,以及AddButton是否在SetButtons之后调用——顺序反了会被SetButtons覆盖掉。

3.3 动态下拉菜单的挂接

CToolBarEx支持按钮点击后弹出下拉菜单。实现方式通常是在OnToolBarBtnClick里判断按钮 ID,然后CMenu::TrackPopupMenu。在MainFrm.cpp里重写:

void CMainFrame::OnToolBarBtnClick(UINT nID) { if (nID == ID_BUTTON_BARCODE) { CMenu menu; menu.LoadMenu(IDR_BARCODE_MENU); // 资源里预先定义的菜单 CMenu* pPopup = menu.GetSubMenu(0); CRect rect; m_wndToolBar.GetItemRect( m_wndToolBar.CommandToIndex(nID), &rect); m_wndToolBar.ClientToScreen(&rect); pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON, rect.left, rect.bottom, this); } }

CommandToIndex把命令 ID 转成按钮索引,GetItemRect拿到按钮矩形,ClientToScreen转成屏幕坐标,最后在按钮正下方弹出菜单。注意TrackPopupMenu的第三个参数是拥有者窗口,传this表示菜单命令会发到主框架的消息映射里。

如果菜单弹不出来,先确认IDR_BARCODE_MENU在资源视图里存在且至少有一个子菜单项。VC6 的资源编辑器有时候会把菜单存成空壳,需要手动加一项。

3.4 按钮状态切换与重绘触发

运行时改变按钮状态用SetButtonInfo或自定义的SetButtonState。比如要禁用某个按钮:

m_wndToolBar.SetButtonInfo(5, ID_BUTTON_BARCODE, TBBS_BUTTON | TBBS_DISABLED, 5); m_wndToolBar.Invalidate();

SetButtonInfo的第二个参数是按钮索引,第三个参数是样式组合,第四个参数是位图索引。改完必须调Invalidate触发重绘,否则界面不会立即更新。如果按钮状态改了但视觉没变,检查OnCustomDraw里是否根据TBBUTTON的fsState字段选择了正确的图像索引。

4. 避坑与排查:VC6 下 CToolBarEx 的五个血泪经验

4.1 按钮位图错位或只显示第一格

现象:工具栏上所有按钮都显示同一张图,或者按钮图像整体偏移。

原因:Toolbar.bmp的格宽和SetSizes设置的按钮尺寸不匹配。MFC 默认按 16x15 切图,如果位图实际是 24x24 一格,切出来就错位。

解决:在OnCreate里创建工具栏后立即调用SetSizes:

m_wndToolBar.SetSizes(CSize(24, 24), CSize(16, 16)); // 第一个参数是按钮整体尺寸,第二个是图像尺寸

同时用画图工具确认Toolbar.bmp的宽度能被按钮数整除。

4.2 悬停高亮不消失

现象:鼠标移出工具栏后,最后一个悬停的按钮仍然保持高亮。

原因:没有处理WM_MOUSELEAVE消息,或者TrackMouseEvent没有在OnMouseMove里正确调用。

解决:在OnMouseMove里加:

if (!m_bTracking) { TRACKMOUSEEVENT tme; tme.cbSize = sizeof(tme); tme.dwFlags = TME_LEAVE; tme.hwndTrack = m_hWnd; _TrackMouseEvent(&tme); m_bTracking = TRUE; }

然后加ON_MESSAGE(WM_MOUSELEAVE, OnMouseLeave),在OnMouseLeave里把m_bTracking置 FALSE 并清除悬停索引,最后Invalidate。

4.3 下拉菜单弹出位置偏移

现象:菜单弹在屏幕左上角或按钮上方,而不是按钮正下方。

原因:GetItemRect返回的是客户区坐标,直接传给TrackPopupMenu会被当成屏幕坐标。

解决:必须调ClientToScreen转换。如果工具栏是浮动状态(CFrameWnd的EnableDocking后拖出来),还要考虑父窗口坐标系的差异,稳妥做法是用GetWindowRect拿工具栏窗口矩形,再根据按钮索引计算偏移。

4.4 编译报错“无法解析的外部符号”

现象:链接阶段报unresolved external symbol,指向CToolBarEx的某个成员函数。

原因:ToolBarEx.cpp没有加入工程,或者函数声明了但没实现。

解决:在 VC6 的FileView里右键Source Files,选Add Files to Project,把ToolBarEx.cpp加进去。如果已经加了还报错,检查函数签名是否和头文件完全一致,包括const和参数类型。

4.5 工具栏在对话框程序里不显示

现象:把CToolBarEx移植到CDialog派生类里,调用Create后工具栏不出现。

原因:对话框默认没有CFrameWnd的工具栏停靠机制,CToolBar::Create需要指定父窗口样式。

解决:在对话框的OnInitDialog里创建时,用WS_CHILD | WS_VISIBLE | CBRS_TOP样式,并且父窗口传this。如果还是不行,检查对话框是否设置了WS_CLIPCHILDREN,这个样式会导致子窗口绘制被裁剪。

5. 进阶:用 CToolBarEx 做图标文字混排与运行时换肤

图标文字混排是CToolBarEx比标准CToolBar最直观的增强。标准控件要显示文字,得同时设置TBSTYLE_LIST和TBSTYLE_EX_MIXEDBUTTONS,但 VC6 的CToolBar封装没有暴露SetExtendedStyle,所以很多人卡在这一步。CToolBarEx通常会在OnCreate里直接发TB_SETEXTENDEDSTYLE消息:

DWORD dwExStyle = TBSTYLE_EX_MIXEDBUTTONS | TBSTYLE_EX_DRAWDDARROWS; m_wndToolBar.SendMessage(TB_SETEXTENDEDSTYLE, 0, dwExStyle);

TBSTYLE_EX_MIXEDBUTTONS允许同一工具栏上有的按钮显示文字、有的不显示。TBSTYLE_EX_DRAWDDARROWS会在按钮右侧画一个小箭头,提示有下拉菜单。这两个扩展样式必须在SetButtons之前设置,否则不生效。

运行时换肤的思路是准备多套Toolbar.bmp,在按钮点击或菜单命令里调LoadBitmap重新加载,然后SetBitmap替换。注意SetBitmap后要调SetSizes重新计算按钮尺寸,否则新位图如果格宽不同会错位。我一般会把位图资源 ID 和格宽存在一个配置数组里,换肤时遍历更新。

验证换肤是否成功,不要只看界面,用Spy++抓TB_GETBITMAP消息的返回值,确认句柄变了。如果句柄没变,说明LoadBitmap失败,检查位图资源是否被正确编译进 EXE。

从那以后我每次改工具栏资源,都强制走一遍“改位图 → 调 SetSizes → Invalidate → 用 Spy++ 确认句柄”的流程,再也没出现过按钮画串的玄学问题。希望帮到你。

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

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

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

立即咨询