☰
VC6老MFC工程实战:CToolBarEx扩展工具栏下拉菜单与自绘
2026/10/3 2:47:18 网站建设 项目流程

简介:这份资源面向使用 Visual C++ 与 MFC 进行 Windows 界面开发的初中级程序员,聚焦 CToolBarEx 扩展工具栏类的实际应用。CToolBarEx 在标准 CToolBar 基础上增强了自定义按钮图像、悬停与按下状态、下拉动态菜单、图标文字并存以及禁用、隐藏、分组等按钮状态管理,能显著提升工具栏的交互性与美观度。压缩包共 30 个文件,约 43KB,包含 7 个 h 头文件、6 个 cpp 源文件、4 个 ico 图标、1 个 bmp 位图及 rc 资源脚本、dsp/dsw 工程文件等,构成一套可直接编译运行的完整示例工程。示例演示了创建 CToolBarEx 对象、添加按钮、绑定消息映射、重载点击响应及设置按钮状态等关键环节,读者可据此掌握工具栏定制与动态菜单的实现思路。目前已有 93 人学习,适合希望深入理解 MFC 工具栏扩展机制的开发者参考。

1. 从 TB.rar 拆开看:一个能跑在 VC6 里的 CToolBarEx 完整示例

如果你手里还压着 VC6 或 VS2008 的老 MFC 工程,想给工具栏加下拉菜单、悬停高亮、图标加文字,又不想从零重写CToolBar的绘制逻辑,那TB.rar这个包值得花十分钟拆一遍。它不是什么新框架,就是一份CToolBarEx的完整可编译示例:TB.dsw工作区、TB.dsp工程、MainFrm.cpp里挂工具栏、ToolBarEx.cpp/h是扩展类本体、TBView.cpp负责视图侧响应,资源目录里还塞了Toolbar.bmp、Barcode.ico、drink.icl这些位图与图标素材。整个包 68 个文件,解压后直接能看出 MFC 文档视图结构的骨架。它解决的核心问题很具体——标准CToolBar不支持按钮下拉菜单、不支持三态图像切换、不支持图标文字混排,而这份代码把这三件事都做成了可复用的类。适合谁?手上维护老 MFC 项目、需要快速给工具栏加交互、又不想引入第三方 UI 库的 C++ 桌面开发者。下面按“先看懂类结构 → 再动手编译 → 再改按钮和菜单 → 最后排坑”的顺序走一遍。

2. CToolBarEx 的类结构与消息映射:先搞清它比 CToolBar 多了什么

2.1 从 ToolBarEx.h 看扩展点在哪

拆包后第一件事不是急着编译,而是打开ToolBarEx.h看类声明。标准CToolBar继承自CControlBar,而CToolBarEx通常直接继承CToolBar,然后在上面叠加三块能力:按钮状态缓存、下拉菜单句柄、自绘入口。你重点看这几个成员——按钮结构体数组(存每个按钮的nID、nStyle、iImage、hMenu)、OnPaint或DrawButton的重写、以及OnToolBarBtnClick这类自定义消息处理函数。TB.h和TB.cpp是应用程序对象,MainFrm.h/cpp是主框架窗口,工具栏的创建和EnableDocking一般写在MainFrm::OnCreate里。TBView.h/cpp是视图,按钮命令最终落到这里或TBDoc。搞清这条链路,后面改代码才不会迷路。

常见做法是:MainFrm::OnCreate里先m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC),再m_wndToolBar.LoadToolBar(IDR_MAINFRAME),最后m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY)。CToolBarEx的增强就发生在LoadToolBar之后——它需要额外调用类似SetDropDownMenu或AddButton的接口,把下拉菜单资源和按钮 ID 绑起来。如果你只LoadToolBar不调扩展接口,那它退化成一个普通工具栏,这也是很多人说“下了包但没效果”的第一个原因。

2.2 消息映射:ON_COMMAND 与下拉菜单通知的分工

按钮点击走ON_COMMAND,下拉菜单弹出走ON_NOTIFY或自定义的WM_USER消息,这两条路不能混。TB.rc里你会看到IDR_MAINFRAME工具栏资源,按钮 ID 通常是ID_BUTTON32771这种自动生成的。在MainFrm.cpp的消息映射块里,ON_COMMAND(ID_BUTTON32771, OnBtn32771)负责普通点击;如果这个按钮带下拉箭头,那点击箭头区域时CToolBarEx会发一个通知,你在ON_NOTIFY里TrackPopupMenu。下面这段是典型的消息映射写法,直接抄进你的MainFrm.cpp对应位置即可:

// MainFrm.cpp 消息映射片段 BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) ON_WM_CREATE() ON_COMMAND(ID_BUTTON32771, OnToolbarBtn1) // 普通按钮点击 ON_COMMAND(ID_BUTTON32772, OnToolbarBtn2) ON_NOTIFY(TBN_DROPDOWN, IDR_MAINFRAME, OnToolbarDropdown) // 下拉箭头通知 END_MESSAGE_MAP() void CMainFrame::OnToolbarDropdown(NMHDR* pNMHDR, LRESULT* pResult) { LPNMTOOLBAR pNMTB = (LPNMTOOLBAR)pNMHDR; switch (pNMTB->iItem) { case ID_BUTTON32772: { CMenu menu; menu.LoadMenu(IDR_POPUP_MENU); // 资源里预定义的下拉菜单 CMenu* pPopup = menu.GetSubMenu(0); CRect rect; m_wndToolBar.GetItemRect( m_wndToolBar.CommandToIndex(ID_BUTTON32772), &rect); m_wndToolBar.ClientToScreen(&rect); pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON, rect.left, rect.bottom, this); // 在按钮下方弹出 break; } } *pResult = 0; }

逻辑说明:TBN_DROPDOWN是工具栏下拉箭头的标准通知码,iItem告诉你哪个按钮被点了箭头。CommandToIndex把命令 ID 转成按钮索引,GetItemRect拿到按钮屏幕坐标,TrackPopupMenu在按钮正下方弹出菜单。参数上注意TPM_LEFTALIGN保证菜单左对齐按钮,rect.bottom让菜单贴着按钮下沿。如果你发现菜单弹在屏幕左上角,八成是忘了ClientToScreen。这段代码不依赖CToolBarEx的私有实现,即使你换回标准CToolBar,只要工具栏样式带TBSTYLE_DROPDOWN,通知照样能收到——区别在于CToolBarEx帮你把箭头绘制和状态管理封装好了。

3. 把 TB.dsw 跑起来:VC6 到 VS2022 的编译参数与资源加载

3.1 工程文件版本转换与字符集设置

TB.dsw和TB.dsp是 VC6 时代的工程格式,VS2005 之后需要转换。用 VS2022 打开会提示“升级”,确认后生成.vcxproj。转换后第一件事是检查字符集:老工程默认多字节(MBCS),而新 VS 默认 Unicode。如果你不改,LoadToolBar、TrackPopupMenu这些调用会报const char*与LPCWSTR不匹配。两个选择——要么在项目属性里把“字符集”改成“使用多字节字符集”,要么在预处理器里加_AFXDLL并保持 Unicode 然后逐个改字符串。我一般选前者,因为这份示例代码里的字符串字面量全是窄字符,改 Unicode 工作量更大。具体路径:项目属性 → 配置属性 → 高级 → 字符集 → 使用多字节字符集。

第二个坑是 MFC 库链接方式。TB.dsp里可能写死了“静态库中使用 MFC”或“共享 DLL 中使用 MFC”。如果你机器上没装对应版本的 MFC 运行库,链接会报nafxcw.lib找不到。稳妥做法是改成“在共享 DLL 中使用 MFC”,前提是装了 VS 的“MFC 组件”。VS2022 安装器里要勾“使用 C++ 的桌面开发”下的“MFC 最新版 v143”。没勾的话,#include <afxwin.h>直接飘红。

3.2 资源文件与位图加载的路径问题

TB.rc里引用了res\Toolbar.bmp、res\TB.ico、res\Barcode.ico等。转换工程后,资源视图可能报“无法打开文件”。原因是.rc里的相对路径基于.dsp所在目录,而 VS 转换后工作目录变了。解决办法:在资源视图里右键.rc→ 查看代码,找到IDB_TOOLBAR BITMAP "res\\Toolbar.bmp"这类行,确认路径大小写和斜杠方向。Windows 不区分大小写,但 VS 的资源编译器有时对反斜杠转义敏感。如果位图加载后工具栏按钮全白,检查Toolbar.bmp的颜色深度——VC6 时代的工具栏位图通常是 16 色或 256 色,如果你用画图工具另存成了 24 位真彩色,LoadToolBar可能只取到左上角一小块。常见做法是保持原图不动,或者用 VS 自带的位图编辑器重新导出为 16 色。

编译命令层面,如果你习惯命令行,可以用msbuild TB.vcxproj /p:Configuration=Debug /p:Platform=Win32。注意平台必须是 Win32,因为这份老工程没有 x64 配置。强行切 x64 会报afxwin.h找不到——MFC 的 x64 库在 VS2022 里是有的,但工程里的#pragma comment(lib, "nafxcw.lib")写死了 32 位库名。改 x64 需要把链接器输入里的nafxcw.lib换成nafxcw64.lib,同时检查所有DWORD到DWORD_PTR的转换。对一份示例来说,没必要,直接 Win32 编译最省事。

提示:编译前把Debug和Release目录清掉,老工程里可能残留 VC6 的.obj,和新编译器产物混在一起会报LNK2005重复定义。

4. 按钮状态与下拉菜单的实操:从 AddButton 到 SetButtonInfo

4.1 动态添加按钮与三态图像绑定

CToolBarEx的卖点之一是按钮在正常、悬停、按下三种状态下显示不同图像。标准CToolBar只支持一组图像,靠TBSTYLE_FLAT自动做高亮,但高亮效果是系统画的,改不了。CToolBarEx的做法通常是维护三个CImageList或一个三倍宽度的位图,然后重写DrawButton。在示例里,你找ToolBarEx.cpp中的DrawButton或OnPaint,会看到类似pDC->DrawState或pDC->StretchBlt的调用,根据nState取不同偏移。如果你要加自己的按钮,不要直接改IDR_MAINFRAME的位图,而是用SetButtonInfo动态改:

// 在 MainFrm::OnCreate 中 LoadToolBar 之后 int nIndex = m_wndToolBar.CommandToIndex(ID_BUTTON32771); if (nIndex != -1) { m_wndToolBar.SetButtonInfo(nIndex, ID_BUTTON32771, TBBS_BUTTON | TBBS_DROPDOWN, 0); // 0 是图像索引 m_wndToolBar.SetButtonText(nIndex, _T("导出")); // 图标旁加文字 } // 如果 CToolBarEx 提供了 AddButton,则: // m_wndToolBar.AddButton(ID_BUTTON32773, TBBS_BUTTON, 3, _T("刷新"));

逻辑说明:CommandToIndex把命令 ID 映射到按钮位置,SetButtonInfo的第三个参数是样式,TBBS_DROPDOWN让按钮带下拉箭头,第四个参数是图像列表中的索引。SetButtonText设置按钮文字,前提是工具栏样式带TBSTYLE_FLAT且按钮宽度够。参数上注意:如果你把nStyle设成TBBS_SEPARATOR,那iImage参数会被忽略,按钮变成一条分隔线。AddButton不是 MFC 标准接口,是CToolBarEx自己加的,所以你得确认ToolBarEx.h里确实声明了它,否则编译报C2039。

4.2 下拉菜单与按钮状态的联动

下拉菜单弹出后,按钮应该保持按下状态,直到菜单关闭。标准CToolBar不会自动做这件事,CToolBarEx一般通过SetButtonState或直接改m_bBtnPressed数组来实现。在OnToolbarDropdown里,TrackPopupMenu之前把按钮状态设为按下,之后恢复:

void CMainFrame::OnToolbarDropdown(NMHDR* pNMHDR, LRESULT* pResult) { LPNMTOOLBAR pNMTB = (LPNMTOOLBAR)pNMHDR; int nIndex = m_wndToolBar.CommandToIndex(pNMTB->iItem); if (nIndex != -1) { m_wndToolBar.SetButtonStyle(nIndex, m_wndToolBar.GetButtonStyle(nIndex) | TBBS_PRESSED); m_wndToolBar.Invalidate(); } // ... TrackPopupMenu ... if (nIndex != -1) { m_wndToolBar.SetButtonStyle(nIndex, m_wndToolBar.GetButtonStyle(nIndex) & ~TBBS_PRESSED); m_wndToolBar.Invalidate(); } *pResult = 0; }

逻辑说明:GetButtonStyle和SetButtonStyle是CToolBar的标准方法,TBBS_PRESSED让按钮显示按下态。Invalidate触发重绘。注意TrackPopupMenu是阻塞的,菜单关闭后才执行后面的恢复代码,所以顺序没问题。参数上,如果你用的是CToolBarEx自己的SetButtonState,那可能直接传BTN_STATE_PRESSED枚举,具体看头文件定义。这里有个血泪经验:不要在OnToolbarDropdown里delete菜单对象,CMenu的析构会处理,手动删会导致下次弹出崩溃。

4.3 按钮禁用与隐藏的运行时切换

CToolBarEx支持运行时禁用或隐藏按钮,这在权限控制场景里很实用。标准CToolBar用CToolBarCtrl::EnableButton或HideButton,但CToolBar封装层没有直接暴露。常见做法是GetToolBarCtrl().EnableButton(ID_BUTTON32771, FALSE)和GetToolBarCtrl().HideButton(ID_BUTTON32771, TRUE)。注意HideButton之后按钮位置会空出来,如果你希望后面的按钮补位,得调GetToolBarCtrl().PressButton配合SetRows重排,或者干脆用SetButtonInfo把按钮宽度设成 0。CToolBarEx如果重写了布局逻辑,那它可能自动处理补位,你直接调它的HideButton就行。判断方法:看ToolBarEx.cpp里有没有RepositionBars或CalcDynamicLayout的重写。

5. 避坑与排查:老 MFC 工程在 Win11 上的五个翻车点

5.1 现象:编译报 cl.exe failed with exit status 2

原因:这个报错本身不是编译错误,是cl.exe返回了非零退出码,真正错误在它上面几行。常见触发是#include <afxwin.h>找不到,因为 VS 安装时没勾 MFC 组件;或者TB.rc里的中文注释在 UTF-8 和 GBK 之间乱码,导致资源编译器解析失败。解决:先看输出窗口里cl.exe命令行上面那条fatal error C1083或RC1015。如果是 MFC 缺失,打开 VS 安装器 → 修改 → 单个组件 → 搜“MFC” → 勾选“适用于最新 v143 生成工具的 C++ MFC”。如果是.rc乱码,用 VS 打开.rc文件 → 文件 → 高级保存选项 → 编码选“简体中文 (GB2312) - 代码页 936”。

5.2 现象:工具栏按钮图标错位或只显示第一个

原因:Toolbar.bmp被改过尺寸或颜色深度。标准工具栏位图要求每个按钮 16x15 像素(或 20x20,取决于TB_SETBITMAPSIZE),且所有按钮图像横向排列在一张位图里。如果你用截图工具重新保存,图像间距变了,LoadToolBar按固定宽度切图就会错位。解决:用 VS 资源编辑器打开IDB_TOOLBAR,看每个按钮的宽度是否一致。不一致就重新用 16 色位图编辑器排列,或者调m_wndToolBar.SetSizes(CSize(20,20), CSize(16,15))手动指定按钮和图像尺寸。

5.3 现象:下拉菜单弹出后点击菜单项没反应

原因:菜单项的ID没有对应的ON_COMMAND映射,或者映射写在了视图类而菜单的父窗口是框架类。TrackPopupMenu的第四个参数传的是this(框架窗口),所以命令会先发给框架窗口。如果ON_COMMAND只写在CTBView里,框架窗口不处理就丢弃了。解决:要么把ON_COMMAND写到CMainFrame,要么在TrackPopupMenu里传GetActiveView()作为父窗口。后者更符合文档视图架构,但要注意GetActiveView()可能返回NULL。

5.4 现象:Win11 下工具栏悬停高亮不显示

原因:Win11 的视觉样式对老 MFC 工具栏的TBSTYLE_FLAT支持有变化,系统不再自动画高亮边框。CToolBarEx如果依赖DrawItem或NM_CUSTOMDRAW做自绘,可能因为SetWindowTheme没调而失效。解决:在MainFrm::OnCreate里工具栏创建后加SetWindowTheme(m_wndToolBar.GetSafeHwnd(), L"", L""),禁用主题,让CToolBarEx自己的绘制逻辑接管。或者反过来,调SetWindowTheme(..., L"Explorer", NULL)启用资源管理器主题,看哪种效果符合预期。

5.5 现象:Debug 编译通过,Release 崩溃

原因:Release 下ASSERT被优化掉,某些在 Debug 下被断言拦住的空指针或越界访问直接执行了。老 MFC 代码里常见的是GetActiveView()返回NULL后直接调成员函数,或者CommandToIndex返回 -1 后没判断就SetButtonInfo。解决:在 Release 配置里打开“生成调试信息”(项目属性 → C/C++ → 常规 → 调试信息格式 → 程序数据库),崩溃时用 VS 附加进程看调用栈。重点检查所有CommandToIndex的返回值,以及TrackPopupMenu前GetSubMenu是否返回NULL。

6. 进阶:把 CToolBarEx 的绘制逻辑抽出来复用到新工程

如果你不想整体迁移这份老工程,只想把CToolBarEx的按钮自绘和下拉菜单能力搬到 VS2022 的新 MFC 项目里,可以只取ToolBarEx.h和ToolBarEx.cpp两个文件,然后在新工程里手动挂接。步骤是:把两个文件加入新工程 → 在MainFrm.h里把CToolBar m_wndToolBar改成CToolBarEx m_wndToolBar→ 在MainFrm.cpp的OnCreate里保持CreateEx和LoadToolBar不变 → 把老工程MainFrm.cpp里所有m_wndToolBar.开头的扩展调用(AddButton、SetButtonState、SetDropDownMenu之类)复制过来。编译时如果报ToolBarEx.h里某个#include找不到,通常是它引了TB.h或resource.h,把这两个文件里的资源 ID 定义拷到新工程的resource.h即可。

验证是否挂接成功,最简单的办法是在CToolBarEx::DrawButton里加一行TRACE(_T("DrawButton index=%d state=%d\n"), nIndex, nState);,然后运行程序,鼠标划过按钮时看输出窗口有没有打印。有打印说明自绘入口被调到了,没打印说明CToolBarEx的OnPaint没被触发,检查CreateEx的样式里是否带了TBSTYLE_FLAT——不带的话系统走默认绘制,不会进你的DrawButton。

参数边界上,CToolBarEx的按钮图像索引是int,但SetButtonInfo的iImage参数在 MFC 内部是UINT,传负数会变成很大的正数,导致图像列表越界。我一般会在AddButton或SetButtonInfo前加一句ASSERT(iImage < m_imageList.GetImageCount())。另外,下拉菜单的TrackPopupMenu返回值是用户选中的命令 ID,如果返回 0,说明用户点了菜单外部取消,不要当成有效命令处理。

从那以后我每次拿到这种老 MFC 示例包,都强制先做三件事:把字符集改成多字节、把Debug目录清空、在CommandToIndex后面加ASSERT(nIndex != -1)。这三步能挡掉八成编译和运行时崩溃。希望帮到你。

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

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

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

立即咨询