ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MFC工具栏扩展实战:CToolBarEx集成与高DPI适配指南

MFC工具栏扩展实战:CToolBarEx集成与高DPI适配指南 简介这份资源围绕MFC扩展工具栏类CToolBarEx在Visual C中的应用展开面向具备一定MFC基础、希望提升Windows界面交互效果的开发者。压缩包共30个文件约43KB以h头文件与cpp源文件为主体配合ico图标、bmp位图、rc资源脚本及dsp、dsw工程文件构成一套可直接编译运行的完整示例工程。内容涵盖自定义按钮图像、按钮与下拉菜单关联、图标文字并存、按钮禁用隐藏分组等状态管理并演示了创建对象、添加按钮、绑定消息映射、重载点击响应及设置按钮样式的完整流程。已有93人学习读者可通过源码理解CToolBarEx的集成方式掌握工具栏定制与动态菜单实现思路并借助工程文件快速搭建调试环境适合作为界面开发的学习起点与参考模板。1. 从 TB.rar 里的 CToolBarEx 说起一个被低估的 MFC 工具栏扩展类如果你手里有一个叫TB.rar的压缩包解压后看到CToolBarEx这个类名同时工程又是 Visual C 的 MFC 项目那你大概率正面对一个很具体的需求标准CToolBar不够用了。它不能放下拉按钮、不能做扁平化悬停高亮、不能把图标和文字排得好看甚至在高 DPI 下直接糊成一团。CToolBarEx就是在这个缝隙里长出来的东西——它是对 MFC 原生工具栏的一次扩展封装通常以.h/.cpp两个文件的形式散落在老项目里配合TB.rar这种打包方式流传。这篇文章不讲空泛的 MFC 历史只解决一件事拿到CToolBarEx这套代码后怎么在 Visual C 里把它跑起来、接进现有对话框或框架窗口、把按钮状态和消息映射调通以及那些年久失修的工程在 Unicode、高 DPI、资源 ID 冲突上会怎么翻车。适合正在维护老 MFC 项目、又不想推倒重写工具栏的工程师。2. CToolBarEx 到底扩展了什么先看清它和 CToolBar 的边界2.1 原生 CToolBar 的三个硬伤MFC 自带的CToolBar本质上是对TOOLBARCLASSNAME通用控件的薄封装。它有几个绕不过去的限制这也是CToolBarEx存在的理由。第一按钮样式固定。CToolBar只认TBBS_BUTTON、TBBS_SEPARATOR、TBBS_CHECKBOX这几种想做「按下后保持高亮」或者「带下拉箭头的分割按钮」得自己去SetButtonInfo里塞TBSTYLE_DROPDOWN然后手动处理TBN_DROPDOWN通知代码散得到处都是。第二绘制不可控。原生工具栏的图标来自CImageList尺寸在SetSizes里定死。你想在鼠标悬停时换一张高亮图或者根据禁用状态画灰度CToolBar不给你钩子只能整个自绘而自绘又要处理NM_CUSTOMDRAW复杂度陡增。第三布局僵化。图标和文字要么只显示图标要么图标加文字但位置由系统决定。做 Ribbon 风格或者紧凑型工具条时CToolBar的TBSTYLE_LIST只能算勉强能用间距和对齐很难调。CToolBarEx的常见做法是继承CToolBar重写DrawItem和OnCustomDraw把按钮状态普通、悬停、按下、禁用、选中拆成独立的绘制分支同时暴露一组SetButtonStyleEx、SetIconSize、EnableText之类的接口。这样调用方不用碰底层TBBUTTON结构改样式只调一个函数。2.2 从 TB.rar 里辨认可用版本拿到TB.rar后别急着往工程里拖。先解压看目录结构典型的CToolBarEx包会有这几类文件文件作用需要关注的点ToolBarEx.h类声明看是否继承CToolBar有没有DECLARE_DYNAMICToolBarEx.cpp实现看DrawItem是否用了CDC::DrawStateres\*.bmp按钮位图确认是 16x16 还是 24x24是否含 alphademo\*.dsp/*.vcxproj示例工程老.dsp需要 VS 转换可能丢配置ReadMe.txt说明常写「适用于 VC6」别全信如果只有.h和.cpp没有示例工程那就要自己建一个 MFC 对话框程序来验证。判断代码年代有个快办法看ToolBarEx.h里有没有#if _MSC_VER 1300这类条件编译。有说明作者考虑过 VS2003 之后的编译器没有大概率是 VC6 时代的产物直接拿到 VS2019/2022 下编译会报一堆C2440类型转换错误。2.3 最小接入路径三步把 CToolBarEx 挂到对话框上假设你已经在 Visual C 里建好了一个基于对话框的 MFC 工程现在要把CToolBarEx加进去。下面是最小可运行步骤。第一步把ToolBarEx.h和ToolBarEx.cpp复制到工程目录在「解决方案资源管理器」里右键项目 → 添加 → 现有项把两个文件加进来。注意.cpp要加入编译只加.h是不够的。第二步在对话框类的头文件里包含并声明成员// MyDlg.h #include ToolBarEx.h class CMyDlg : public CDialogEx { // ... 其他声明 protected: CToolBarEx m_wndToolBar; // 工具栏对象随对话框生命周期 // ... };第三步在OnInitDialog里创建并加载资源// MyDlg.cpp BOOL CMyDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 创建工具栏窗口绑定到对话框 if (!m_wndToolBar.Create(this, WS_CHILD | WS_VISIBLE | CBRS_TOP)) { TRACE0(Failed to create toolbar\n); return -1; } // 加载工具栏资源IDR_TOOLBAR1 需在资源视图里存在 if (!m_wndToolBar.LoadToolBar(IDR_TOOLBAR1)) { TRACE0(Failed to load toolbar resource\n); return -1; } // 设置按钮尺寸和图标尺寸高 DPI 下要按比例放大 m_wndToolBar.SetSizes(CSize(32, 32), CSize(24, 24)); // 启用文字标签并设置文字与图标的排列方式 m_wndToolBar.EnableText(TRUE); m_wndToolBar.SetTextAlign(CToolBarEx::TEXT_RIGHT); // 让工具栏自动调整大小并停靠 m_wndToolBar.SetBarStyle(m_wndToolBar.GetBarStyle() | CBRS_SIZE_DYNAMIC); RepositionBars(AFX_IDW_CONTROLBAR_FIRST, AFX_IDW_CONTROLBAR_LAST, 0); return TRUE; }这段代码的逻辑说明Create的第三个参数CBRS_TOP决定工具栏初始停靠位置如果对话框不需要停靠可以只给WS_CHILD | WS_VISIBLE。LoadToolBar传入的是资源 ID不是文件名所以必须先在资源视图里建好ToolBar资源并画上按钮。SetSizes第一个参数是按钮整体尺寸第二个是图标实际像素两者差值就是文字和边距的空间。EnableText和SetTextAlign是CToolBarEx相对原生类多出来的接口具体函数名可能因版本不同有差异如果编译报「未声明的标识符」去ToolBarEx.h里搜Text关键字找到实际暴露的方法名。参数上最容易错的是SetSizes。很多人只改图标尺寸不改按钮尺寸结果文字被裁掉。记住一个经验比例按钮宽度 ≈ 图标宽度 文字宽度 16 像素边距。如果按钮上只有图标按钮尺寸比图标大 8 到 12 像素比较舒服。3. 消息映射与状态同步让 CToolBarEx 的按钮真正干活3.1 ON_COMMAND 与 ON_UPDATE_COMMAND_UI 的配合工具栏按钮点下去没反应九成是消息映射没写对。CToolBarEx继承自CToolBar命令消息走的是标准 MFC 路由ON_COMMAND处理点击ON_UPDATE_COMMAND_UI处理启用/禁用和选中状态。在对话框类的消息映射表里加// MyDlg.cpp 的消息映射块 BEGIN_MESSAGE_MAP(CMyDlg, CDialogEx) ON_COMMAND(ID_BTN_OPEN, CMyDlg::OnBtnOpen) ON_UPDATE_COMMAND_UI(ID_BTN_OPEN, CMyDlg::OnUpdateBtnOpen) ON_COMMAND(ID_BTN_SAVE, CMyDlg::OnBtnSave) ON_UPDATE_COMMAND_UI(ID_BTN_SAVE, CMyDlg::OnUpdateBtnSave) END_MESSAGE_MAP()对应的处理函数void CMyDlg::OnBtnOpen() { // 点击打开按钮的实际逻辑 CFileDialog dlg(TRUE); if (dlg.DoModal() IDOK) { // 处理文件 } } void CMyDlg::OnUpdateBtnOpen(CCmdUI* pCmdUI) { // 根据当前状态决定按钮是否可用 pCmdUI-Enable(!m_bBusy); // m_bBusy 为真时按钮变灰 }逻辑说明ON_UPDATE_COMMAND_UI会在空闲时被 MFC 反复调用所以不要在里面做耗时操作否则界面会卡。pCmdUI-Enable(FALSE)会让按钮进入禁用状态CToolBarEx如果重写了绘制应该自动画成灰度图如果没重写按钮只是不响应点击外观不变这时候需要手动调SetButtonStyleEx刷新。参数上注意CCmdUI的SetCheck和SetRadio。做「按下保持」的切换按钮时用pCmdUI-SetCheck(m_bToggle)CToolBarEx会把按钮画成按下状态。如果按钮是TBBS_CHECKBOX样式MFC 会自动切换m_bToggle但前提是你在ON_COMMAND里没有手动改状态。3.2 下拉按钮和自定义通知的处理CToolBarEx如果支持下拉按钮通常会暴露一个SetDropDownButton或者要求调用方在LoadToolBar之后手动改样式。典型做法是// 把第 2 个按钮改成下拉样式 int nIndex 1; UINT nID m_wndToolBar.GetItemID(nIndex); m_wndToolBar.SetButtonStyleEx(nIndex, TBBS_DROPDOWN | TBBS_BUTTON); m_wndToolBar.SetButtonInfo(nIndex, nID, TBBS_DROPDOWN | TBBS_BUTTON, nIndex);然后处理TBN_DROPDOWN通知。这个通知不是命令消息要在父窗口的消息映射里用ON_NOTIFY// 消息映射 ON_NOTIFY(TBN_DROPDOWN, IDR_TOOLBAR1, CMyDlg::OnToolbarDropdown) // 处理函数 void CMyDlg::OnToolbarDropdown(NMHDR* pNMHDR, LRESULT* pResult) { LPNMTOOLBAR pNMTB reinterpret_castLPNMTOOLBAR(pNMHDR); switch (pNMTB-iItem) { case ID_BTN_OPEN: // 在按钮下方弹出菜单 { CMenu menu; menu.LoadMenu(IDR_MENU_OPEN); CMenu* pPopup menu.GetSubMenu(0); CRect rect; m_wndToolBar.GetItemRect( m_wndToolBar.CommandToIndex(ID_BTN_OPEN), rect); m_wndToolBar.ClientToScreen(rect); pPopup-TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON, rect.left, rect.bottom, this); } break; } *pResult 0; }这里的关键参数是CommandToIndex它把命令 ID 转成按钮索引再用GetItemRect拿到屏幕坐标。如果CToolBarEx重写了GetItemRect但没处理好文字偏移弹出的菜单可能会偏几个像素这是常见的小毛病不影响功能但看着别扭。3.3 高 DPI 下的尺寸与图像列表重建Visual C 项目从 VS2015 开始默认带app.manifest高 DPI 感知打开后CToolBarEx如果按固定像素创建CImageList在 150% 缩放下图标会模糊。解决办法是在OnInitDialog里根据当前 DPI 重新算尺寸// 获取当前窗口 DPI HDC hdc ::GetDC(m_hWnd); int nDpi ::GetDeviceCaps(hdc, LOGPIXELSX); ::ReleaseDC(m_hWnd, hdc); // 基准 DPI 是 96算出缩放比例 int nScale nDpi * 100 / 96; // 按比例设置尺寸 m_wndToolBar.SetSizes( CSize(32 * nScale / 100, 32 * nScale / 100), CSize(24 * nScale / 100, 24 * nScale / 100)); // 如果 CToolBarEx 有重建图像列表的接口在这里调用 m_wndToolBar.RebuildImageList();注意RebuildImageList不是标准 MFC 接口是CToolBarEx可能提供的方法。如果头文件里没有就得手动SetImageList换一张高分辨率位图。高 DPI 这块没有银弹最稳的做法是准备两套位图资源按 DPI 切换。4. 避坑与排查CToolBarEx 在老工程里最容易翻车的五件事4.1 编译报 C2440不能从 const char* 转换为 LPCTSTR现象把 VC6 时代的ToolBarEx.cpp拖进 VS2019 后满屏C2440错误集中在字符串赋值和CString构造上。原因VC6 默认多字节字符集CString内部是charVS2019 新建工程默认 UnicodeCString变成wchar_t。老代码里写strText Open这种字面量在 Unicode 下类型不匹配。解决不要急着把工程改成多字节字符集那会牵连整个项目。正确做法是在ToolBarEx.cpp顶部加#include tchar.h然后把所有...字面量包上_T()例如_T(Open)。如果代码里用了sprintf换成_stprintf。改完重新编译C2440基本能清掉。4.2 工具栏显示为空白条资源 ID 对不上或位图没加载现象Create和LoadToolBar都返回成功但工具栏区域一片灰没有按钮。原因LoadToolBar传入的IDR_TOOLBAR1在资源里不存在或者存在但位图资源被删了。MFC 的LoadToolBar会去资源里找RT_TOOLBAR类型的资源如果只有位图没有工具栏资源定义它加载不到按钮布局。解决打开资源视图确认Toolbar节点下有对应 ID 的资源双击能看到按钮编辑器。如果只有一张 BMP需要手动新建一个 Toolbar 资源把按钮一个个画上去再把 ID 改成代码里用的那个。另一个可能是SetSizes设得太大按钮被挤出可视区域把尺寸改小试试。4.3 按钮点击无响应消息映射没进对类现象按钮画出来了鼠标悬停也有高亮但点击没反应。原因ON_COMMAND写在了错误的类里。如果工具栏属于对话框命令消息会先发给对话框再发给框架窗口。如果对话框没有对应的ON_COMMAND消息会继续往上冒最终没人处理。解决在对话框类的消息映射里加ON_COMMAND并确认命令 ID 和工具栏按钮的 ID 一致。用CommandToIndex反查一下按钮 ID 是否和预期相同。如果工具栏是挂在CFrameWnd上的命令消息可能被CView截获检查CView的映射表。4.4 高亮状态错乱自绘时没区分 hot 和 pressed现象鼠标划过按钮时高亮但点下去之后高亮不消失或者多个按钮同时高亮。原因CToolBarEx的DrawItem里用了一个成员变量记录当前 hot 按钮但没有在OnMouseMove里清除上一个按钮的状态。老代码常见写法是m_nHot nIndex却忘了在鼠标离开时置-1。解决在OnMouseMove里先判断鼠标是否还在工具栏客户区不在就m_nHot -1并Invalidate。如果CToolBarEx没有暴露这个逻辑在对话框的OnMouseMove里调m_wndToolBar.SendMessage(WM_MOUSEMOVE, ...)模拟一下或者直接改ToolBarEx.cpp在OnMouseLeave里重置状态。4.5 停靠后位置漂移RepositionBars 调用时机不对现象工具栏创建后位置正常但对话框一改变大小工具栏就跑到左上角或者和别的控件重叠。原因RepositionBars在OnInitDialog里调用时对话框还没完成布局算出来的客户区尺寸是错的。另外如果对话框用了WS_THICKFRAME每次OnSize都要重新排。解决把RepositionBars放到OnSize里并且在OnInitDialog的最后调一次。如果工具栏和别的控件抢位置用m_wndToolBar.GetWindowRect拿到实际高度手动MoveWindow调整其他控件。停靠工具栏的CBRS_TOP样式会让它占据客户区顶部对话框里的其他控件要相应下移。5. 进阶把 CToolBarEx 改造成可换肤的现代工具栏5.1 用 CMFCVisualManager 接管绘制如果你的 Visual C 版本是 VS2008 SP1 及以上MFC 已经带了CMFCVisualManager体系。与其在CToolBarEx里硬编码颜色不如让它继承CMFCVisualManager的绘制接口。做法是给CToolBarEx加一个SetVisualManager方法在DrawItem里先问 manager 要颜色// 在 ToolBarEx.cpp 的 DrawItem 里 CMFCVisualManager* pVM CMFCVisualManager::GetInstance(); if (pVM) { // 让 visual manager 画按钮背景 pVM-OnDrawToolbarButton(pDC, this, nIndex, nState, rect); } else { // 回退到老的自绘逻辑 DrawItemLegacy(pDC, nIndex, nState, rect); }这样换肤时只需要切换CMFCVisualManager的实例不用改CToolBarEx的代码。参数nState要传对CMFCVisualManager认的是AFX_BUTTON_STATE枚举不是自定义的整数。5.2 验证改造是否成功的三个检查点改完之后别只看界面按下面三个点验证检查点操作预期结果状态切换鼠标悬停、按下、禁用按钮三种状态颜色明显不同无残留换肤调CMFCVisualManager::SetDefaultManager换一个 manager工具栏颜色跟着变不崩高 DPI把系统缩放调到 150%重启程序图标不模糊按钮不重叠如果换肤后工具栏没变检查CToolBarEx是否在OnUpdateCmdUI里强制Invalidate没有的话手动加一个RedrawWindow。5.3 我踩过的一个坑别在 DrawItem 里创建 GDI 对象最后说一个血泪经验。早期我在DrawItem里图省事每次绘制都CreatePen、CreateSolidBrush结果工具栏按钮一多GDI 对象数飙升程序跑半小时就报「GDI 资源耗尽」。后来改成在类初始化时创建好CPen、CBrush成员DrawItem里只SelectObject用完恢复。这个习惯救了我后面好几个项目。如果你也在维护老 MFC 工具栏先别急着推翻重写把CToolBarEx接进去、消息映射调通、高 DPI 处理掉基本就能再战几年。希望帮到你。本文还有配套的精品资源点击获取
返回列表