ARTICLE DETAIL

资讯详情

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

MFC工具栏自绘实战:CToolBarEx原理、接线与避坑指南

MFC工具栏自绘实战:CToolBarEx原理、接线与避坑指南 简介这份压缩包提供了CToolBarEx在Visual C中的完整示例程序面向使用MFC开发Windows应用的C开发者尤其适合需要增强工具栏交互效果的界面设计场景。包内共30个文件约43KB以h头文件和cpp源文件为核心配合ico图标、bmp位图、rc资源脚本及dsp/dsw工程文件构成可直接编译学习的微型MFC项目。已有93人学习下载。资源重点展示了扩展工具栏的关键能力通过AddButton定制多状态按钮图像、关联下拉动态菜单、同时显示图标与文字以及使用SetButtonInfo管理禁用、隐藏、分组等按钮状态。示例代码还包含消息映射与OnToolBarBtnClick重载的具体写法便于读者直接移植到实际项目中。压缩包目录结构清晰Debug目录外均布源代码与资源文件适合作为CToolBarEx入门到进阶的参考模板。1. TB.rar 里的 CToolBarExMFC 工具栏自绘到底能省多少事拿到TB.rar_CToolBarEx_visual c这类压缩包只要你做过几年 Visual C 的 MFC 维护一眼就能判断出里面装的是什么一份靠 CToolBarEx 类实现的自绘工具栏示例工程外加按钮位图资源。CToolBarEx 本质上是把 MFC 原生 CToolBar 的绘制逻辑接管过来让工具栏从“灰底凸边、按钮生硬”变成“扁平底色、图标透明、悬停高亮”的现代样子。它解决的是老工程里最常见的界面痛点MFC 默认工具栏能停靠、能弹 tooltip、能响应命令但外观很难看Ribbon 又太重换框架成本太高。适合两类人一是被产品经理逼着“把界面做得现代一点”的 MFC 老人二是刚接手老项目、发现工具栏按钮行为诡异的新手。如果你也在这条船上这篇就把机制、接线、绘图和排障一次讲完。2. CToolBarEx 要解决什么先看清 CToolBar 的边界再谈自绘2.1 原生 CToolBar 的按钮状态能力其实不少丑才是原罪很多刚接触 MFC 的人会以为工具栏按钮只是“一张图加一个 ID”其实CToolBar内部是一台标准 Windows 工具栏控件的封装TBBUTTON结构里装了命令 ID、图标索引、状态和样式。它能做到的交互不少按下时的凹陷、禁用时的灰化、TBBS_CHECKED的选中态、TBBS_INDETERMINATE的三态以及鼠标悬停时的透明白色高光这些全是系统自带的行为。问题在于系统画出来的“经典样式”太旧了。按钮的立体边是 Windows 通用控件用 3D 色画死的整条工具栏背景默认是灰色渐变按钮间距、边框粗细都不能细调。你现在拿一个 1920 分辨率的屏幕看它观感上就像一套 2005 年的收银软件。CToolBarEx 的核心思路就是用自绘代码替掉系统默认的绘制路径同时保留命令路由、停靠、tooltip 这些 MFC 现成的骨架。2.2 自绘的三条路线DrawButton、OnPaint 与 owner-draw想让按钮变好看理论上可以走三条路。第一条是重写CToolBar::DrawButton它是虚函数MFC 内部在逐个画按钮时会调用它适合只调整按钮样式的场景第二条是彻底重写OnPaint自己遍历按钮、自己算矩形、自己画一切灵活性最高但工作量大第三条是给按钮走BS_OWNERDRAW路线让每个按钮像自绘控件一样发WM_DRAWITEM通知这条在工具栏上很少用因为工具栏按钮不是真正的子控件消息反射链路不直观网上流传的 CToolBarEx 也极少采用它。我见过的大多数 CToolBarEx 示例走的是前两条的混合重写OnEraseBkgnd填背景重写DrawButton画单个按钮。比如这样// 截取网上常见 CToolBarEx 示例包的 DrawButton 重写思路 void CToolBarEx::DrawButton(CDC* pDC, int x, int y, UINT nState, int nIndex) { CRect rcBtn(x, y, x m_sizeButton.cx, y m_sizeButton.cy); // nState 里带的是按下、禁用、选中这些位标志 BOOL bPressed (nState TBBS_PRESSED); BOOL bDisabled (nState TBBS_DISABLED); BOOL bChecked (nState TBBS_CHECKED); // 先画按钮背景顺序很重要禁用态优先其次悬停、按下、选中 if (bDisabled) pDC-FillSolidRect(rcBtn, m_clrDisabledBg); else if (bPressed || bChecked) pDC-FillSolidRect(rcBtn, m_clrPressedBg); else if (nIndex m_iHotIndex) pDC-FillSolidRect(rcBtn, m_clrHoverBg); else pDC-FillSolidRect(rcBtn, m_clrBack); // 再画图标这个函数在示例包里通常是按 nIndex 从位图里抠图 DrawButtonBitmap(pDC, rcBtn, nIndex); }这里最需要读懂的是nState。它不是一个简单的布尔而是把TBBS_PRESSED、TBBS_DISABLED、TBBS_CHECKED等标志位打包在一起绘制时必须先判断禁用再判断悬停否则禁用按钮悬停时也会出现高亮背景交互上就是错的。nIndex是按钮在位图里的索引不是命令 ID网上不少版本在这里直接把nIndex当command ID用结果三个按钮时只能显示前三个图标翻车的帖子一问一个准。2.3 选型什么场景值得上 CToolBarEx什么场景别碰不是所有 MFC 工程都需要动工具栏。如果界面上只有五六个按钮、功能万年不变、用户也不在乎外观原生CToolBar完全够用强行自绘只会给自己增加维护负担。值得用 CToolBarEx 的场景主要是这几个场景原生 CToolBarCToolBarEx按钮扁平化、去立体边做不到系统画死完全可控悬停高亮反馈只有系统默认的那一点自定义底色和边框工具栏背景透明 / 渐变做不到自绘背景随便画按钮间距、尺寸微调局限受系统布局影响每个按钮尺寸可指定工程里已有大量命令消息兼容完全兼容不用动命令路由想彻底换 Ribbon不用换不用换但外观上限比 Ribbon 低一句话如果你只是想让老 MFC 工程看起来体面一点且不想动整个界面框架CToolBarEx 是一条性价比很高的路。如果你期望它达到 Office 那种 Ribbon 的动效和自动布局它做不到别在这棵树上吊死。3. 把 CToolBarEx 接进你的 MDI 主框架最小改造流程3.1 拿到 TB.rar 之后先做什么确认类文件与工程配置解压这类包常见的做法是用 WinRAR 或命令行工具直接解开# Windows 下最省事的方式其实是用 WinRAR命令行则这样解 unrar x TB.rar解开后先别急着把一堆文件拖进工程。按下 Ctrl 键点选先看里面有哪些类型的文件.h和.cpp是 CToolBarEx 类的实现.bmp或.ico是按钮图标资源另外大概率有一个演示工程文件可能是.dsw、.dsp老 VC6 格式也可能是.sln。这一步很重要的原因是老示例的工程配置大概率和你当前的 Visual Studio 版本对不上直接打开往往编译一脸懵但类文件本身是跨版本可用的。正确姿势是把CToolBarEx.h和CToolBarEx.cpp拷进自己工程的源码目录然后重新添加文件而不是迁就压缩包里的工程文件。老工程里还要注意字符集。如果 CToolBarEx 的源码里有char*和TCHAR*混用你的工程若是 Unicode可能会有C2664这类编译错通常把_T()宏补齐就解决了。3.2 替换原生工具栏OnCreate 里的四步接线在 MFC 单文档或多文档程序里主窗口工具栏的创建位置在CMainFrame::OnCreate。把原来的m_wndToolBar成员变量类型从CToolBar改成CToolBarEx然后按下面的顺序接上// MainFrm.h 里改成员声明 CToolBarEx m_wndToolBar; // MainFrm.cpp 的 OnCreate 里原来是 CToolBar 的创建代码整体替换 int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CFrameWnd::OnCreate(lpCreateStruct) -1) return -1; // 第一步创建工具栏窗口 if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, // 扁平样式自绘代码大多依赖这个 WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC)) { TRACE0(CToolBarEx 创建失败\n); return -1; } // 第二步加载按钮位图和按钮数组 if (!m_wndToolBar.LoadToolBar(IDR_MAINFRAME)) { TRACE0(加载工具栏资源失败\n); return -1; } // 第三步允许停靠并实际停靠到顶部 m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY); DockControlBar(m_wndToolBar); return 0; }这段代码最需要理解的是CreateEx的两个样式参数。第一组TBSTYLE_FLAT是工具栏控件样式它让按钮在没有悬停时没有立体边框自绘代码通常依赖它先压掉系统默认的凸起第二组CBRS_*是 MFC 控制条样式CBRS_TOP控制初始停靠位置CBRS_GRIPPER决定是否显示最左边的可拖动把手CBRS_TOOLTIPS和CBRS_FLYBY分别管 tooltip 弹出和悬停提示。CBRS_SIZE_DYNAMIC必须加否则工具栏浮动后不能按内容自动伸缩。很多新手会问为什么这里没有ShowWindow原因在于DockControlBar会自动调整工具栏可见状态手动调用ShowWindow反而可能在停靠布局尚未完成时造成闪一下。3.3 把 TB.rar 的位图资源搬进自己的工程CToolBarEx 的按钮图片通常是一张整图里面按顺序排着一个个按钮图标LoadToolBar时 MFC 会根据按钮数组中每个按钮的iBitmap索引从这张大图里切出对应的小图。示例包自带的工具栏资源在自己工程里不一定有所以要把位图导入到自己的资源文件里。操作路径是在资源视图里右键.rc文件 - “导入”选中那张.bmp资源类型选BITMAP把资源 ID 改成你自己工程的 ID比如IDR_TOOLBAR1。完毕后再改加载代码if (!m_wndToolBar.LoadToolBar(IDR_TOOLBAR1)) // 不再是 IDR_MAINFRAME { TRACE0(加载自绘工具栏资源失败\n); return -1; } // 如果位图和按钮不是一一对应需要显式指定按钮顺序 static const UINT uButtons[] { ID_FILE_NEW, ID_FILE_OPEN, ID_FILE_SAVE, ID_SEPARATOR, ID_EDIT_CUT, ID_EDIT_COPY, ID_EDIT_PASTE, ID_SEPARATOR, ID_APP_ABOUT }; m_wndToolBar.SetButtons(uButtons, sizeof(uButtons) / sizeof(UINT));注意SetButtons会把你传入的命令 ID 数组按顺序映射到按钮上数组里的ID_SEPARATOR不是按钮是分隔条它在自绘代码里要跳过否则绘制逻辑会把分隔条当普通按钮框出来显示成一小块灰底非常难看。位图的顺序靠按钮结构体里的iBitmap字段决定如果你发现第三个按钮显示成第一张图多半是位图索引没对上这在后面避坑章节会再展开。4. 自绘是重头戏把扁平、透明、悬停高亮一次画对4.1 接管绘制入口先处理背景闪烁再画每个按钮CToolBarEx 的绘制起点在OnEraseBkgnd和OnPaint。OnPaint是最终绘制的入口但如果不处理OnEraseBkgnd窗口在重绘时会先擦除背景再画内容一擦一画之间就会出现闪烁。最常见的做法是让OnEraseBkgnd直接填充背景色并返回 TRUE告诉系统“我已经擦过了”然后所有绘制逻辑集中在 OnPaint 里BOOL CToolBarEx::OnEraseBkgnd(CDC* pDC) { CRect rcClient; GetClientRect(rcClient); pDC-FillSolidRect(rcClient, m_clrBack); // 整个工具栏先填同一底色 return TRUE; } void CToolBarEx::OnPaint() { CPaintDC dc(this); CRect rcClient; GetClientRect(rcClient); dc.FillSolidRect(rcClient, m_clrBack); // 遍历所有按钮逐个绘制 for (int i 0; i GetCount(); i) { UINT nStyle GetButtonStyle(i); if (nStyle TBBS_SEPARATOR) continue; // 分隔条不当作按钮画 CRect rcBtn; GetItemRect(i, rcBtn); DrawOneButton(dc, i, rcBtn, nStyle); } }GetCount()返回按钮和分隔条的总数GetItemRect(i)取的是工具栏内部每个条目的矩形这和你手算坐标相比最大的好处是它能跟随停靠和浮动状态自动变化。绘制顺序上要先画整个背景、再画按钮否则按钮周围会残留背景色块。DrawOneButton是自绘函数里面负责把前面DrawButton的思路落实成实际画法填充按钮底色、画边框、贴图标、按状态覆盖半透明色。4.2 悬停高亮要用“局部刷新”不要整条 Invalidate自绘工具栏最容易犯的错是鼠标移动时对整条工具栏调用Invalidate()。工具栏按钮通常有二十多个每次整条重绘不仅费 CPU还会在文字按钮、图标交错时出现高亮同时亮一片的残影。正确做法是跟踪“当前悬停按钮索引”只在旧索引和新索引对应的矩形上做局部刷新void CToolBarEx::OnMouseMove(UINT nFlags, CPoint point) { int iNewHot -1; // 命中测试找到鼠标所在的按钮索引 for (int i 0; i GetCount(); i) { if (GetButtonStyle(i) TBBS_SEPARATOR) continue; CRect rcBtn; GetItemRect(i, rcBtn); if (rcBtn.PtInRect(point)) { iNewHot i; break; } } if (iNewHot ! m_iHotIndex) { // 旧按钮区域失效重画新按钮区域失效重画 if (m_iHotIndex 0) InvalidateButton(m_iHotIndex); m_iHotIndex iNewHot; if (m_iHotIndex 0) InvalidateButton(m_iHotIndex); } CToolBar::OnMouseMove(nFlags, point); } void CToolBarEx::InvalidateButton(int nIndex) { CRect rcBtn; GetItemRect(nIndex, rcBtn); InvalidateRect(rcBtn, FALSE); // FALSE 表示不擦除背景由 OnPaint 直接覆盖 }InvalidateRect的最后一个参数FALSE是这里不留闪烁的关键。它告诉系统重绘时不要先擦除这块区域而是直接调用OnPaint把新状态画上去。如果不加这个参数局部刷新同样会闪。按钮索引和命令 ID 在这里又容易混淆OnMouseMove里拿到的i是数组索引绘制函数里按索引查位图没问题但如果要拿它去查命令 ID必须用GetItemID(i)转换别直接用i当消息 ID 发。4.3 图标透明化位图底色别硬抠绘制时处理老示例包里最常见的位图是 256 色 BMP背景是一块纯色通常是灰色或洋红色。这类图直接贴上去会带一个难看的底色块。自绘代码里处理它的标准做法是加载位图后创建一个掩码绘制时先BitBlt掩码再BitBlt图标用 SRCPAINT 和 SRCAND 实现透明合成BOOL CToolBarEx::DrawTransparentBitmap(CDC* pDC, int x, int y, CBitmap* pBitmap, COLORREF crTransparentColor) { BITMAP bm; pBitmap-GetBitmap(bm); int nWidth bm.bmWidth; int nHeight bm.bmHeight; // 内存画布分别存放原图、单色掩码和合成结果 CDC memDC, maskDC; memDC.CreateCompatibleDC(pDC); maskDC.CreateCompatibleDC(pDC); CBitmap bmpMem, bmpMask, bmpResult; bmpMem.CreateCompatibleBitmap(pDC, nWidth, nHeight); bmpMask.CreateBitmap(nWidth, nHeight, 1, 1, NULL); // 第一步把原图复制到内存画布 CBitmap* pOldMem memDC.SelectObject(bmpMem); memDC.SelectObject(pBitmap); memDC.BitBlt(0, 0, nWidth, nHeight, memDC, 0, 0, SRCCOPY); // 第二步根据透明色生成掩码 CBitmap* pOldMask maskDC.SelectObject(bmpMask); memDC.SetBkColor(crTransparentColor); maskDC.BitBlt(0, 0, nWidth, nHeight, memDC, 0, 0, SRCCOPY); // 第三步先显式背景后显示 掩码消底 图标叠加 pDC-BitBlt(x, y, nWidth, nHeight, maskDC, 0, 0, SRCAND); pDC-BitBlt(x, y, nWidth, nHeight, memDC, 0, 0, SRCPAINT); memDC.SelectObject(pOldMem); maskDC.SelectObject(pOldMask); return TRUE; }这段代码三个步骤要一次看完第一步生成原图第二步用SetBkColor把透明色变成单色掩码第三步把掩码当作图案先“挖洞”再叠上图标。SRCAND负责把目标区域中和掩码黑色部分对应的像素保留SRCPAINT再把图标画上去。这里最易翻车的地方在于SetBkColor设置的颜色必须和 BMP 里的背景色完全一致差一个 RGB 值都会导致边缘出现色斑。老示例包里的位图大多用RGB(192, 192, 192)或RGB(255, 0, 255)拿不准就在画图工具里用“取色器”吸一下边角颜色。5. 避坑CToolBarEx 的编译、布局和刷新排查记录5.1 现象把 TB.rar 里的类文件加入工程编译报一堆 C2065 标识符未定义原因基本是资源 ID 冲突或预编译头顺序不对。老示例的resource.h和你工程里的resource.h定义了相同的IDR_TOOLBAR两个头文件互相覆盖或者 CToolBarEx.cpp 里用了ID_*但该文件没有包含你自己的resource.h编译时找不到符号。解决把 CToolBarEx 类文件里所有用到的资源 ID 改成你工程里真实存在的 ID并确认每个.cpp顶部都#include stdafx.h。如果你的工程不用预编译头就在项目属性里把“预编译头”设为“不使用”二选一不能两边都留着。这个报错是最常见的从老包移植失败主因九成不是类写错是环境没归置好。5.2 现象按钮全都一个样或者第一张图反复出现图标和按钮对不上原因十有八九是SetButtons传入的按钮数组顺序和位图索引不一致。工具栏按钮显示哪张图不是由按钮 ID 决定的而是由每个按钮的iBitmap字段决定当你用SetButtons填充数组时iBitmap会按数组下标自动赋成 0、1、2、3…… 如果位图上的图标顺序和按钮命令顺序不一致画面自然全错。解决先拿位图编辑器看一眼大图里每个图标的排列比如第一个图标是“新建”,第二个是“打开”那么按钮数组的顺序也必须把ID_FILE_NEW放在第一位、ID_FILE_OPEN放在第二位。别指望靠SetButtonInfo事后改iBitmap那只适合个别微调。这里还有一个隐藏坑MFC 的LoadToolBar其实会自动按位图顺序匹配如果用LoadToolBar加载后按钮顺序错优先查资源里的按钮信息表有没有定义错。5.3 现象鼠标悬停时高亮闪烁或者移动窗口时留下一排按钮的残影闪烁的根源是重绘时先擦了背景再画按钮OnEraseBkgnd和OnPaint里各画了一遍底色一擦一画之间形成了两帧画面。残影则相反是OnEraseBkgnd直接返回 TRUE 但OnPaint没有覆盖到所有区域比如按钮之间的缝隙没画到旧的像素残留在窗口表面。解决把底色的填充统一放进OnPaintOnEraseBkgnd里只做一次FillSolidRect并返回 TRUE两处用的颜色必须一致。另外在WM_WINDOWPOSCHANGED消息里补一次Invalidate()确保窗口移动后整个客户区强制刷新。闪烁这件事靠调参数是调不好的根子都在“擦除”和“绘制”这两个阶段重复劳动。5.4 现象浮动工具栏关掉再显示按钮顺序没变但宽度全乱了原因是工具栏浮动时MFC 会根据内容计算窗口宽度而 CToolBarEx 自绘代码如果接管了按钮尺寸计算却没有在浮动和停靠两个状态下给出不同结果就会出现宽度错乱。停靠时工具栏宽度由按钮行数和每个按钮宽度决定浮动时由窗口宽度决定两套逻辑在CalcFixedLayout和CalcDynamicLayout里必须分别处理。解决在CalcDynamicLayout里nMode为LM_HORZ且dwFlags LM_COMMIT时按浮动状态重新设定每行按钮数停靠状态则调用基类的CToolBar::CalcFixedLayout。别在OnSize里硬算按钮矩形停靠布局在OnSize之后还有一次调整硬算的结果会被覆盖。这条是布局问题不是绘制问题排查时先断点看CalcDynamicLayout传入的参数。5.5 现象tooltip 正常但按钮禁用时图标不变灰看起来仍然可点这是自绘按钮最常见的逻辑漏项系统自带的灰化效果已经被你的自绘代码完全替代但你的DrawOneButton里只处理了背景色没有对图标做灰度处理。按钮仍是禁用状态鼠标点击没反应但视觉上完全不像禁用。解决在绘制图标前判断TBBS_DISABLED对位图做灰度变换。简单做法是先把图标画到内存画布用GetPixel/SetPixel遍历每个像素把 RGB 按gray (r * 299 g * 587 b * 114) / 1000算成灰度后写回更高效的做法是生成一张灰度掩码位图绘制时直接替换。这里没有魔法就是多一步处理但很多人漏掉导致用户以为按钮坏了点到手软。6. 进阶把 CToolBarEx 从“示例代码”变成“可用组件”6.1 让按钮灰化和选中态跟着命令状态自动走自绘之后ON_UPDATE_COMMAND_UI消息仍然有效这是 MFC 工具栏最好的部分。你不需要自己在自绘代码里维护“现在哪个按钮应该禁用”而是在每个命令的UPDATE_COMMAND_UI处理函数里调用pCmdUI-Enable(...)和pCmdUI-SetCheck(...)void CMainFrame::OnUpdateFileSave(CCmdUI* pCmdUI) { // 文档没修改过就禁用“保存”按钮 pCmdUI-Enable(m_bDocDirty); } void CMainFrame::OnUpdateViewToolbar(CCmdUI* pCmdUI) { // 让“显示工具栏”菜单项和工具栏按钮保持勾选同步 pCmdUI-SetCheck(m_bShowToolbar); }MFC 会在空闲时间自动调用这些处理函数把Enable和SetCheck的结果同步到按钮状态位。CToolBarEx 自绘时只要读GetButtonStyle(i)检查TBBS_DISABLED和TBBS_CHECKED就能得到和原生工具栏一致的逻辑状态。把这个机制用好自绘类里就不用管理任何业务状态类保持纯粹后续维护成本大幅下降。6.2 验证工具栏是否“画对了”的三步检查法每改一次自绘代码我都固定走三步验证。第一步停靠状态下逐按钮悬停确认高亮切换无闪烁、无残影第二步拖出工具栏变成浮动窗口再拖回原位置确认按钮宽度、行数、图标位置都恢复原样第三步同时按下多个按钮再按右键关闭工具栏后从“视图”菜单重新打开确认窗口状态被正确还原。最后用 Spy 看工具栏窗口类名和子窗口列表确认没有多出奇怪的控件。如果你把这套验证沉淀成一份清单每次改完走一遍比事后接到用户投诉再回查值得多。我最初把 TB.rar 里的 CToolBarEx 接进工程时为了省事直接整个工具栏Invalidate()结果每次鼠标移过工具栏都闪成雪花屏当时还以为是电脑配置不行。后来静下来画了一下午状态图才明白闪烁不是玄学是擦除和绘制两段逻辑在打架。从那以后凡是涉及自绘控件我都会先把“哪些区域会失效、哪些像素需要重画”写在纸上再动手。希望这篇的细节能帮你少走这段弯路。本文还有配套的精品资源点击获取
返回列表