恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
CToolBarEx实战:MFC扩展工具栏集成与避坑指南
首页
资讯中心
/
CToolBarEx实战:MFC扩展工具栏集成与避坑指南
CToolBarEx实战:MFC扩展工具栏集成与避坑指南
发布时间:2026/10/5 1:25:07
简介面向Visual C MFC开发者的CToolBarEx扩展工具栏示例包主要解决自定义按钮、动态菜单、图标文字混排及按钮状态管理等问题适合需要提升Windows界面交互性的中初级MFC学习者。压缩包共30个文件以.h头文件、.cpp源文件为核心内含完整的示例工程.dsp/.dsw/.rc等工程与资源文件另有图标与位图素材整体仅43KB便于快速下载与阅读。压缩包中除工程源码外还提供了ReadMe说明文件方便对照理解工具栏类派生与消息映射流程同时包含www.pudn.com.txt来源注释可追溯到更多同类学习资料。目前已有93人学习浏览适合想要通过实例掌握CToolBarEx按钮添加、状态设置及下拉菜单关联的开发者可作为直接可运行的入门范例也能从中提取自定义绘制工具栏的通用思路。1. 从 pudn 翻出来的 CToolBarEx 示例一个 VC6.0 老工程能解决什么先说结论CToolBarEx 是 Visual C 6.0 时代在 MFC 工具栏 CToolBar 之上扩展出来的一套类网上流传的版本不少但多数只给一个头文件和零零散散的实现片段。这份 TB.rar 难得是完整工程TB.dsw / TB.dsp 打开就是一套能编译能运行的 MFC 单文档程序工具栏、下拉菜单、图标资源全配齐。它解决的是标准工具栏最容易被嫌弃的三件事按钮想带文字、按钮想加下拉菜单、鼠标悬停和按下想换图CToolBar 默认做不到这些CToolBarEx 把这几个能力补齐了。适合三类人手里有老 MFC 项目要维护的开发、正在做内部工具桌面的程序员、想彻底搞清工具栏消息怎么路由的学习者。接下来我把这套工程的每个文件、每段关键代码和我在实际改造里踩过的坑拆开讲你照着能少走弯路。2. 拆解 TB.rarMFC 单文档工程的文件职责与 ToolBarEx 核心接口2.1 文件清单哪些要复制、哪些直接删解压后看到三十几个文件第一反应别慌。这份压缩包混着 IDE 自动生成的缓存文件和真正的源码复制到自己工程时先分个类。我按生产环境里的价值把这堆文件分成了三档必拿、参考、删除。文件 / 目录职责我的处理建议TB.dsw / TB.dspVC6 工程文件保留作为学习参照老工程迁移时用 VS 打开 .dsp 升级ToolBarEx.h / ToolBarEx.cpp扩展工具栏类实现单独拷贝到自己工程这是唯一必拿的文件MainFrm.h / MainFrm.cpp主框架窗口工具栏创建入口看它怎么调用 CToolBarEx不必整目录搬TB.h / TB.cpp应用类 CWinApp标准模板泛读TBDoc.h / TBDoc.cpp文档类标准模板TBView.h / TBView.cpp视图类标准模板resource.h / TB.rc资源 ID 与资源脚本把需要的 ID 和 TOOLBAR 区块移植过去TB.rc2 / res/附加资源、图标、位图位图按需拷贝TB.ico / TBDoc.ico / Barcode.ico / Toolbar.bmp / drink.icl程序与按钮图标资源我只挑 Barcode.ico 和 Toolbar.bmp 留下来当示例#60 of 68.ico从图标站收集的素材文件名带「#」号进工程会在资源解析时出怪问题改成 ico60.ico 再引入Thumbs.db / TB.opt / TB.ncb / TB.aps / TB.clw系统或 IDE 生成的缓存全部删除不影响编译整个包里真正决定 CToolBarEx 能不能用的是 ToolBarEx.h 和 ToolBarEx.cpp 两个文件。我在自己维护的老项目里集成时通常只把这两个文件放进新建的 source 目录再补一个资源 ID 头文件就能脱离示例工程单独工作。MainFrm 里的 OnCreate 是接线点TBView / TBDoc 只是 MFC Doc/View 的骨架泛读一下知道消息能传到哪里就够了。pudn 上传者自带的那个 www.pudn.com.txt 是下载来源说明没有代码价值解压后可以直接删掉。2.2 ToolBarEx 对外接口与内部数据结构通读一遍 ToolBarEx.h你会发现它的设计思路是保持和 CToolBar 一样的资源加载方式再额外提供一套按钮管理接口。我把这个类里最关键的接口按用途列出来方便你对照示例代码看它到底做了什么。class CToolBarEx : public CToolBar { public: BOOL Create(CWnd* pParentWnd, DWORD dwStyle, UINT nID); BOOL LoadToolBar(UINT nIDResource); BOOL AddButton(UINT nID, int nImage, LPCTSTR lpszText); BOOL AddSeparator(); BOOL SetButtonInfo(int nIndex, UINT nID, UINT nStyle, int nImage); BOOL SetButtonMenu(int nIndex, CMenu* pMenu); CMenu* GetButtonMenu(int nIndex) const; BOOL SetButtonState(int nIndex, BYTE fsState); BOOL SetButtonText(int nIndex, LPCTSTR lpszText); protected: virtual void OnToolBarBtnClick(UINT nID); virtual BOOL OnToolBarCommand(UINT nID); };参数含义说清楚nID 是命令 ID最终会通过消息映射分发nImage 是位图条里的格子索引不是按钮顺序lpszText 是按钮上显示的文字nStyle 里用 TBBS_BUTTON、TBBS_CHECKBOX、TBBS_GROUP 这几个组合控制按钮类型。我看过不同人手里的 CToolBarEx 版本接口名可能稍微有差异但 AddButton、SetButtonInfo、OnToolBarBtnClick 这三角色几乎每个版本都有你在示例工程里碰到的也是这一套。内部实现层面按钮数据最终落到 Win32 的 TBBUTTON 结构上idCommand 是命令 IDiBitmap 是位图索引fsStyle 决定普通按钮还是分隔符iString 保存文字指针。CToolBarEx 和 CToolBar 的差别主要就是在这套 TBBUTTON 之外再挂了一组可选的隐藏菜单指针和分组信息所以它既能沿用标准 CToolBar 的位图逻辑保持兼容又能在消息出口处多做一个菜单弹出的分支。理解这条线后面按钮不显示、菜单位置不对这类问题你就知道该往哪个方向查。2.3 资源脚本与 Toolbar.bmp按钮和位图的对应约定VC6 的工具栏位图通常是一整条 Toolbar.bmp按 16x15 像素等分资源脚本里用 TOOLBAR 区块声明按钮顺序。示例工程的 TB.rc 里大致是这个样子IDR_MAINFRAME TOOLBAR DISCARDABLE 16, 15 BEGIN BUTTON ID_FILE_NEW BUTTON ID_FILE_OPEN SEPARATOR BUTTON ID_VIEW_BARCODE BUTTON ID_VIEW_DRINK END这里有两个约定最容易踩坑。第一位图条从左到右的每一格对应脚本里按顺序出现的每个 BUTTON中间一旦插了 SEPARATOR按钮顺序会被打断但 SEPARATOR 不占位图索引。第二SetButtonInfo 里的 nImage 不是“第几个按钮”而是“位图条里第几格”很多人把这两个概念混着用结果图标全错位。16x15 是经典尺寸如果换成 32x32 的大图标.rc 里的 TOOLBAR 声明也要同步改成 32, 31否则图片会被裁剪显示出来歪一半。示例工程用的 Toolbar.bmp 和 Barcode.ico 都是老资源我实际测试时发现位图条里有几格是纯色占位你自己换图的时候注意把占位格去掉。3. 把 CToolBarEx 集成进自己的 MFC 工程从 OnCreate 到消息分发的完整接线3.1 替换框架窗口里的标准 CToolBar第一步把 ToolBarEx.h 和 ToolBarEx.cpp 复制到工程目录并加入工程。然后在 MainFrm.h 里把工具栏成员类型从 CToolBar 换成 CToolBarEx这是整个接入过程里最不起眼但最关键的一步。// MainFrm.h #include ToolBarEx.h class CMainFrame : public CFrameWnd { protected: CToolBarEx m_wndToolBar; // 原来这里是 CToolBar m_wndToolBar ... };第二步在 MainFrm.cpp 的 OnCreate 里创建工具栏。Create 第三个参数和 LoadToolBar 参数要用同一个资源 ID比如 IDR_MAINFRAME这个 ID 定义在 resource.h 里。int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CFrameWnd::OnCreate(lpCreateStruct) -1) return -1; // 创建扩展工具栏IDR_MAINFRAME 在 resource.h 中定义 if (!m_wndToolBar.Create(this, WS_CHILD | WS_VISIBLE | CBRS_TOP, IDR_MAINFRAME) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME)) { TRACE0(创建工具栏失败\n); return -1; } m_wndToolBar.ShowWindow(SW_SHOW); return 0; }这段代码的作用有三个把工具栏窗口挂在主框架上、加载这一条位图、把按钮和资源 ID 对齐。Create 的第一个 this 表示父窗口是 CMainFrame第二个参数里的 CBRS_TOP 决定初始位置在窗口上方LoadToolBar 是真正读 .rc 里 TOOLBAR 区块的地方。如果 LoadToolBar 返回 FALSE不要往下查业务代码先回 .rc 里看资源 ID 有没有写错。还有一点容易让人误会在 Visual Studio 里新建 C 工程后你会发现工具箱里找不到任何工具栏控件这很正常MFC 工具栏是代码创建的不是从工具箱拖出来的CToolBarEx 也一样。3.2 AddButton 添加按钮ID、图标索引、文字的对应关系OnCreate 创建成功后用 AddButton 往工具栏里追加按钮。示例工程把按钮都放在资源脚本里但实际改造时我方更常用 AddButton 动态追加特别是运行时才决定要显示哪些功能时。// 在 OnCreate 创建成功后追加按钮 m_wndToolBar.AddButton(ID_VIEW_BARCODE, 0, _T(条码)); m_wndToolBar.AddButton(ID_VIEW_DRINK, 1, _T(饮品)); m_wndToolBar.AddButton(ID_HELP_ABOUT, 2, _T(关于));代码逻辑第一个参数是按钮的 ID相当于按钮的身份证点击后进入消息路由经过 ON_COMMAND 分发到处理函数第二个参数是位图索引对应 Toolbar.bmp 里的那一格第三个参数是按钮上显示的文字。参数说明里有个细节位图索引从 0 开始和 AddButton 的调用顺序一一对应如果中间 AddSeparator 插入分隔符分隔符不占位图索引但会占按钮下标后续按钮的 nImage 要重新对齐。文字传 NULL 也可以但很多老版本里传 NULL 仍然会留出图片加文字的间距我惯用做法是直接传空字符串 _T()让布局干净一点。3.3 消息映射ON_COMMAND 和 ON_BN_CLICKED 到底用哪个工具栏按钮的点击处理标准 MFC 走的是 ON_COMMAND加在 CMainFrame 或视图类的消息映射里。示例工程里命令处理就放在主框架这是最稳妥的位置因为工具栏按钮消息会优先路由到框架窗口。BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) ON_COMMAND(ID_VIEW_BARCODE, CMainFrame::OnViewBarcode) ON_COMMAND(ID_VIEW_DRINK, CMainFrame::OnViewDrink) END_MESSAGE_MAP() void CMainFrame::OnViewBarcode() { AfxMessageBox(_T(条码按钮被点击)); }这里有个容易混淆的点ON_BN_CLICKED 是按钮控件专用的消息映射宏很多人会把工具栏按钮当成普通按钮去套结果毫无反应。差别在于 CToolBarEx 的按钮本质上是命令项不是 CDialog 上的子控件当工具栏被放进对话框里使用时才需要考虑 ON_BN_CLICKED。参数说明ID_VIEW_BARCODE 必须和 resource.h 里定义的 ID 完全一致处理函数写了还不够一定要在消息映射里登记少了映射那一行按钮点击就是石沉大海这是新手翻车率最高的位置。3.4 给按钮挂下拉菜单的两种做法CToolBarEx 支持把弹出菜单挂到按钮上示例工程里按钮点击后弹菜单的用法很典型。第一种做法是直接用 SetButtonMenu把 CMenu 对象挂到指定按钮上。// MainFrm.h 里声明成员变量 CMenu m_drinkMenu; // OnCreate 阶段创建菜单并挂到索引 1 的按钮上 m_drinkMenu.CreatePopupMenu(); m_drinkMenu.AppendMenu(MF_STRING, ID_DRINK_TEA, _T(绿茶)); m_drinkMenu.AppendMenu(MF_STRING, ID_DRINK_COFFEE, _T(咖啡)); m_wndToolBar.SetButtonMenu(1, m_drinkMenu);代码说明SetButtonMenu 的第一个参数是按钮下标第二个参数是菜单指针点击按钮后会在按钮下方弹出这个菜单。菜单里的命令 ID 同样走 ON_COMMAND不用单独处理。这里有个血泪教训CMenu 必须保存为成员变量不能是局部变量局部对象在函数返回时析构菜单指针就悬空了点击按钮要么没反应要么直接崩溃。第二种做法是在 OnToolBarBtnClick 虚函数里手动弹菜单好处是可以在弹出前动态决定菜单项适合菜单内容每次点击都要变化的场景但不同版本的 CToolBarEx 对这个回调的支持不完全一致我在示例工程上不深度依赖它。3.5 用 SetButtonInfo 控制禁用、置灰与分组状态按钮状态管理是 CToolBarEx 的强项SetButtonInfo 可以原地改按钮样式不用删了重建。示例工程里对状态控制给了一个很直接的入口。// 禁用索引 3 的按钮 m_wndToolBar.SetButtonInfo(3, ID_EDIT_UNDO, TBBS_BUTTON | TBBS_DISABLED, 3); // 改成 checkbox 样式点击后界面自动切换选中/未选中 m_wndToolBar.SetButtonInfo(3, ID_EDIT_UNDO, TBBS_CHECKBOX, 3);第一行里 TBBS_DISABLED 是禁用态按钮置灰且不再响应点击。第二行的 TBBS_CHECKBOX 是复选样式点一下按下再点一下弹起界面状态由系统维护。如果想让几个按钮互相排斥加 TBBS_GROUP再把同一组按钮的 ID 排在一起MFC 会按组处理选中态。参数说明里最容易被忽略的是最后一个 nImage 参数它代表按钮在当前位图条里的格子索引改状态的时候如果这个值传错按钮图标会跟着跳变所以我的习惯是复制原来的值只改 nStyle 那一项避免连带问题。4. 避坑指南VC6.0 CToolBarEx 最容易翻车的五个位置4.1 按钮显示不出来或图标错位现象编译通过运行后工具栏要么一片空白要么按钮图标张冠李戴。原因位图索引和按钮顺序没对齐。CToolBarEx 默认按 .rc 里 TOOLBAR 区块的 BUTTON 出现顺序去匹配 LoadToolBar 加载的位图如果你后来 AddButton 时给的位图索引和位图条里实际位置没有一一对应图标就会乱。中间插入 SEPARATOR 后这种情况更隐蔽因为分隔符不占位图索引但占按钮下标。解决我处理这个问题的固定做法是先把 .rc 里的按钮顺序抄出来再对照 Toolbar.bmp 从左到右数格子两个序号必须一一对上。AddButton 时把 nImage 显式写成位图格子号不要依赖自动累加这样至少错的时候一眼能看出来。4.2 Win11 下工程打开闪退或运行闪退现象从 pudn 下的 TB.rar 在 Win11 上双击 .dsw 文件VC6 的 IDE 一闪就没了编译出来的 exe 双击运行也是闪退。原因VC6 是上个时代的工具和新版 Windows 的图形驱动、老运行库兼容性都不好系统里缺旧版 MFC 运行库时exe 起来就崩。解决常见做法是把 VC6 的安装目录设置成兼容模式运行右键属性里选 Windows 7 兼容模式再补装 Microsoft Visual C 6.0 运行时库如果只是要跑编译成果安装对应版本的 visual c redistributable 包就能覆盖大部分 MFC42、msvcp 依赖。另一种更稳的思路是别在新系统里硬开 VC6 IDE直接用 Visual Studio 2019 及以上版本打开 TB.dsp让向导升级成新工程再编译。升级后 MFC 头文件路径会变可能要手动清理 atlbase.h 这类老引用。我在自己机器上就是 VC6 兼容模式加 redistributable 一起用工程能正常打开。4.3 下拉菜单弹出位置跑到屏幕左上角现象点按钮菜单没有出现在按钮下方而是浮在屏幕左上角。原因坐标转换漏了一步。TrackPopupMenu 弹出时接收的是屏幕坐标而按钮矩形 GetItemRect 拿到的是工具栏窗口内的客户区坐标两者直接混用就会跑到左上角。解决先取按钮矩形再转换到屏幕坐标最后传给菜单弹出函数。CRect rc; m_wndToolBar.GetItemRect(nIndex, rc); m_wndToolBar.ClientToScreen(rc); pMenu-TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, rc.left, rc.bottom, this, NULL);参数说明TrackPopupMenu 的第三个参数是菜单弹出的水平位置第四个是垂直位置rc.bottom 让菜单刚好贴在按钮下方水平位置用 rc.left 而不是 rc.right是因为贴右边缘会被屏幕边界截断。这个坑在示例工程里没有完全踩平需要自己补上。4.4 图标背景发黑或发青现象Toolbar.bmp 里看着干干净净运行后按钮图标背景一大片黑色或者青色。原因MFC 加载工具栏位图时默认把左上角第一个像素当作透明色。如果你用画图工具编辑过位图左上角恰好是黑或青色所有同颜色区域都会被抠成透明剩下其他颜色形成色块。解决编辑位图时保证左上角像素就是你想要的透明色且整张图不要出现和它相同颜色的干扰区域。更彻底的办法是换成 32 位带 alpha 通道的位图用 LoadImage 载入后再替换按钮位图新系统上效果最好但文件体积更大。我这几年做新界面已经不再用左上角透明那套老约定统一走 alpha 通道。4.5 压缩包自带缓存文件导致工程混乱现象把 TB.rar 复制到新目录后修改代码VC6 里跳转错乱、资源编辑器显示旧图甚至弹窗提示“请插入 Microsoft Visual C 6.0 盘”。原因包里带着 Thumbs.db、.ncb、.opt、.aps、.clw 这些缓存文件它们是从上传者机器上生成的路径、状态全部不适用于你的环境VC6 会误判工程状态。Thumbs.db 还会让资源管理器显示旧缩略图干扰判断。解决解压后第一件事删掉 Thumbs.db、TB.opt、TB.ncb、TB.aps、TB.clw再打开工程IDE 会重新生成这些文件。另外那个 #60 of 68.ico 文件名里的井号在资源编辑器里容易引发解析问题复制前先改成 ico60.ico 再引入。我拿到任何源码包都先做这步清理能省下大量排查假故障的时间。5. 把示例工程改造成自己的工具栏两个能直接落地的扩展点5.1 给普通按钮加 Toggle 语义工具栏最常用的扩展是“点一下选中、再点取消”配合 SetButtonInfo 的 TBBS_CHECKBOX 加自己的状态判断就能实现。示例工程的按钮大多是普通命令按钮我一般这样改void CMainFrame::OnViewBarcode() { BOOL bChecked (m_wndToolBar.GetButtonStyle(0) TBBS_CHECKED); m_wndToolBar.SetButtonInfo(0, ID_VIEW_BARCODE, bChecked ? TBBS_CHECKBOX : TBBS_CHECKBOX | TBBS_CHECKED, 0); ToggleBarcodePanel(bChecked ? FALSE : TRUE); }说明GetButtonStyle 是从 CToolBar 继承来的方法按位判断 TBBS_CHECKED 就能知道当前选中态。整段逻辑等于自己维护一个布尔状态比在类成员里额外存 BOOL 更不容易与界面失同步因为界面和状态是同一个来源。5.2 运行时切换位图做多套皮肤验证这个工程能不能扩展成多皮肤最快的方法是再备一条 Toolbar.bmp运行时替换if (!m_wndToolBar.LoadBitmap(IDR_TOOLBAR_DARK)) return; m_wndToolBar.Invalidate();LoadBitmap 把按钮位图换成另一张图Invalidate 强制重绘。两套位图的图标顺序和数量必须完全一致否则切换后按钮错位。这个扩展点不需要改任何按钮逻辑也是我验收 CToolBarEx 是否适合自己项目时最先做的实验能换图、能保持尺寸稳定这个类就能用。从那以后我每次接手别人发来的 VC6 源码包第一件事就是先删缓存文件再改代码先核对资源脚本、位图、按钮索引三个地方再谈业务逻辑。这套 CToolBarEx 工程代码量不算大但脏环境造成的假故障比真代码问题多得多你上来就调逻辑只会越调越浑。希望这点经验能帮到你。本文还有配套的精品资源点击获取