简介:面向Visual C++开发者,这份资源演示了如何在MFC对话框程序中加入并管理工具栏,帮助解决界面交互增强与按钮消息响应问题。压缩包共19个文件,以.h头文件、.cpp源文件为主,包含对话框类、视图/文档类及主框架实现,另有.rc/.rc2资源脚本、.bmp位图与.ico图标,以及.dsp/.dsw/.clw等工程与类向导文件,结构完整,便于对照学习。目前已有180人学习,适合初学MFC或需要在对话框项目中快速集成工具栏的编程人员。通过示例工程,可掌握创建CDialog派生类、编辑工具栏资源、在OnInitDialog中加载控件并为按钮添加消息映射的关键步骤,也能了解MFC应用中文档视图结构与预编译头文件的使用,是一份可直接运行、辅助排错的实用参考。
1. 在 VC 里给对话框添加工具栏,真正的成本在布局不在创建
很多 Visual C++ 的 MFC 工程第一次给对话框添加工具栏时,都会撞上一面墙:明明按标准方式用CToolBar创建了工具栏,按钮位图也加载成功,但工具栏不是停在窗口顶部,就是位置奇怪,或者被后面的控件盖住。原因在于CDialog和CFrameWnd对子窗口布局的责任不一样,对话框没有“控制栏”的概念。主框架窗口里RecalcLayout会替你把工具栏摆好,而对话框模板只负责它自己的控件,工具栏创建完之后没有任何机制来接管它的坐标。
下面从设计差异讲起,先把“为什么”说透,再给一条经过验证的实现路径。中间会覆盖资源准备、OnInitDialog初始化、WM_SIZE重排、按钮样式和消息路由,最后给一个可以放进工程的通用函数。新手能够直接照着敲,老手可以重点看CalcFixedLayout的使用、按钮 ID 冲突和资源类型这些容易踩的边界。
2. 对话框工具栏的设计差异:CDialog 没有 CFrameWnd 的布局引擎
先分清一个概念:不是“对话框加不了工具栏”,而是“工具栏加进对话框后,没有任何机制帮你把它摆放好”。主窗口里CToolBar的状态由CFrameWnd、CDockBar和RecalcLayout三方协作维护;对话框是CDialog,继承链到CWnd就结束了,没有对接CControlBar停靠布局的那套代码。所以 MFC 的EnableDocking、DockControlBar在对话框里几乎不产生效果,它们要找的 DockBar 根本不存在。
一句话概括定位:在对话框上,工具栏只是一个普通的子窗口,父窗口就是对话框本身。视觉上它可以和主窗口工具栏长得一样,但之后的位置变化全部要由对话框窗口过程或WM_SIZE处理函数驱动,自动停靠换成了手工MoveWindow。
2.1 CToolBar 在对话框里创建成功的必要条件
CToolBar在创建时不带自己的父窗口,CreateEx的第一个参数决定了它挂到谁下面;父窗口必须是对话框的 HWND,工具栏才能参与对话框的消息分发和子窗口裁剪。典型的最小创建代码:
// 在 OnInitDialog 中 if (!m_wndToolBar.CreateEx(this, // 父窗口 = 对话框 TBSTYLE_FLAT | TBSTYLE_TOOLTIPS, // 子窗口扩展样式 WS_CHILD | WS_VISIBLE | CBRS_TOP) || // 常规窗口样式 !m_wndToolBar.LoadToolBar(IDR_DLG_TOOLBAR)) // 工具栏资源 { TRACE(_T("对话框工具栏创建失败\n")); // 对工具类面板,继续显示对话框通常比直接退出更稳妥 }这里CreateEx的三组参数容易混:第二个参数是给 Windows 通用控件用的TBSTYLE_扩展样式;第三个参数是 CWnd 建窗时的窗口风格,CBRS_TOP只是 MFC 的方位提示。对话框不会自动把CBRS_TOP落实成“顶部贴边”,所以创建完成后,必须紧接着用MoveWindow给它一个矩形。很多初学者以为CBRS_TOP会触发自动布局,这是第一个常见误区。
2.2 三条实现路线的取舍:CToolBar、CDialogBar、CreateToolbarEx
针对“Visual C++ 给对话框添加工具栏”这个需求,工程里有三种常见做法。放在同一张表里比较,再讲选型会更直观:
| 方案 | 资源准备 | 布局方式 | 命令响应 | 适用场景 |
|---|---|---|---|---|
| CToolBar + MoveWindow | .rc 中加 Toolbar 资源 | 手工在 WM_SIZE、OnInitDialog 里重排 | WM_COMMAND 自动发给父对话框 | 大多数对话框工具栏 |
| CDialogBar 内嵌 | 对话框模板占位 | 由对话框模板定义像素位置 | 同样发到父窗口 | 工具栏内还要嵌别的控件 |
| CreateToolbarEx 纯 API | 位图 + TBBUTTON 数组 | 完全手工 | 需自己转发 WM_COMMAND | 不想引入 MFC 控制栏时 |
CDialogBar本质是一个“控制栏容器”,它不提供按钮位图编辑,一般用于把一组控件放进框架的停靠区。如果强行让对话框承载CDialogBar,仍然要自己管理尺寸,并不比CToolBar省事。所以绝大多数老旧 VC 项目里,最常用的做法是第一种:CToolBar作为对话框成员变量,初始化时创建并手工摆放。
假如想完全不依赖 MFC 资源加载,还可以退到 Win32 层的CreateToolbarEx。推荐在维护纯 C 工程、或者想避开 .rc 工具栏编辑器时采用。TBBUTTON 数组的定义方式:
TBBUTTON ab[] = { { 0, ID_TOOL_OPEN, TBSTATE_ENABLED, TBSTYLE_BUTTON, {0}, 0, 0 }, { 1, ID_TOOL_SAVE, TBSTATE_ENABLED, TBSTYLE_BUTTON, {0}, 0, 0 }, }; HWND hToolbar = ::CreateToolbarEx(hWndDlg, WS_CHILD | WS_VISIBLE | TBSTYLE_FLAT | TBSTYLE_TOOLTIPS, IDC_TOOLBAR, 2, hInst, IDB_TOOLBAR, ab, 2, 16, 16, 16, 16, sizeof(TBBUTTON));TBBUTTON里的第一个字段是位图索引,不是按钮 ID;第二个字段才是发给父窗口的WM_COMMAND命令 ID。资源和位图宽高通过后面四个参数传递,若混写会出现编译通过但显示错位的现象。这个方案的一个隐藏成本是,按钮状态、提示文本、图标更新都要自己调用SendMessage来维护,代码量并不比 MFC 方案少。
2.3 选型结论:老项目偏好 CToolBar,新项目可以先做封装
CToolBar的封装把LoadToolBar、SetBarStyle、提示文本这些低频操作收进成员方法,比纯 API 少写一半代码。同时按钮状态仍可通过GetToolBarCtrl().EnableButton直接控制,适合和老版本 Visual C++ 6.0、VC2010 工程共存的代码风格。唯一要接受的就是后续所有布局自己维护。
3. 逐步实现:在 OnInitDialog 中给 VC 对话框加载工具栏资源
3.1 先在 .rc 里建一个 Toolbar 资源,而不是 Bitmap
在 Visual C++ 资源视图中右键添加资源,类型选 Toolbar。新建后,资源编辑器会把整张位图切成若干 16×16 的小块,每一块就是一个按钮图标。双击某个按钮块可以给它写 ID,这个 ID 就是后面消息路由用的命令 ID,建议按功能起名,比如ID_DLG_OPEN、ID_DLG_SAVE。按钮块的数量要和最终期望的按钮数一致,分隔条可以通过把空白块拖宽来生成,编译后它会变成TBBS_SEPARATOR。
比较常见的失败现场是:在 .rc 里写了IDR_DLG_TOOLBAR BITMAP "res\\dlgtool.bmp",但代码里调用LoadToolBar(IDR_DLG_TOOLBAR)。虽然编译不报错,但运行时加载失败,因为资源类型不匹配。LoadToolBar只认TOOLBAR类型的资源,不认普通位图资源。下面这张表把两者区别列清楚:
| 资源类型 | 资源脚本里的声明 | 加载函数 | 常见问题 |
|---|---|---|---|
| BITMAP | IDR_DLG_TOOLBAR BITMAP "dlgtool.bmp" | CBitmap::LoadBitmap | 被LoadToolBar加载时报错 |
| TOOLBAR | IDR_DLG_TOOLBAR TOOLBAR后跟 BUTTON ID 序列 | CToolBar::LoadToolBar | 按钮块与 ID 错位 |
判断方法很简单:打开 .rc 文件,找到IDR_DLG_TOOLBAR那一行,类型是TOOLBAR才对。如果是BITMAP,要么把资源改回 Toolbar 类型,要么换用SetButtons手工指定按钮结构。
3.2 对话框成员和初始化序列
在对话框头文件里加一个CToolBar成员,不需要指定父窗口,CreateEx会完成绑定。建议同时加一个ResizeToolBar成员函数,所有需要重新布局的地方都进这个函数:
class CMyDlg : public CDialogEx { public: void ResizeToolBar(); protected: CToolBar m_wndToolBar; };然后在OnInitDialog中完成“创建、加载、指定样式、布局”四件事,顺序不要颠倒。工具栏创建失败时,工具类对话框一般选择继续显示,而不是直接返回 FALSE 结束弹窗:
BOOL CMyDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 减少工具栏子窗口重绘闪烁 ::SetWindowLong(m_hWnd, GWL_STYLE, ::GetWindowLong(m_hWnd, GWL_STYLE) | WS_CLIPCHILDREN); if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT | TBSTYLE_TOOLTIPS, WS_CHILD | WS_VISIBLE | CBRS_TOP) || !m_wndToolBar.LoadToolBar(IDR_DLG_TOOLBAR)) { TRACE0("对话框工具栏创建失败\n"); } else { m_wndToolBar.SetBarStyle(m_wndToolBar.GetBarStyle() | CBRS_TOOLTIPS | CBRS_FLYBY); ResizeToolBar(); } return TRUE; } void CMyDlg::ResizeToolBar() { if (!m_wndToolBar.GetSafeHwnd()) return; CRect rcClient; GetClientRect(&rcClient); int nHeight = m_wndToolBar.CalcFixedLayout(FALSE, TRUE).cy; m_wndToolBar.MoveWindow(0, 0, rcClient.Width(), nHeight); }CalcFixedLayout(FALSE, TRUE)是CControlBar的方法,返回根据按钮、图标和边框计算出来的理想尺寸,比手工猜“30 像素”可靠。WS_CLIPCHILDREN加在对话框上以后,工具栏区域内的刷新不会带动整个对话框背景,这也是防闪烁的关键。如果担心运行时SetWindowLong带来一次重绘,可以直接在对话框模板的 Styles 属性里勾选 Clip children。
提示:在对话框上不要调用
EnableDocking和DockControlBar,它们依赖CFrameWnd的 DockBar,在CDialog里调用通常不报错,但也不会有任何停靠动作。
3.3 控件跟着工具栏向下挪:WM_SIZE 里的重排逻辑
对话框里已有的 Edit、List 控件坐标写在模板里,工具栏占掉顶部约 24 到 30 像素以后,第一行内容会被遮住。稳妥的做法是在WM_SIZE响应里统一重新布局,而不是去改 .rc 的像素值。以列表控件为例:
void CMyDlg::OnSize(UINT nType, int cx, int cy) { CDialogEx::OnSize(nType, cx, cy); if (!m_wndToolBar.GetSafeHwnd() || !m_wndList.GetSafeHwnd()) return; CRect rcClient(0, 0, cx, cy); int nTop = m_wndToolBar.CalcFixedLayout(FALSE, TRUE).cy; m_wndToolBar.MoveWindow(0, 0, rcClient.Width(), nTop); m_wndList.MoveWindow(0, nTop, rcClient.Width(), rcClient.Height() - nTop); }这里必须在CDialogEx::OnSize之后取cx, cy,它们已是客户区的新尺寸。m_wndList的MoveWindow把列表控件顶边对齐工具栏下沿,宽度随对话框伸缩。如果对话框里控件很多,可以把“内容区起点”抽成一个成员变量,所有控件以它为准计算相对位置,而不是逐个硬编码。
4. 工具栏参数调整:按钮样式、图标尺寸与提示文本
代码能编译出来了,接下来是视觉与交互细节。对话框里没有菜单栏和状态栏帮工具栏“撑”高度,按钮尺寸、图标大小和背景颜色都直接暴露在窗体上。下面三个参数最常被调整,也最容易出问题。
4.1 按钮与位图尺寸分离设置
CToolBar默认按位图资源中每块 16×16 取图标。想做成 24×24 的大按钮时,只把位图画成 24×24 还不够,还要告诉工具栏按钮矩形成比例放大。用SetSizes:
m_wndToolBar.SetSizes(CSize(32, 30), // 整个按钮的矩形,含边距 CSize(24, 24)); // 位图中每个图标的尺寸第一个参数决定按钮之间间距,第二个参数决定位图内的采样框大小。两个参数必须是图标宽高加偶数边距,否则按钮文字和图标对不齐。高 DPI 显示器上,Windows 会把旧位图做模糊拉伸,最省事的兼容方案是保留 16×16 原始位图并关闭进程的 DPI 虚拟化;如果必须在 DPI 感知进程中工作,就要为 120%、144% 分别准备位图资源,SetSizes的数值也要跟随系统 DPI 换算。
4.2 按钮上显示文字而不是图标
有的工具型对话框希望按钮上直接写“打开”“保存”,省去画图标的成本。在 MFC 工具栏里,给按钮设置文字的前提是按钮样式带TBSTYLE_LIST,这属于工具栏扩展样式,要借助GetToolBarCtrl操作:
m_wndToolBar.GetToolBarCtrl().SetExtendedStyle( m_wndToolBar.GetToolBarCtrl().GetExtendedStyle() | TBSTYLE_EX_MIXEDBUTTONS); m_wndToolBar.SetButtonText(0, _T("打开")); m_wndToolBar.SetButtonText(1, _T("保存"));SetButtonText的第一个参数是按钮在工具栏内部的索引,不是命令 ID。索引按LoadToolBar资源里按钮排列顺序从 0 开始,分隔符也要占索引。如果只给部分按钮设置了文字,另一部分仍然是位图,TBSTYLE_EX_MIXEDBUTTONS保证两种按钮共存时互不影响尺寸。需要同时显示图标和文字时,图标尺寸一般要调小,并给工具栏宽度留出余量。
4.3 常用样式组合与副作用对照
| 样式 | 预期效果 | 对话框场景下的副作用 |
|---|---|---|
| TBSTYLE_FLAT | 鼠标悬停时按钮凸起 | 父窗口不是纯色背景时,按钮背景可能盖成灰块 |
| TBSTYLE_TRANSPARENT | 按钮背景透出父窗口 | 父窗口重绘时需先重绘工具栏下方区域 |
| TBSTYLE_TOOLTIPS | 鼠标悬停弹出工具提示 | 工具栏获得焦点,不影响按钮命令 |
| CBRS_FLYBY | 在状态栏显示提示文本 | 对话框没有状态栏时提示不会显示 |
| CBRS_BORDER_TOP/BOTTOM | 工具栏上下加边框线 | 边框线会占进 MoveWindow 的高度,要注意多出的 2 像素 |
工具栏提示文本和命令 ID 的关联,通常写在资源编辑器里的 Prompt 属性中,形式是“打开文件\n打开文件对话框”。\n前是状态栏文本,后面是 ToolTip。如果只写了前半句,ToolTip 会为空;对话框上看不到状态栏,因此这种问题会被误判成“提示功能没生效”。排查时可以用 Spy++ 给工具栏窗口发TB_GETBUTTONTEXT,看返回字符串是否包含预期文字。
注意:每次调用
SetBarStyle或SetSizes后,都要重新走一遍ResizeToolBar()。工具栏样式改变会重新计算高度,比如加上CBRS_BORDER_TOP后,CalcFixedLayout返回的高度会多 2 像素;如果不重排,上下边缘可能互相覆盖。
5. 命令路由、状态灰化与对话框工具栏的四个失败排查点
对话框工具栏的消息流和主框架工具栏不完全一样,但原理足够简单:工具栏是子窗口,用户点按钮后 Windows 会给工具栏的父窗口发WM_COMMAND,父窗口就是对话框,因此消息映射命中的无非是ON_COMMAND。真正容易出问题的是按 ID 范围和灰显状态这两件事。
5.1 用消息映射接住按钮 ID
在 .rc 里给工具栏按钮分配ID_DLG_OPEN、ID_DLG_SAVE之后,在对话框类的消息映射中加入:
BEGIN_MESSAGE_MAP(CMyDlg, CDialogEx) ON_WM_SIZE() ON_COMMAND_RANGE(ID_DLG_OPEN, ID_DLG_SAVE, &CMyDlg::OnToolCommand) END_MESSAGE_MAP() void CMyDlg::OnToolCommand(UINT nID) { if (nID == ID_DLG_OPEN) { // 打开文件逻辑 } else if (nID == ID_DLG_SAVE) { // 保存逻辑 } }ON_COMMAND_RANGE的起始和结束 ID 必须在 resource.h 中明确存在。如果中间某个数值被重复定义,编译期不会有提示,运行期行为却取决于消息映射的注册顺序,这种问题最难排查。
注意:工具栏按钮 ID 要避开对话框模板里已有的控件 ID。Visual C++ 自动生成的对话框通常带
IDC_STATIC、IDOK、IDCANCEL,如果工具栏按钮 ID 不小心等于IDC_EDIT1的值,点工具栏就等于点了一个隐藏热点,消息会发生错乱。最好给工具栏命令单独划一段显式范围,比如 40000 到 40100。
5.2 状态灰化:ON_UPDATE_COMMAND_UI 在对话框上的边界条件
在单文档框架里,ON_UPDATE_COMMAND_UI会在空闲时自动更新按钮的灰显状态。模态对话框走的是 CWinApp 消息循环,MFC 的OnIdle会触发控制栏更新,因此多数场景下“打开可点、保存置灰”可以直接用更新处理函数。但如果你用的是CDialog::CreateModeless或DialogBoxParam这类不完全的循环,ON_UPDATE_COMMAND_UI不一定按预期触发。
这时可以绕开命令更新机制,直接操作工具栏控件:
m_wndToolBar.GetToolBarCtrl().EnableButton(ID_DLG_SAVE, bCanSave);这行代码放在业务状态变化之后立即生效,界面响应更及时。另一种做法是给对话框消息映射加WM_KICKIDLE手动调用UpdateDialogControls,但对普通对话框应用没有直接操作工具栏控件方便。
5.3 四个常见失败点的排查顺序
| 现象 | 检查对象 | 常见根因 |
|---|---|---|
| 对话框上看不到工具栏 | 资源类型声明 | .rc 里写成 BITMAP 而非 TOOLBAR |
| 按钮图标错乱 | 分隔符与按钮块 | 资源编辑器里拖动过分隔条,导致采样框整体偏移 |
| 点了按钮没反应 | 按钮 ID 范围 | ID 与 IDC_CONTROL 或 IDOK、IDCANCEL 重叠 |
| 工具栏尺寸随窗口拉伸变成异常宽度 | 布局函数调用时机 | 固定写MoveWindow(0,0,60,30),没有在 OnSize 里重算 |
其中“按钮图标错乱”最隐蔽。工具栏资源的本质是“位图块数组”,每个按钮占一个块,分隔符在画面上没有独立块,但会出现在LoadToolBar后的按钮数组里。如果在资源编辑器中把某两个按钮块之间的分隔条拖动了几个像素,看到的按钮还是那几个,但运行时采样框会偏移,图标头部会跑进上一个按钮的区域。解决办法是删除多余分隔条、保证按钮宽度一致,或者用SetButtons手工传入按钮数组,彻底绕开资源编辑器的块划分。
6. 收敛成一个可复用函数:把对话框工具栏做成通用基础件
虽然标题里的下载包名字看起来像一个现成源码,但工程里真正省事的是先沉淀一个通用函数。对话框每次的父窗口、资源 ID、工具栏成员都不同,工厂函数需要同时接收这三个信息。下面是我在 MFC 工程里常用的写法:
BOOL CreateDlgToolBar(CDialog* pDlg, CToolBar& tb, UINT nResID) { if (!tb.CreateEx(pDlg, TBSTYLE_FLAT | TBSTYLE_TOOLTIPS, WS_CHILD | WS_VISIBLE | CBRS_TOP) || !tb.LoadToolBar(nResID)) { return FALSE; } tb.SetBarStyle(tb.GetBarStyle() | CBRS_TOOLTIPS | CBRS_FLYBY); ::SetWindowLong(pDlg->GetSafeHwnd(), GWL_STYLE, ::GetWindowLong(pDlg->GetSafeHwnd(), GWL_STYLE) | WS_CLIPCHILDREN); // 用工具栏报告的实际高度,而不是写死 30 CRect rcClient; pDlg->GetClientRect(&rcClient); int nHeight = tb.CalcFixedLayout(FALSE, TRUE).cy; tb.MoveWindow(0, 0, rcClient.Width(), nHeight); return TRUE; }调用方在OnInitDialog里写成:
if (CreateDlgToolBar(this, m_wndToolBar, IDR_DLG_TOOLBAR)) { // 工具栏已经贴在客户区顶部,下方内容从 nHeight 开始布局 }CalcFixedLayout(FALSE, TRUE)的返回值在这里替代了所有硬编码高度。它内部依据按钮大小和边框计算,不同 DPI 机器上也能保持基本适配。但MoveWindow里用的rcClient.Width()只在对话框初始显示时正确;如果之后用户拉伸窗口,仍然需要在OnSize里重新调用一次工具栏的MoveWindow。
封装完成之后,再用 Spy++ 验证一遍最终状态是个好习惯。打开 Spy++,找到对话框窗口,查看它的子孙窗口里是否有一个类名为ToolbarWindow32的子窗口;确认它的父窗口是对话框 HWND,位置是(0, 0),宽度接近客户区宽度。这一步比单步调试更快,也能最直观地说明对话框上工具栏的实质:它就是一个被正确摆放的普通子窗口。以后再遇到“工具栏不见了”的问题,不要怀疑 MFC 内部逻辑,而是直接看 HWND 的父子层级和矩形坐标。
本文还有配套的精品资源,点击获取