首页 / 资讯中心 / 文章详情

MFC CToolBar自定义图片与文字:完整方案与避坑指南

MFC CToolBar自定义图片与文字:完整方案与避坑指南 ★ FEATURED ARTICLE
简介在Windows界面开发中工具栏是高频交互组件而MFC的CToolBar常因资源类型、样式位和尺寸设置不当导致图片不显示或文字错位。理解CToolBar底层基于Win32工具栏控件的原理是自定义图片按钮和文字布局的关键——SetButtons定义按钮结构CImageList承载图标TBSTYLE_TEXT与TBSTYLE_LIST控制图文排列。掌握这些机制后开发者可通过动态创建、系统图标复用及自绘扩展灵活适配固定与动态按钮、多语言切换及高DPI场景。本文围绕CToolBar的图片加载链路、文字注入方式及样式控制系统梳理从资源编辑器到代码实现的完整路径并结合常见故障案例提供可直接落地的工程实践方案帮助读者高效打造简洁可靠的MFC工具栏。1. CToolBar 自定义图片和文字为什么照着教程写还是显示不对做 MFC 界面的人早晚都要跟 CToolBar 打交道尤其是想做出“带图标又带文字”的工具按钮时大多数人会卡在同一个地方位图明明加载了按钮上却一片空白或者文字怎么设置都不显示更有甚者图片和文字挤在一起看起来像没对齐的半成品。这个标题里的“自定义图片及文字”看起来简单实际牵扯到 CToolBar 的样式位、按钮状态、资源加载顺序、高 DPI 适配几条线。我最早做某跨平台系统时也被这玩意磨了一周后来把整个调用链捋清楚才发现问题根源经常不在“画图”这一步而在资源类型、样式注册和尺寸设置这三者的配合上。这篇文章就把我从 CToolBar 的按钮数组构建、位图加载、文字注入到自绘扩展的完整方案讲清楚代码可以直接拿去改每一步都说明为什么这么做以及最容易翻车的地方在哪里。2. 自定义图片按钮从资源编辑器到 SetButtons 的完整链路2.1 为什么大多数教程教你“画位图”却还是显示空白很多初学 MFC 的人拿到 CToolBar 的教程第一步通常是在资源编辑器里导入一张 BMP然后调用LoadToolBar(IDR_TOOLBAR)接着就等着按钮显示出来。但结果往往是工具栏出现了按钮也有立体框可框里根本没有图。这时候大多数人会怀疑是位图格式问题其实真正的坑在于 MFC 的LoadToolBar是从.rc文件里解析TOOLBAR资源的它按固定的“内部按钮 ID 表”去裁剪位图。如果你导入的是一个普通位图而不是TOOLBAR类型的资源LoadToolBar完全可以返回成功但它会认为位图是按“一个按钮对应一个固定宽度”切割的而普通 BMP 的资源头里根本没有按钮宽度信息于是切割出来的每一块都只包含左上角几个像素视觉上就是空白。另一种常见的做法是用SetBitmap指定一个 HBITMAP再配合SetButtons告诉它按钮个数。这条路比LoadToolBar更直接但很多人在这里翻车的原因是SetBitmap使用的位图必须是一个“横向平铺的位图”即 N 个按钮图标的像素从左到右连续排列。而很多初学者用某画图工具画了一个 16x16 的单独小图标直接传给SetBitmapCToolBar 却默认按GetButtonStyle中的TBSTYLE_AUTOSIZE去切切出来的第一块就是整个位图剩下按钮没有图像于是只有第一个按钮有图后面的全是空白。所以在动手写代码前必须先把概念立住CToolBar 的图片来源可以是一个“图带”image strips也可以是一个CImageList。前者适合静态设计资源后者适合运行时动态创建。我们下面两条路都讲但你要先决定自己的场景——工具栏按钮数量是固定的还是会在运行时增加或删除。固定用图带动态用CImageList这是选型的第一原则。2.2 用 SetBitmap 和 LoadToolBar 的两种常规做法先说最简单的LoadToolBar。要让它可靠工作必须保证两点第一资源是真正的TOOLBAR类型而不是 BITMAP第二按钮 ID 数组与资源里定义的按钮顺序一致。一般我在.rc文件里看到的结构是这样的IDR_MAIN_TOOLBAR TOOLBAR 16, 16 BEGIN BUTTON ID_FILE_NEW SEPARATOR BUTTON ID_FILE_OPEN BUTTON ID_FILE_SAVE END这段资源声明里16, 16表示每个按钮图片的宽度和高度。MFC 会在加载时把整张位图按这个尺寸切成若干块切出来的顺序就是BEGIN块里BUTTON的顺序。注意SEPARATOR不会占图片位置但它会在按钮数组里产生一个空项。如果你在代码里用SetButtons自己定义按钮数组也要把分隔符的位置对应好否则后续按钮的图片会整体错位一位。// 工具栏加载使用资源编辑器自动生成的 CToolBar if (!m_wndToolBar.Create(this, WS_CHILD | WS_VISIBLE | CBRS_TOP) || !m_wndToolBar.LoadToolBar(IDR_MAIN_TOOLBAR)) { TRACE(工具栏加载失败\n); return -1; } // 必须调用以下两个函数让命令路由生效 m_wndToolBar.SetWindowText(_T(主工具栏)); m_wndToolBar.EnableDocking(CBRS_ALIGN_TOP);代码逻辑Create创建窗口和按钮条LoadToolBar根据 TOOLBAR 资源自动建立按钮数组和位图关联。这两步不能换顺序。如果你在Create之后再改按钮样式一定要调用SetWindowText只是为了给停靠时显示标题用不是必须但建议养成习惯。参数说明CBRS_TOP是停靠方向的初始值不是最终停靠状态。EnableDocking是框架窗口调用的不是给工具栏自己的。如果将来按钮文字不能显示问题绝对不在这两行而在后面要讲的样式上。2.3 动态创建工具栏并给按钮贴图的代码示例与参数说明需要动态创建工具栏时LoadToolBar就不够灵活了。常见业务场景是根据用户权限决定显示哪些按钮甚至按钮图标不是来自资源而是来自外部 ICO 文件。这时我一般直接用CImageList管理图标。下面的代码演示一个最小可运行流程// 动态创建带图片的工具栏 CImageList imageList; imageList.Create(16, 16, ILC_COLOR32 | ILC_MASK, 5, 1); // 从资源或文件加载图标追加到 ImageList HICON hIcon (HICON)::LoadImage(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_ICON_NEW), IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR); imageList.Add(hIcon); // 继续添加其它图标... // 创建 CToolBar 控件开关窗口 m_wndToolBar.Create(this, WS_CHILD | WS_VISIBLE | CBRS_TOP, IDC_MY_TOOLBAR); m_wndToolBar.SetImageList(imageList); m_wndToolBar.SetButtons(NULL, 3); // 先创建3个空按钮 // 为每个按钮设置命令ID同时把按钮样式改成标准按下态 m_wndToolBar.SetButtonInfo(0, ID_FILE_NEW, TBBS_BUTTON, 0); m_wndToolBar.SetButtonInfo(1, ID_FILE_OPEN, TBBS_BUTTON, 1); m_wndToolBar.SetButtonInfo(2, ID_FILE_SAVE, TBBS_BUTTON, 2);逻辑说明CImageList必须是工具栏成员变量或static成员不能是局部变量。因为SetImageList只是保存了指针局部对象析构后原图像会失效。这是新手最容易犯的错后面避坑章节再展开。SetButtons(NULL, 3)第一个参数传 NULL 的意思是“不预设按钮 ID全部用默认 ID”真正配置在SetButtonInfo中完成。SetButtonInfo三个参数分别是按钮索引、命令 ID、按钮样式和图片索引。参数说明ILC_COLOR32让图标支持带透明通道的 BMPILC_MASK则把点阵当透明掩码。对于现代视觉风格建议只需要ILC_COLOR32不要混用 MASK因为 MASK 会按单一颜色抠图遇到图标抗锯齿边缘会留下黑边或白边。如果你从外部加载 PNG 转成 HICON用 COLOR32 就够了。IMAGE_ICON加载出来的图标自带 alpha视觉效果最干净。这里有个细节SetButtonInfo的第三个参数TBBS_BUTTON是普通按钮如果要做SEPARATOR或TBBS_CHECKBOX在动态创建时也用同一个函数你把样式位组合传进去就行。图片索引从 0 开始对应CImageList里的顺序千万别传混淆。3. 让文字显示出来按钮文字、命令提示与多语言的土办法3.1 为什么文字总是不出现TBSTYLE_TEXT 和 SetButtonText 的关系工具栏按钮文字是 MFC 里一个比较拧巴的功能。你给按钮调用了SetButtonText运行起来却什么字都看不到。这几乎可以断定是按钮条本身没有启用TBSTYLE_TEXT样式。在 Win32 工具栏控件中SetButtonText只是写入了一个字符缓冲但控件的绘制逻辑在没有TBSTYLE_TEXT时默认不绘制文字。MFC 的 CToolBar 在Create时并不会自动替你把文字样式加上即使工具栏资源里定义了文字也需要代码里明确设置。另一个容易忽略的点是如果工具栏按钮同时有图标和文字而样式里没有启用TBSTYLE_LIST那么图片和文字就会重叠显示文字直接把图标盖住一部分视觉效果是“文字出现了但很丑”。TBSTYLE_LIST表示文字放在图标右侧并且水平排列如果不加工具栏默认按下按钮时会顺序绘制图标和文字但图标尺寸受SetBitmap的图片尺寸约束文字会覆盖在图标上。所以结论是要做图文并排必须同时设置TBSTYLE_TEXT和TBSTYLE_LIST。样式设置时机也有关必须在按钮创建之后、第一次显示之前设置否则窗口已重绘再改样式不生效或者只是闪了一下又变回去。我把这个问题列为“工具栏文字显示”头号坑。// 开启文字显示的完整样式组合 m_wndToolBar.ModifyStyle(0, TBSTYLE_TEXT | TBSTYLE_LIST); // 为每个按钮设置文字内容 m_wndToolBar.SetButtonText(0, _T(新建)); m_wndToolBar.SetButtonText(1, _T(打开)); m_wndToolBar.SetButtonText(2, _T(保存)); // 设置文字与图标的间距避免贴在一起 m_wndToolBar.SetInsertMarkColor(RGB(255, 0, 0)); // 无关只是举例逻辑说明ModifyStyle第一个参数是需要清除的样式第二个是要添加的样式。这里传 0 表示不清除任何原有样式。SetButtonText的索引和GetButtonInfo返回的索引一一对应凡是SEPARATOR的按钮索引也占位如果你在中间插入了分隔符后续按钮的索引要跳过那个位置否则文字会写错按钮。参数说明TBSTYLE_TEXT让按钮显示文字TBSTYLE_LIST让文字在图标右侧水平排列。还有TBSTYLE_ALTDRAG、TBSTYLE_FLAT等但图文显示相关的核心就是这两个。ModifyStyle是CWnd的通用方法对于 Win32 工具栏来说它最终会向控件发送TB_SETSTYLE消息吗实际上 MFC 的CToolBar::ModifyStyle在工具栏的派生类里被重写内部会调用CToolBar::ChangeExStyle和CToolBar::SetButtonStructSize等但最终影响样式的是TB_SETSTYLE。你不需要深究细节只需知道必须在Create之后立刻设置。3.2 设置文字并控制布局的代码示例每个按钮文字的宽度自适应是最常见的需求。CToolBar 提供了一个GetButtonWidth和SetButtonWidth但在图片和文字同时存在时更建议让控件自己算调用AutoSize()之后它会根据按钮上的图标尺寸和文字字符串长度自动调整按钮宽度和工具栏整体宽度。// 自动调整按钮尺寸以适应文字 void CMainFrame::RefreshToolBarText() { for (int i 0; i m_wndToolBar.GetCount(); i) { CString strText; // 根据命令ID从资源表读取本地化文字 UINT nID m_wndToolBar.GetItemID(i); if (nID 0) continue; // 分隔符 BOOL bResult strText.LoadString(nID); if (bResult) { m_wndToolBar.SetButtonText(i, strText); } } m_wndToolBar.AutoSize(); // 强制重排父窗口客户区避免布局错乱 RecalcLayout(); }逻辑说明GetItemID返回按钮的命令 ID如果返回 0 说明是分隔符。LoadString(nID)会从字符串表资源中读取对应 ID 的字符串。这种做法的好处是文字集中管理切换语言时可以加载不同语言的字符串资源而不用改代码。AutoSize是 CToolBar 的固有方法它会重算每个按钮的宽高但注意工具栏的AutoSize不会自动调用父级的RecalcLayout所以你还需要在函数最后通知控制条重新布局否则可能出现工具栏尺寸变宽了但整个带状区域没有重绘的残留痕迹。参数说明GetCount返回按钮总数包含分隔符。如果你在资源里定义了SEPARATOR它的GetItemID返回 0而空白位置也占一个索引这个索引在SetButtonText时要跳过否则后续按钮的文字全跑到前一个按钮上。强烈建议用ID_SEPARATOR常量代替裸的 0 判断因为 MFC 内部确实用 0 表示分隔。3.3 图片文字水平排列与垂直排列的调整有些界面希望文字在图片下方像标准 Office 大按钮。CToolBar 本身不直接支持上下结构但要实现并不难把TBSTYLE_LIST去掉仅保留TBSTYLE_TEXT同时把按钮宽度设成固定值并把文字绘制成多行。但 MFC 自带的绘制逻辑不会做多行居中文字会从左上角开始画。于是很多人开始踩“文字对不齐”的坑。我的做法是放弃默认绘制转而用NM_CUSTOMDRAW消息自己做部分绘制。先让工具栏使用标准风格然后在OnTbCustomDraw里针对按钮文字绘制做调整// 在消息映射里添加 ON_NOTIFY(NM_CUSTOMDRAW, IDC_MY_TOOLBAR, CMainFrame::OnTbCustomDraw) void CMainFrame::OnTbCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) { LPNMTBCUSTOMDRAW lpTBCD (LPNMTBCUSTOMDRAW)pNMHDR; if (lpTBCD-nmcd.dwDrawStage CDDS_PREPAINT) { *pResult CDRF_NOTIFYITEMDRAW; return; } if (lpTBCD-nmcd.dwDrawStage CDDS_ITEMPREPAINT) { // 在这里可以重设文本颜色、背景色、以及处理上下布局 *pResult CDRF_SKIPDEFAULT; // 或者 CDRF_NOTIFYPOSTPAINT 做二次绘制 } }逻辑说明NM_CUSTOMDRAW是通用控件自绘通知工具栏也支持。上面的代码只是骨架真正的垂直排列绘制要做成先调用默认绘制画图标再用DrawText在按钮下半部画文字上半部留出图标空间。这种方法的优点是完全掌控布局缺点是绘制代码和字体、颜色状态绑定工作量大。更适合的方式是直接创建多个较小的按钮用TBSTYLE_LIST 两行文字或者干脆用CMFCToolBarMFC 功能包提供的扩展工具栏后者自带SetButtonTextCMFCToolBarButton的多行显示能力。不过本文主题限定在 CToolBar所以我建议只针对个别需要上下排的大按钮做自绘不要全局改。这里补充一个判断标准如果你只想让文字水平跟在图标右侧用TBSTYLE_TEXT | TBSTYLE_LIST就足够代码改动最小。如果你必须做上下结构就用自绘并且建议彻底弃用TBSTYLE_TEXT自己全部画否则文字由系统画、图标由你画两套逻辑叠在一起容易出鬼影。4. 自定义外观的高阶控制样式、尺寸、状态与自绘4.1 CToolBar 常用的样式位TBSTYLE_ 家族与 CControlBar 的配合CToolBar 虽然是 MFC 类但底层是 Win32 工具栏控件它支持一系列TBSTYLE_开头的样式。这些样式位决定了按钮的表现形式。我个人使用过的常用组合如下表可以作为选型参考样式作用使用建议TBSTYLE_FLAT扁平按钮鼠标悬停时高亮现代界面默认开启TBSTYLE_TRANSPARENT工具栏背景透明适合停靠到非标准背景容易造成重绘闪烁慎用TBSTYLE_LIST文字在图标右侧水平排列图文并排必加TBSTYLE_TEXT允许显示按钮文字不设置则SetButtonText无效TBSTYLE_CHECK按钮变为复选样式可以点选与取消做开关按钮很方便TBSTYLE_CHECKGROUP单选组样式组内只能选一个需要配合TBBS_GROUP使用“样式位和 CControlBar 的配合”指的是CBRS_*系列成员函数它们控制停靠、拉伸、边框等行为但与TBSTYLE_没有直接冲突。你可以共存。例如CBRS_FLOAT_MULTI允许工具栏停靠时横向排列多个分段而TBSTYLE_FLAT不影响这个行为。我见过有人把CBRS_SIZE_DYNAMIC误以为是“按钮大小可调”其实它只是让工具栏可被用户拖拽时动态扩展宽度。4.2 调整按钮大小和图片大小的入口SetSizes 与 SetHeight当你觉得按钮太拥挤或者图标模糊时最容易想到的是改位图资源。但这有滞后性。运行时用代码调整更高效。SetSizes和SetHeight是两个不同层面的函数经常被搞混// 设置按钮总尺寸包括边框和图片尺寸 m_wndToolBar.SetSizes(CSize(32, 32), CSize(20, 20)); // 单独设置控制条高度仅影响工具栏整体高度按钮按原尺寸居中 m_wndToolBar.SetHeight(40);逻辑说明SetSizes的第一个参数指定按钮矩形的宽高第二个参数指定位图区域的宽高。图片会被拉伸或压缩到第二参数大小按钮的可点击区域由第一参数控制。如果图片尺寸与按钮尺寸相差过大图片会在按钮内按左上角对齐而不是居中所以我用这个表格来统一参数场景按钮尺寸 (cx, cy)图片尺寸 (cx, cy)常规小图标24, 2416, 16常用工具条28, 2820, 20触摸友好36, 3624, 24SetHeight只是一个快捷方法内部还是会调用SetSizes只不过保持了图片原尺寸。如果你发现设置后按钮图片有偏移优先检查是不是直接用了SetButtonInfo里的图片索引去对SetSizes产生的坐标二者没有关系但容易让人误以为设置顺序会影响居中。需要强调SetSizes必须在按钮创建之后调用而且调用之前不要先SetBitmap否则工具栏内部缓存的老尺寸会影响后续绘制。最稳妥的做法是在LoadToolBar或SetButtons之后、任何UpdateWindow之前调用。如果界面已经显示再调用SetSizes可能会发生按钮重叠需要紧跟Invalidate和UpdateWindow。4.3 用 OwnerDraw 自绘实现任意效果的思路当图标来源复杂或者要绘制动画状态时标准按钮样式就不够用了。OwnerDraw 可以让按钮完全由你绘制但 CToolBar 的 OwnerDraw 有个特别麻烦的地方每个按钮都有TBBS_OWNERDRAW样式绘制时系统通过WM_DRAWITEM消息把按钮矩形交给你。你需要在OnDrawItem中处理而工具栏自身的背景和立体边框得你自己画。void CMainFrame::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct) { if (lpDrawItemStruct-CtlType ODT_BUTTON) { // 从按钮自定义数据中获取原始 ID区分不同按钮 WORD nCmdID LOWORD(lpDrawItemStruct-CtlID); // 这里根据 nCmdID 绘制图标和文字 // 注意要处理 ODS_SELECTED 和 ODS_DISABLED 状态 } else { CFrameWnd::OnDrawItem(nIDCtl, lpDrawItemStruct); } }逻辑说明OnDrawItem是一个分散在多个窗口类里的回调工具栏的 owner-draw 按钮会触发它。绘制过程中需要判断状态lpDrawItemStruct-itemState里的ODS_SELECTED表示按下ODS_DISABLED表示禁用。你还可以用ODS_CHECKED处理复选状态。按钮上如果既要有图标又要有文字就把位图先绘制在矩形左侧再计算文字矩形用DrawText绘制文字并居中。参数说明nIDCtl是触发绘制子控件的资源 ID但如果你在SetButtonInfo里设置的按钮 ID 是命令 ID这里的CtlID可能不是命令 ID。更可靠的方式是在按钮创建时为每个按钮设置一个唯一的子 ID例如从IDC_TOOLBAR_BTN_BEGIN递增然后在SetButtonInfo里把命令 ID 设置成实际希望发送命令的那个 ID。绘制时用子 ID 区分点击行为用命令 ID。这种拆分逻辑对自绘按钮很重要因为工具栏最底层的按钮头里可以保存一组自有数据MFC 的GetButtonInfo返回的则是被包装过的。自绘的坑在于Windows 工具栏自己会处理鼠标点击和焦点如果你绘制逻辑没有适配按钮矩形偏移点击时可能发生热点错位。我见过最诡异的情况是按钮显示的地方和实际点击热区相差几个像素就是因为自绘时画到了扩展的按钮矩形但命中的是按钮的普通矩形。解决办法是把 DrawItem 得到的矩形和按钮本身的大小对齐不要使用子类化后的坐标换算。5. 工具栏自定义避坑指南图片不显示、文字错位与高DPI的五个踩坑记录5.1 图片总是灰色/空白LoadBitmap 的 ID 类型不是 TOOLBAR现象用LoadToolBar加载一个 BITMAP 资源成功但运行后工具栏按钮没有图片按钮只是形状和状态栏文字。原因LoadToolBar只认TOOLBAR类型的资源。直接导入位图得到的资源类型是 BITMAPMFC 的解析逻辑会读不到TOOLBAR头于是按钮数组个数变成 0工具栏只显示一个空框。解决在 VS 资源视图里为工具栏新建目录选择“Toolbar”而不是“Bitmap”然后把位图粘贴到 Toolbar 编辑器里。如果是从外部文明资源迁移建议直接在.rc文件里手动新增TOOLBAR资源块同时添加TOOLBAR的宏定义。也可以放弃资源方式改用CImageListSetButtons作为统一方案。我后来彻底改用后一种方式彻底绕开资源类型问题。5.2 文字显示成“烫烫烫”字符串编码和按钮文本长度问题现象SetButtonText传入的字符串是正常的中文按钮上显示却是乱码或类似“烫烫烫”的填充字符。原因中文在非 Unicode 的 MFC 工程里CString默认是 ANSI 编码但工具栏内部存储文本时可能按 Unicode 处理或反过来。尤其当资源文件编码和工程字符串编码不一致时LoadString读出的字节流在转换为 TCHAR 数组时发生截断。SetButtonText底层调用TB_SETBUTTONINFO其pszText指向的是内部缓冲如果字符串超长或编码不匹配就会读出内存里的未初始化区域表现就是“烫烫烫”。解决确保工程使用 Unicode 字符集。在项目属性 - 常规 - 字符集里选“使用 Unicode 字符集”。然后所有字符串直接使用_T()宏包裹。如果必须使用 ANSI需要调用MultiByteToWideChar转换后再传入。另外不要把字符串直接写入按钮文本而是从字符串表资源读取由系统保证编码一致性。5.3 按钮尺寸设置后图片居中不了SetSizes 与位图尺寸不匹配现象调用SetSizes(CSize(32, 32), CSize(16, 16))后按钮看起来宽了但图片被挤在按钮左上角右下区域空白。原因CToolBar 内部计算图片位置时会根据SetSizes的第二参数设置图片矩形但图片绘制位置默认是从按钮矩形的左上角偏移边框宽度开始。如果按钮边框较大图片就不会居中。在 MFC 中SetSizes会同时设置按钮的边距和图片的绘制原点但如果你此前直接SetBitmap而且位图宽度小于按钮宽度图片会居左显示。解决不要让图片矩形小于可用区域太多。推荐的图片和按钮差值为 4-8 像素比如 16 的图片配 22-24 的按钮。另外SetSizes前先调用GetToolBarCtrl().SetButtonSize(CSize(w, h))然后把图片尺寸设成相同的值或略小最后再AutoSize。如果仍偏移就自绘按钮并自己控制居中这也是很多代工具条采用的绝对办法。5.4 高DPI下图标模糊缩放按钮尺寸却没换图标资源现象系统缩放 150% 时工具栏按钮变大但图标边缘发虚文字也有重影。原因CToolBar 不会自动切换位图资源。它在创建时按 DPI 感知的级别设置按钮尺寸但位图是固定像素的高 DPI 下只能做位图拉伸当然模糊。文字则因为 GDI 字体未同步缩放导致文字和图标尺寸不协调。解决在应用启动早期调用SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)针对 Win10 1703 以上然后处理WM_DPICHANGED消息。在WM_DPICHANGED中重新创建工具栏的CImageList从多个分辨率图标中选择一个或者直接加载 SVG 转换成的 HICON。如果不想做多套图标可以固定使用 20x20 或 24x24 的图标并关闭 DPI 缩放但界面会发虚。更实用的是只保留 16、20、24 三套位图通过编译常量或资源 ID 后缀切换。5.5 热区混乱在 CToolBar 里插入了其他控件导致坐标偏移现象工具栏上添加了组合框或按钮后点击它右边的按钮时实际响应的是左边的按钮。原因CToolBar 支持在按钮间嵌入子窗口但当你用SetButtonInfo调整按钮顺序时MFC 内部维护的按钮数组和子控件矩形没有同步更新。尤其是你使用了TBSTYLE_SEP而不是TBBS_SEPARATOR分隔符的宽度设置不当会压缩下一个按钮的热区坐标。解决在动态创建包含子控件的工具栏时分隔符必须使用TBBS_SEPARATOR并且通过SetButtonInfo设置其宽度为 0然后在消息TBN_GETBUTTONINFO中指定子窗口的显示区域。更稳妥的方案是把子控件单独作为一个子窗口放在工具栏旁边而不是嵌入按钮之间。如果必须嵌入记住插入子控件后要重新AutoSize和RecalcLayout而且不要再修改按钮索引否则热点映射会全乱。6. 进阶无位图工具栏 系统图标列表 运行时换肤的一种干净做法6.1 放弃位图用系统图标列表生成按钮如果你不想维护那一堆 BMP可以换成直接使用系统图标资源比如SHGetFileInfo或ImageList_LoadImage从 shell 提取图标。这在做文件管理类工具时尤其方便按钮图标能直接反映文件类型。做法是先用SHGetFileInfo拿到 HICON再Add进CImageList后续逻辑和动态创建工具栏完全一样。关键是图标句柄的释放时机SHGetFileInfo返回的图标在添加到 ImageList 后ImageList 会复制图标内容而不持有原始句柄所以你可以立刻DestroyIcon释放。如果忘了释放每次创建工具栏都会泄漏 GDI 句柄应用长时间运行后界面会逐渐渲染异常。我在某图像处理 Demo 里就是这样出现工具栏上图标随机消失的现象排查了许久才发现是 GDI 泄漏。// 从文件扩展名获得图标 SHFILEINFO shfi { 0 }; if (::SHGetFileInfo(_T(.txt), FILE_ATTRIBUTE_NORMAL, shfi, sizeof(shfi), SHGFI_USEFILEATTRIBUTES | SHGFI_ICON | SHGFI_SMALLICON)) { m_imageList.Add(shfi.hIcon); ::DestroyIcon(shfi.hIcon); }逻辑说明SHGFI_USEFILEATTRIBUTES表示不访问实际文件仅根据扩展名返回默认图标。SHGFI_SMALLICON取得 16x16 的图标。DestroyIcon要立刻释放因为Add时我们已经拷贝了像素数据。这个方案可以让你省去制作所有按钮位图的繁琐工作尤其是当按钮含义是“打开某种已在系统中注册的文件类型”时。6.2 运行时切换图表集并保持文字不变有些应用需要用户自己换主题比如深色模式下图标要换成高对比度的。CToolBar 切换图标集并不难难的是不要破坏已经配置好的文字。以下函数演示在运行时替换CImageListvoid CMainFrame::SetToolBarIconSet(UINT nIconResStart, int nCount) { // 创建新的图标列表 CImageList* pNewList new CImageList; pNewList-Create(16, 16, ILC_COLOR32, nCount, 1); // 从连续的资源 ID 加载图标 for (int i 0; i nCount; i) { HICON hIcon (HICON)::LoadImage(AfxGetInstanceHandle(), MAKEINTRESOURCE(nIconResStart i), IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR); pNewList-Add(hIcon); ::DestroyIcon(hIcon); } // 替换工具栏当前图标列表 CImageList* pOldList m_wndToolBar.GetImageList(); m_wndToolBar.SetImageList(pNewList); delete pOldList; // 小心确认旧列表是动态分配的不是成员变量 }逻辑说明SetImageList会更新按钮显示但不会动按钮文字和命令 ID。所以你的文字、分隔符、状态全部保留。这里要重点注意旧列表的删除如果旧列表是CImageList局部变量在Create时构造现在它还活着你直接delete就会内存错误。我习惯把工具栏的CImageList声明为CMainFrame的成员切换时把新列表指针存到成员旧列表则在新列表赋值后删除。还有一种做法是把列表指针存成std::unique_ptr并用SetImageList的返回值配合Exchange但 MFC 接口没有提供 swap容易踩坑。因此我的规矩是工具栏的图标列表要么是成员变量要么通过new创建并且用delete释放绝不传全局对象。6.3 最后的一点收获这些技巧积累到现在我最大的感受是CToolBar 的“自定义”并没有多高级但每个细节之间互相牵连。资源类型、样式位、尺寸和 DPI 四件事如果有一件没同步最后出来的工具栏就是有一种“说不出的别扭”。我通常会在界面框架初始化阶段就把这几项固定成一套统一函数以后所有工具栏都复用避免在多个窗口里重复调参。换图标、切换语言、调整高 DPI 都只改入口函数而不是到处碰按钮索引。如果你也要做类似功能建议从一套固定尺寸开始先用动态方式跑通再考虑资源方式。希望这篇内容能帮你少走我之前走过的弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站