ARTICLE DETAIL

资讯详情

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

VC++ MFC对话框添加工具栏:CToolBar实现与常见问题排查

VC++ MFC对话框添加工具栏:CToolBar实现与常见问题排查 简介一份面向Visual C开发者的对话框工具栏实现示例主要解决MFC应用程序中对话框与工具栏资源关联、消息映射与界面交互设计等问题适合正在学习VC对话框编程或需要快速集成工具栏功能的初中级开发者。压缩包共含19个文件覆盖6个头文件、5个C源文件以及图标、位图、资源脚本和项目配置等类型头文件与源文件分别承载对话框类、视图类、文档类的声明与实现rc与rc2资源文件定义工具栏布局和按钮资源dsp/dsw保存工程构建信息。整体17KB结构精简便于阅读和直接复用。已有181人学习使用内容浓缩了从创建CDialog派生类、添加工具栏资源、OnInitDialog加载关联到ON_COMMAND消息响应的完整实现链路并配有视图类与文档类框架文件可作为VC界面开发实战参考对需要在对话框上快速集成工具按钮的开发者也能提供现成的改造模板。1. 对话框加工具栏为什么默认做不到又要怎么做在 Visual C 的 MFC 工程里给对话框类程序添加工具栏几乎是每个做上位机、小工具的老开发都绕不开的活儿。Doc/View 框架的程序天生就有CMainFrame的EnableDocking和CreateToolBar一条龙服务但基于CDialog的程序没有框架窗口默认压根不给你工具栏——你打开一个对话框工程界面就是一块白板得全靠自己动手。标题里这个visual c vc给对话框添加工具栏.zip打包的正是这类示例工程在一个原生对话框上用 MFC 的CToolBar或CToolBarCtrl把工具栏嵌进去再处理按钮的消息、图标、布局和那堆让人头大的停靠问题。这篇文把从零到能跑的路径拆开讲适合要维护 VC6 老工程、或者还在用 Visual Studio 2010 之前环境做 MFC 小工具的人也适合手头有个对话框工程、想加一排常用功能按钮却被CToolBar::Create的返回值卡住的新手。先说一个反直觉的结论对话框加工具栏最稳的做法不是去模仿 Doc/View 的停靠机制而是老老实实把工具栏当成一个子窗口手动算好位置和大小再用ON_COMMAND把按钮消息接到你的对话框类里。这套路看着土但它不依赖主框架的布局逻辑在 VC6 和现代 Visual Studio 的 MFC 工程里都能跑而且翻车概率最低。2. 先想清楚用什么控件CToolBar 还是 CToolBarCtrl2.1 两种方案的选型理由与适用场景MFC 里工具栏有两层接口。CToolBar是 MFC 对工具栏的封装类和CFrameWnd配合最多但它也能作为子窗口挂到对话框上内部实际是用 Windows 通用控件TOOLBARCLASSNAME实现的也就是CToolBarCtrl的底层。CToolBarCtrl是直接包了 Win32 工具栏控件的 MFC 类更贴近 API风格和消息都是裸的适合你不想被 MFC 那层状态同步逻辑干扰的场合。我一般这么选如果只是加一排按钮、点击后弹个框或者执行一个函数用CToolBar因为它自带LoadToolBar和命令路由按钮 ID 能直接映射到对话框的消息映射里省事。如果要做下拉按钮、自定义绘制、按钮风格动态切换这类操作用CToolBarCtrl它对TB_ADDBUTTONS和TB_SETSTYLE的控制粒度更细也好查 Windows SDK 文档。标题里的示例工程两种都会涉及但核心演示一般落在CToolBar上因为对话框加工具栏的 80% 需求就是“加一排按钮点一下有反应”。还有一个关键点CToolBar挂在对话框上你不需要它停靠甚至不需要它浮动。把m_wndToolBar.Create的父窗口参数指向this风格里去掉了CBRS_GRIPPER这类边框拖拽风格它就是一个纯子窗口。这个思路决定了后面所有布局代码的写法。2.2 基于 CToolBar 的最小实现三步让工具栏出现在对话框上假设你有一个标准的 MFC 对话框工程主对话框类叫CMyDlg资源里已经切好了一个 16x16 的位图IDB_TOOLBAR1。第一步是在头文件里声明成员变量// MyDlg.h 片段 class CMyDlg : public CDialog { // ... protected: CToolBar m_wndToolBar; // 工具栏对象 afx_msg void OnToolBarBtn1(); // 按钮命令处理 DECLARE_MESSAGE_MAP() };第二步在OnInitDialog里创建工具栏并加载按钮图// MyDlg.cpp 的 OnInitDialog 中 BOOL CMyDlg::OnInitDialog() { CDialog::OnInitDialog(); // 创建工具栏子窗口ID 用 IDR_TOOLBAR1父窗口是 this if (!m_wndToolBar.Create(this, WS_CHILD | WS_VISIBLE | CBRS_TOP, IDR_TOOLBAR1)) { TRACE0(Failed to create toolbar\n); return FALSE; // 创建失败直接返回别硬跑 } // 加载位图和按钮定义按钮 ID 列表在资源里 if (!m_wndToolBar.LoadToolBar(IDR_TOOLBAR1)) { TRACE0(Failed to load toolbar bitmap\n); return FALSE; } // 工具栏要能接收命令必须把命令 ID 的范围告诉框架 m_wndToolBar.GetToolBarCtrl().SetButtonStyle( m_wndToolBar.GetToolBarCtrl().GetButtonStyle(0), TBBS_BUTTON); return TRUE; }Create 的第一个参数传this把父窗口指定为对话框WS_CHILD | WS_VISIBLE | CBRS_TOP里CBRS_TOP只是告诉布局逻辑工具栏要贴着客户区上边。注意这里没有CBRS_SIZE_DYNAMIC对话框工具栏不需要拖拽变轨。LoadToolBar会读IDR_TOOLBAR1资源——包括位图和TOOLBAR资源段里的按钮 ID 表。你不需要手动去数工具栏上有几个按钮全在资源里定义。第三步是消息映射把按钮 ID 接到处理函数// MyDlg.cpp 消息映射 BEGIN_MESSAGE_MAP(CMyDlg, CDialog) ON_WM_SYSCOMMAND() ON_WM_PAINT() ON_WM_QUERYDRAGICON() ON_COMMAND(ID_BTN_TEST, CMyDlg::OnToolBarBtn1) END_MESSAGE_MAP() void CMyDlg::OnToolBarBtn1() { AfxMessageBox(_T(工具栏按钮被点击了)); }按钮 ID 用ID_BTN_TEST定义在资源头文件里。到这一步工具栏已经能出现在对话框左上角按钮也能响应。但先别高兴太早你运行起来会发现工具栏把对话框客户区给盖住了一部分原来的控件被挤到看不见——这就是下一节要处理的布局问题。另外ON_COMMAND能生效的前提是按钮 ID 在消息映射里有对应条目而且按钮必须是TBBS_BUTTON风格不能是TBBS_SEPARATOR。3. 按钮图资源和命令消息把图标和逻辑绑到一起3.1 工具栏位图的资源格式与编辑注意事项LoadToolBar能正常工作的前提是资源文件里有一个TOOLBAR资源块而不是一个孤零零的位图。很多人第一次加工具栏只在资源里导入了一张 BMP代码里写LoadBitmap结果工具栏创建成功但按钮全是空白。原因在于 MFC 的LoadToolBar读两个东西位图 ID 和 TOOLBAR 段里的按钮映射表。TOOLBAR 段长这样IDR_TOOLBAR1 TOOLBAR 16, 15 BEGIN BUTTON ID_BTN_OPEN SEPARATOR BUTTON ID_BTN_SAVE BUTTON ID_BTN_TEST END16, 15表示每个按钮图标的尺寸是 16x15 像素这个数值必须和位图里每块小图的宽高严格对应否则按钮上的图标会发生错位。整个位图是一个横向长条里面按顺序排列每个按钮的图标顺序要和 TOOLBAR 资源段里BUTTON的声明顺序一致。位图可以用 Visual C 6.0 自带的图像编辑器画也可以从外部导入但要注意位数VC6 时代用 4 位或 8 位位图问题不大高彩色 BMP 在旧版 MFC 里反而容易出现显示异常。还有一个坑如果你的按钮需要禁用状态下的灰度图标MFC 的CToolBar会在运行时根据按钮状态自动用DrawState去处理但前提是位图本身是标准颜色深度而且按钮 ID 在消息映射中没有ON_UPDATE_COMMAND_UI处理时默认处于可用状态。如果需要禁用某个按钮得加ON_UPDATE_COMMAND_UI(ID_BTN_TEST, OnUpdateBtnTest)在里头用pCmdUI-Enable(FALSE);。3.2 非 TOOLBAR 资源段方案用 TB_ADDBUTTONS 硬造按钮有些场景下你不想动 .rc 文件——特别是维护别人留下的老工程.rc 里已经乱成一锅粥的时候。这时可以用CToolBarCtrl直接加按钮用一个 ImageList 承载图标代码完全自包含// 用 CToolBarCtrl 硬加按钮的示例 // 1. 创建工具栏控件风格加 TBSTYLE_FLAT 让按钮是平的 m_wndToolBarCtrl.Create(WS_CHILD | WS_VISIBLE | TBSTYLE_FLAT | TBSTYLE_TOOLTIPS, CRect(0, 0, 0, 0), this, AFX_IDW_TOOLBAR); // 2. 关联 ImageList图标从资源里加载 m_imgList.Create(IDB_TOOLBAR_ICONS, 16, 1, RGB(255, 0, 255)); m_wndToolBarCtrl.SetImageList(m_imgList); // 3. 定义按钮数组iBitmap 对应 ImageList 里第几张图 TBBUTTON tbb[] { { 0, ID_BTN_OPEN, TBSTATE_ENABLED, TBSTYLE_BUTTON, {0}, 0, 0 }, { 1, ID_BTN_SAVE, TBSTATE_ENABLED, TBSTYLE_BUTTON, {0}, 0, 0 }, }; m_wndToolBarCtrl.AddButtons(sizeof(tbb)/sizeof(TBBUTTON), tbb);TBBUTTON结构里idCommand字段对应点击后发送的 WM_COMMAND 通知的 wParam你的对话框消息映射照样用ON_COMMAND(ID_BTN_OPEN, ...)来接。iBitmap是图标在 ImageList 里的下标从 0 开始。fsStyle用TBSTYLE_BUTTON表示普通按压按钮如果你想做开关型按钮改成TBSTYLE_CHECK再配合TBSTATE_CHECKED就能实现按下保持。用CToolBarCtrl的好处是消息流更透明按钮点击后直接给父窗口发WM_COMMAND不需要 MFC 那套命令路由机制兜底。这意味着你把按钮 ID 定义成WM_APP 1这类自定义编号也没问题只要消息映射能对上。3.3 按钮 ID 规划的边界不要和控件 ID 冲突工具栏按钮的 ID 规划是个容易埋雷的地方。如果你把按钮 ID 定义成IDC_EDIT_NAME这种明明已被某个编辑框占用的值点击按钮时消息会派发到编辑框的父窗口处理链里ON_COMMAND可能触发编辑框的EN_CHANGE逻辑或者干脆被对话框的消息映射里的ON_COMMAND截到行为完全乱套。我见过一个工程工具栏上一个“保存”按钮 ID 和确定按钮IDOK用了一样的值结果点工具栏直接关闭了对话框。ID 规划上两个原则一是从ID_前缀的自定义宏开始避开IDC_和IDCANCEL/IDOK这些系统占位二是保持资源头文件里 ID 唯一性检查VC6 的 ResourceView 不会报重复 ID 的错这是玄学问题只能人肉盯。4. 布局与自绘让工具栏不挡住对话框控件4.1 客户区高度修正与子窗口重排工具栏创建之后默认在客户区左上角而对话框上已有的控件资源是按原来没有工具栏摆放的——创建工具栏之后客户区变小了原来贴着上边缘的静态文本框、编辑框会被工具栏盖住或者整个对话框底部溢出屏幕。处理方式有两个流派一个是把工具栏的矩形算进OnSize里调整所有子控件的位置另一个是调整对话框初始尺寸把工具栏占用的高度加进去。常见做法是第一种因为你不知道用户会拉伸窗口还是固定尺寸。最小实现是在OnInitDialog里读工具栏高度然后在OnSize里对控件SetWindowPos重新排布// 获取工具栏高度 CRect rcToolBar; m_wndToolBar.GetWindowRect(rcToolBar); m_nToolBarHeight rcToolBar.Height(); // OnSize 里把所有控件往下挪 void CMyDlg::OnSize(UINT nType, int cx, int cy) { CDialog::OnSize(nType, cx, cy); if (m_wndToolBar.GetSafeHwnd() ! NULL) { // 工具栏贴顶部 m_wndToolBar.SetWindowPos(NULL, 0, 0, cx, m_nToolBarHeight, SWP_NOZORDER | SWP_NOACTIVATE); // 假设 IDC_STATIC_INFO 是原本贴顶的控件往下偏移一个工具栏高 CWnd* pWnd GetDlgItem(IDC_STATIC_INFO); if (pWnd ! NULL) { CRect rc; pWnd-GetWindowRect(rc); ScreenToClient(rc); rc.top m_nToolBarHeight; rc.bottom m_nToolBarHeight; pWnd-MoveWindow(rc); } } }这里有个细节值得解释GetWindowRect拿到的是屏幕坐标在对话框里MoveWindow用的是客户区坐标所以必须先ScreenToClient转换一次。很多新手直接把两个坐标混用结果工具栏是好了下面控件跑到屏幕外面。4.2 用 TB_ADJUSTRECT 让尺寸自适应如果你的工具栏按钮数不固定或者将来要动态增删按钮手写高度就不够稳。CToolBarCtrl提供了一个标准的自适应接口// 让工具栏控件自己修正显示矩形 CRect rcBorders(0, 0, 0, 0); m_wndToolBar.GetToolBarCtrl().SetBorders(rcBorders); // 计算工具栏期望尺寸传入一个矩形函数按按钮数量修正宽高 CRect rcLayout(0, 0, 0, 0); LRESULT lResult m_wndToolBar.GetToolBarCtrl().SendMessage(TB_ADJUSTRECT, 0, (LPARAM)rcLayout);TB_ADJUSTRECT的语义是把“客户区矩形”转换成“窗口矩形”——你传给它的 rcLayout 是你期望按钮排布的空间它会按边上距、按钮间隔、图标宽度等参数把这个矩形扩大到实际需要的窗口尺寸。注意这是 Windows 消息返回值是BOOL语义的 LRESULT别期待它返回具体高度值。真正要拿到高度还是得GetWindowRect。不过这套逻辑配合动态加按钮省心不少每次AddButtons之后调一次TB_AUTOSIZE工具栏会自动调整自身高度不再需要你去猜像素m_wndToolBarCtrl.AutoSize();4.3 自绘按钮和扁平化风格的选择对话框工具栏默认在 VC6 下是有立体边框那种老式按钮2000 年代初看着还行现在拿到高分辨率屏幕上显得很糙。如果你接手的工程还在用 VC6 但目标机器有新版 Windows最好在OnInitDialog里强制设一下风格// 让工具栏按钮扁平鼠标悬停有高亮 m_wndToolBar.GetToolBarCtrl().SetExtendedStyle( m_wndToolBar.GetToolBarCtrl().GetExtendedStyle() | TBSTYLE_EX_DRAWDDARROWS | TBSTYLE_EX_FLAT);TBSTYLE_EX_FLAT是扁平风格的扩展样式加上之后按钮会变成类似 Office 2003 那种悬停才凸起的样式。这里注意别覆盖掉原来的扩展样式要先GetExtendedStyle()再或运算。另外扁平工具栏在对话框上通常需要WS_EX_COMPOSITED来避免背景闪烁——在对话框的OnInitDialog里用ModifyStyleEx(0, WS_EX_COMPOSITED)即可但代价是某些老的 GDI 自绘控件会失效属于一个典型的取舍问题。5. 常见问题排查创建失败、按钮无效、图标错位、布局翻车5.1 Create 返回 FALSE对话框起不来现象程序启动直接弹错TRACE输出 Failed to create toolbar或者对话框还没显示就退出了。原因CToolBar::Create失败最常见的原因有三个。第一个是IDR_TOOLBAR1这个资源 ID 在 .rc 文件里根本没有对应 TOOLBAR 资源段只导入了位图MFC 在LoadToolBar阶段找不到按钮映射表直接翻车。第二个是父窗口指针传了 NULL 或者传了非窗口指针工具栏创建时要把 WS_CHILD 挂到对话框身上。第三个是工具栏资源本身损坏位图文件被外部编辑器另存过色深或格式不对LoadToolBar加载位图失败。解决先在资源视图里确认IDR_TOOLBAR1的类型是 TOOLBAR 而不是 BITMAP展开整个资源树看类型。没有就手动在 .rc 里补一个 TOOLBAR 段参考前面的模板。如果用的是CToolBarCtrl检查Create传的第一个参数——样式里必须有WS_CHILD父窗口句柄不能是 NULL。最后把位图重新导出一份 256 色 BMP再导入资源能解决大部分格式问题。5.2 工具栏显示了但按钮点了没反应现象按钮能看见、能按下鼠标点下去也有视觉反馈但你的处理函数根本没执行。原因MFC 的消息路由机制里ON_COMMAND要生效取决于按钮的idCommand是否在对话框类的消息映射里。有两个特例最容易踩。一个是按钮 ID 用的是IDOK或IDCANCEL点击会走系统按钮逻辑消息被框架截获虚拟键 Enter/Esc 也可能触发你的处理函数收到的是IDOK而不是自定义 ID。另一个是CToolBar的LoadToolBar必须把按钮映射表加载出来如果按钮 ID 在 TOOLBAR 资源段里写成了IDC_XXX而IDC_XXX定义在resource.h里且和某个控件共享了整数常量消息路由会匹配到控件通知路径上出现“按一下跑了两个处理函数”的灵异现象。解决检查按钮 ID 是否以ID_开头且全局唯一。在OnToolBarBtn1里加TRACE或断点看函数到底进没进。确认资源里的按钮映射表和TBBUTTON数组里用的 ID 完全一致。如果按钮是TBBS_SEPARATOR它不会产生命令消息这也是一种“按钮点了没反应”。5.3 图标错位第二个按钮显示的是第三个钮的图现象工具栏上有 5 个按钮每个按钮都有图标但第 2 个按钮显示的是第 4 个的图第 3 个正常整个看图顺序全乱了。原因LoadToolBar加载位图时按位图宽度除以按钮数来切图。如果位图每个小图的宽度不是 16 像素或者按钮定义数量与位图里实际图标数不一致切分就会错位。更隐蔽的情况是位图末尾有多余的透明像素列导致最后一个按钮的图被切掉一截。解决用图像编辑器打开位图查看属性里的宽度确认等于按钮数乘以 16。TOOLBAR 资源段里写的是BUTTON还是SEPARATOR每个BUTTON都要有位图对应SEPARATOR不需要。如果你在中间某个位置插入了SEPARATOR那么位图里这个间隔位置不能有图否则后面所有按钮位置全得往后错一位。最稳的做法是把位图里的图标顺序和 TOOLBAR 段的按钮顺序逐一对照缺一补一多了删掉。5.4 对话框变宽了但工具栏还是老尺寸按钮挤成一团现象窗口拉伸后工具栏只占左上角一小块按钮全挤在一起右边空一大片。原因CWnd::SetWindowPos或者MoveWindow没有在OnSize里被重新调用工具栏窗口尺寸固定为初始值不会自动跟随父窗口宽度变化。解决在OnSize里加工具栏的SetWindowPos用新的cx宽度刷新工具栏宽度。工具栏本身没有停靠吸附逻辑你写了它才动。如果是CToolBarCtrl还要考虑按钮本身宽度是否被固定普通按钮不会自动拉开间隙想做出 Office 那种右对齐的效果需要自己把按钮摆到cx - 按钮总宽的位置。5.5 在高 DPI 屏幕上工具栏图标糊成一片现象程序移到 4K 高分屏上工具栏按钮图标变小、发虚文字标签糊成一团。原因VC6 时代的 MFC 是不感知 DPI 的GetWindowRect拿到的坐标是按系统 DPI 缩放过的但工具栏按钮图标位图是像素固定尺寸系统缩放后插值拉大自然发虚。TB_ADJUSTRECT在这里帮不上忙。解决在OnInitDialog里先声明进程 DPI 感知——这需要在stdafx.h里包含shellapi.h然后调用SetProcessDPIAware()或者更现代的方式是用 manifest 里的 PerMonitorV2。代价是老工程里所有基于坐标的布局代码都要重新审视一遍风险不小。过渡方案是接受缩放把工具栏位图换成更大尺寸的 32x32用SetMaxTextRows和按钮尺寸设置来容纳更大图标系统缩放时的观感会好不少但治标不治本。5.6 工具栏按钮和对话框的 Tab 键焦点的关系现象按下 Tab 键焦点会跳到工具栏按钮上但按钮不响应空格或者焦点跑到屏幕外。原因工具栏按钮默认在WS_TABSTOP样式的控制范围内CToolBar创建时带了CBRS_风格这个风格本身会让 MFC 把工具栏加进 tab 键遍历序列。对话框上如果有编辑框、列表控件Tab 键顺序是按控件创建顺序来的工具栏创建时机在OnInitDialog里比较早可能排在编辑框之前导致 Tab 遍历顺序怪怪的。解决给工具栏去掉WS_TABSTOP直接用ModifyStyle(WS_TABSTOP, 0)。如果还想保留键盘操作在对话框的OnKeyDown里做方向键响应按钮移动但那属于自定义交互了老工程一般不值得折腾。6. 进阶把工具栏做成可复用的对话框基类做到这里你已经能在对话框上挂一排能点击的工具栏了。但每个新对话框都要复制一遍OnInitDialog里的创建代码、OnSize里的重排代码老项目里三五个对话框就要改三五个地方烦得要命。更合理的方式是把工具栏封装进一个基类比如叫CToolbarDialog让所有带工具栏的对话框继承它。基类里做四件事声明m_wndToolBar和m_nToolBarHeightOnInitDialog里统一创建工具栏OnSize里统一重排纯虚函数OnToolBarCommand(UINT nID)让子类处理自定义命令。这样新对话框只需要继承这个基类指定工具栏资源 ID然后覆写命令处理函数即可。消息映射有一个细节基类自己处理ON_COMMAND的话范围会截断子类的消息路由所以更稳的做法是把按钮消息全部定义到一个连续范围内用ON_COMMAND_RANGE消息映射宏在基类里统一接到一个处理函数再分发到虚函数// 基类里集中处理 0x8000 到 0x8100 范围的工具栏按钮 ON_COMMAND_RANGE(ID_TBBTN_FIRST, ID_TBBTN_LAST, CToolbarDialog::OnToolBarCommand) void CToolbarDialog::OnToolBarCommand(UINT nID) { // 转发给虚函数子类覆写 OnToolBarButton(nID); }ON_COMMAND_RANGE是 MFC 里被很多人忽略的宏它在处理连续 ID 段时比逐个ON_COMMAND干净得多。但注意ID_TBBTN_FIRST和ID_TBBTN_LAST必须是一个连续递增的整数段中间不能穿插别的 ID否则会把别人的消息也截进来。为了避免和对话框的其他控件 ID 冲突把工具按钮 ID 段定义在一个偏移量之后比如ID_TBBTN_FIRST 0x8000这个值远大于IDC_控件通常的 1000 以内范围也避开WM_APP和系统消息区间。实测这个分段方法在 VC6 到 VS2019 的 MFC 工程里都没出过岔子。最后一个建议工具栏资源和位图的命名不要随手写IDR_TOOLBAR1这种时间长了根本分不清是哪个对话框的。按对话框语义起名比如IDR_SEARCH_TOOLBAR、IDR_EDIT_TOOLBAR。老工程最怕的不是不会写代码而是三个月后打开自己写的IDR_TOOLBAR1不知道当初加了哪几个按钮。这个坑我踩过好多次现在写对话框工具栏的第一件事就是先把资源 ID 起好名字。希望帮到你。本文还有配套的精品资源点击获取
返回列表