
简介面向使用微软 VC 与 MFC 的开发者这是一个完整示例工程重点解决在对话框中集成工具栏的常见界面需求。适合初学 Windows 桌面程序开发、希望把常用功能集中到对话框工具栏中的读者。压缩包内共 19 个文件主要包含与对话框、视图、文档相关的头文件和实现文件也包含界面资源描述、按钮位图、程序图标、工程配置文件等可在 VC 开发环境中直接打开查看整体项目结构。整个压缩包仅 17KB体量小巧已有 181 人学习浏览。示例中展示了如何构建对话框子类、设计工具栏按钮资源、在窗口初始化阶段挂接工具栏并通过消息映射让按钮产生实际响应同时可观察到文档、视图与对话框之间的协作方式。对正在学习 MFC 界面开发、想要获得可运行参考并排查同类问题的开发者具有实用借鉴意义。1. 给对话框塞一个可停靠工具栏这份 zip 包到底能省多少事在 MFC 程序里给对话框加工具栏是很多用 Visual C 6.0 或 Visual Studio 老版本做桌面工具的人绕不开的一步。网上流传的这包visual c vc给对话框添加工具栏.zip里面装的就是一份可以直接抄作业的示例工程常见内容是一个手工创建工具栏资源、在对话框的OnCreate里挂上按钮、再通过ON_COMMAND接点击事件的完整流程。拿过来用省掉的是从零翻 MSDN、反复试CreateToolBar参数的功夫但如果你不搞懂资源编辑器里那些按钮 ID 和图标文件是怎么对上的zip 解开照样跑不起来。这篇文章就按一个做过多次这类小工具的一线工程师的视角把原理、最小可复现步骤和最常见的几个坑一次说透。2. 从资源到代码对话框工具栏的创建原理与选型理由2.1 为什么对话框不像主框架那样天生带工具栏用过MFC AppWizard生成 SDI 或 MDI 程序的人知道主框架窗口一启动就带着菜单栏、工具栏和状态栏是因为CMainFrame在OnCreate里调用了CreateToolBar或CreateEx然后EnableDocking和DockControlBar一套组合拳。而基于CDialog的程序是由DoDataExchange和对话框管理器在管窗口消息循环对话框资源模板并不包含工具栏这个子窗口所以打开就是一个光秃秃的矩形对话框。要让工具栏出现在对话框里本质上是在对话框创建完成后再动态Create一个CToolBar控件作为它的子窗口。这个动作一般放在响应WM_CREATE的消息处理函数里——也就是对话框类重写的OnCreate。注意OnCreate只在对话框有WS_CREATE流程时才会触发如果你用DoModal模态显示消息序列是OnCreate先于OnInitDialog如果你用Create非模态显示两者的先后仍然是OnCreate先走。所以工具栏创建代码放OnCreate和OnInitDialog都可以但放OnCreate更靠近窗口创建时机不容易抢焦点。另一个容易忽略的点是对话框默认没有CToolBar对应的资源。主框架程序生成工程时资源文件里自动加了IDR_MAINFRAME工具栏里面有一串按钮和一个toolbar.bmp位图。对话框程序要自己手动插入一个工具栏资源。这也是那个 zip 包的价值所在——它把“插入资源 画图 写代码”的整个流程给你走了一遍你照着做就行。2.2 三种常见创建方式选对方案比写对代码更重要给对话框加工具栏业界常见做法有三种我按个人偏好说下选型理由。第一种是 MFC 的CToolBar类配合LoadToolBar加载工具栏资源。这是最老牌、也是 zip 包里八成会采用的方式。它的优点是和 MFC 消息映射紧密结合按钮的ON_COMMAND直接进对话框类缺点是工具栏条外观偏老图标资源要用位图编辑器手工画。第二种是CMFCToolBar出现在较新的 Visual Studio 和 VC2008 之后的 Feature Pack 里。它支持停靠、自定义颜色、透明按钮但跟老工程的CDialog集成要额外EnableDocking而且如果是 Visual C 6.0 工程根本编译不了这套代码。对于标题里的老式visual c课程设计或维护项目我不太建议硬上 CMFCToolBar除非你确定目标环境是 VS2010 以上。第三种是不用工具栏资源直接在内存里Create按钮位图再用AddButtons把按钮塞进去。这种适合按钮数量极少、或者图标需要动态生成的场景。缺点是每个按钮的图标都要LoadBitmap或者画到CBitmap上代码量大而且在高 DPI 下容易糊。为了通用下面所有做法都以第一种CToolBar为核心这也是标题所在 zip 包里最可能的方式。2.3 代码组织把工具栏逻辑封装在对话框类里如果你下载的 zip 解压后能看到DlgToolbar.dsp、DlgToolbar.rc、res\Toolbar.bmp这样的文件那它的代码组织通常是这样的自定义一个从CDialog派生的类在这个类里加一个CToolBar m_wndToolBar;成员变量然后实现OnCreate在里面调用m_wndToolBar.Create(...)和m_wndToolBar.LoadToolBar(...)。这里我强烈建议你把工具栏的创建和按钮响应拆开创建部分集中放在一个BOOL InitToolBar()函数里OnCreate只调用它按钮的ON_COMMAND响应函数放另一个区域。为什么这么拆因为等你往后加右键菜单、加状态条、加按钮态更新时会发现一个OnCreate里堆几百行代码根本没法维护。zip 包里的示例往往为了展示简单把工具栏创建全塞在OnCreate里你可以学但别学它的坏习惯。另外一个值得留意的组织细节是资源 ID 规划。工具栏按钮的 ID 不能随便用建议沿用ID_FILE_OPEN、ID_EDIT_COPY这类 MFC 标准 ID或者自定义成IDC_MY_BTN1。用标准 ID 的好处是MFC 有一些默认命令处理函数能直接响应比如ID_APP_EXIT可以帮你退出程序。如果全用自定义 ID那就得一个一个写ON_COMMAND映射漏一个按钮就点了没反应。3. 照着做就能跑在 Visual C 里为对话框加工具栏的最小步骤3.1 工具栏资源编辑器里的按钮图标与分隔符参数怎么设在继续写代码前先在资源编辑器里把工具栏资源备好。以 Visual C 6.0 为例右键工程资源视图里的Toolbar文件夹选“Insert Toolbar”会生成一个默认 ID 为IDR_TOOLBAR1的工具栏资源同时自动附带一个 32x32 的位图资源。双击打开工具栏编辑器你会看到一排白色小方块每个方块对应一个按钮。左侧显示的是当前编辑的按钮右侧是整体预览。要控制按钮数量就是在这个编辑器里拖动右侧移动条来增删格子。选中一个格子在属性窗口设置其 ID。如果按钮只是用来做视觉间隔把 ID 设成ID_SEPARATOR它就是一条竖线分隔符不产生点击事件。图标绘制问题很关键——旧版 VC 的工具栏编辑器里推荐画按钮时把图标控制在 16x16 有效像素内因为LoadToolBar默认按资源里的格子宽度裁图画大了会被裁掉。这个“图标画出来是斜的/糊的”问题在第 4 章再展开。// 资源文件 .rc 中生成的工具栏资源描述关键行 IDR_TOOLBAR1 TOOLBAR 16, 16 BEGIN BUTTON ID_FILE_OPEN SEPARATOR BUTTON ID_FILE_SAVE BUTTON ID_APP_EXIT END这段代码不是手打的是资源编辑器自动生成的。关键在于TOOLBAR 16, 16表示每个按钮格子的宽度和高度是 16 个像素这个值决定了位图被切割成几个按钮。如果你把16, 16改成24, 24那么位图里的每个按钮区域会按 24x24 切图标会显得大一些但对话框客户区占用也会变大。一般对话框工具栏保持默认16, 16就够高 DPI 场景下再考虑32, 32。3.2 在 OnCreate 里挂工具栏那段必须手写的代码资源准备好了接下来就是最核心的 C 代码。先给对话框类加成员变量CToolBar m_wndToolBar;然后在OnCreate里创建它。下面是一段在 Visual C 6.0 到 VS2019 都能编译的基础版针对对话框的特点做了微调// MyDlg.cpp - 对话框的 OnCreate 里创建工具栏 int CMyDlg::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CDialog::OnCreate(lpCreateStruct) -1) return -1; // 关键参数ws_child 表示子窗口ws_visible 必须设否则白写 if (!m_wndToolBar.Create(this, WS_CHILD | WS_VISIBLE | CBRS_TOP, AFX_IDW_TOOLBAR) || !m_wndToolBar.LoadToolBar(IDR_TOOLBAR1)) { TRACE0(Failed to create toolbar\n); return -1; // 创建失败早返回避免画在无效窗口上 } // 去掉工具栏的边框让它看起来内嵌在对话框里 m_wndToolBar.SetBarStyle( m_wndToolBar.GetBarStyle() | CBRS_TOOLTIPS | CBRS_FLYBY); // 指定工具栏停靠在对话框顶部并允许用户拖动标题栏区域 m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY); EnableDocking(CBRS_ALIGN_ANY); DockControlBar(m_wndToolBar, AFX_IDW_DOCKBAR_TOP); return 0; }逻辑说明Create的第一个参数this指对话框作为父窗口CBRS_TOP告诉 MFC 工具栏默认贴顶部。AFX_IDW_TOOLBAR是一个预定义控件 ID如果不写得自己在资源里定义一个而且还要防冲突。LoadToolBar把资源里IDR_TOOLBAR1的按钮和位图加载进来。这里最容易踩的坑是忽略WS_VISIBLE——很多从主框架代码抄过来的人只写了WS_CHILD | CBRS_TOP结果窗口跑起来工具栏死活不显示。EnableDocking和DockControlBar这两个函数前一个是让工具栏本身可浮动/可停靠后一个是实际把它停靠到对话框的顶部停靠栏上。注意如果你不想让用户拖动工具栏可以去掉这两行只保留Create和LoadToolBar工具栏就会固定在对话框左上角无法移动。但控件条在没有停靠管理时通常不能自动调整位置拖拽对话框变大时工具栏宽度也不会跟着变。所以建议保留除非你刻意要做固定位置。3.3 消息映射与按钮点击响应连接按钮 ID 到处理函数工具栏按钮本质上是一个CMFCToolBarButton或旧版CToolBarButton它通过命令 ID 向父窗口发送WM_COMMAND。因此你得在对话框的 BEGIN_MESSAGE_MAP 里把按钮 ID 映射到处理函数否则点击只会听到系统提示音没有任何反应。// MyDlg.h 中声明两个响应函数 afx_msg void OnFileOpen(); afx_msg void OnAppExit(); // MyDlg.cpp 的消息映射 BEGIN_MESSAGE_MAP(CMyDlg, CDialog) ON_WM_CREATE() ON_COMMAND(ID_FILE_OPEN, CMyDlg::OnFileOpen) ON_COMMAND(ID_APP_EXIT, CMyDlg::OnAppExit) END_MESSAGE_MAP() // 空实现也要写出来让链接器找到入口 void CMyDlg::OnFileOpen() { // TODO: 打开文件对话框处理业务逻辑 AfxMessageBox(_T(Open selected)); } void CMyDlg::OnAppExit() { CDialog::OnOK(); // 关闭模态对话框 }参数说明ON_COMMAND(ID, 函数名)这条宏里ID 必须和工具栏资源按钮属性里的 ID 完全一致不能抄写错字符串。函数必须是对话框类的成员函数返回值void无参数。如果你点按钮没反应先检查两处一是资源里按钮 ID 是否和宏里第一个参数匹配二是这个对话框类的头文件里是否漏了afx_msg声明。我见过不少新手把映射写到别的类里比如写到CWinApp里结果编译过但运行没反应。OnAppExit调CDialog::OnOK()会触发OnOK虚函数关闭模态对话框。这里有个隐藏逻辑OnOK还会调用UpdateData(TRUE)如果你的对话框变量关联了输入框会引发数据校验。如果只是退出程序而不关心表单数据用EndDialog(IDCANCEL)更干净。到此一个最最简单的对话框工具栏已经能显示、能点击了。但你先别急着关这章往前走你就会发现按钮图标、布局、状态更新等一堆细节稍不注意就翻车。下面是几个我在实际维护里反复遇到的坑每一条都按“现象 → 原因 → 解决”写成排查笔记希望能帮你省下几小时的试错时间。4. 对话框工具栏的 5 个经典翻车现场与排查路径4.1 工具栏悬浮不显示背景色被对话框刷掉现象程序启动后对话框干干净净连工具栏的影子都没有但用 Spy 看窗口层级发现ToolbarWindow32其实已经存在只是位置不对或者被盖住了。原因这是把主框架的代码直接搬进对话框最常见的翻车。DockControlBar在没有独立的对话框停靠布局时会乱找位置常见结果是工具栏停靠到客户区之外或者因为父窗口没有调用RecalcLayout导致工具栏的初始宽度为 0宽度为 0 的窗口自然不会画内容。另外如果你的对话框背景设置了WM_ERASEBKGND处理且返回TRUE表示已擦除背景工具栏的刷新消息可能与背景擦除冲突导致工具栏区域被背景色重新刷掉。解决确认OnCreate里边工具栏创建成功返回 0再检查SetBarStyle里是否包含CBRS_SIZE_DYNAMIC。我一般会在Create之后加一行m_wndToolBar.CalcFixedLayout(FALSE, TRUE)主动计算一次宽度防止停靠管理器偷懒。更稳妥的做法是先在OnInitDialog里用MoveWindow手动指定一个初始矩形让工具栏有 64x32 以上的可视范围然后再交给DockControlBar管理。如果问题依旧把EnableDocking(CBRS_ALIGN_ANY)改成CBRS_ALIGN_TOP限制停靠方向能规避不少因布局计算产生的悬空位置。4.2 按钮点了没反应ON_COMMAND 映射写错了类现象编译正常运行后按钮是“按得动”的有明显按下弹起动画但就是不触发任何业务代码消息框也不弹。原因最常见的就是消息映射宏ON_COMMAND写在了错误的类里。工具栏按钮的命令消息默认发给父窗口也就是对话框。如果你在CMyDlgApp或某个全局函数里写了ON_COMMAND消息路由根本不会走到那里。第二个常见原因是按钮 ID 是 MFC 标准 ID比如ID_EDIT_COPY但你没有包含afxres.h或者命令路由被挡在PreTranslateMessage里。解决把ON_COMMAND挪回CMyDlg的消息映射里并保证函数是CMyDlg的成员。如果用的是标准 ID可以在映射里加一个ON_COMMAND(ID_EDIT_COPY, ...)验证下响应。还有一个隐蔽问题如果你在OnToolTip或OnContextMenu里调用Default()返回TRUE可能会吞掉按钮的WM_COMMAND。把这类鼠标右键处理函数清掉按钮点击恢复。4.3 按钮图标模糊在 VC6 下资源里直接画出来的谜之锯齿现象按钮能显示但图标边界明显有锯齿放大看像是把一个低分辨率位图硬拉伸了。尤其是在 Windows 10/11 上老 MFC 32x32 的工具栏位图自带模糊滤镜看着非常廉价。原因工具栏资源里TOOLBAR 16, 16声明的是按钮格子尺寸而不是位图实际像素。VC6 生成的位图资源默认按 16 色索引存储图标手动画的时候只用了 16 色颜色过渡全靠抖动所以在现代屏幕上就看得出颗粒感。另外如果位图资源尺寸是 32x32 但工具栏资源声明为 16x16MFC 加载后会用 GDI 的StretchBlt把 32x32 缩到 16x16这个过程没有滤镜产生明显锯齿。解决如果你维护的是老工程别在 VC6 里硬画直接用外部工具画一张 256 色或 32 位色深的 BMP替换掉res\Toolbar.bmp保证每个按钮图标的有效像素是 16x16 的整数倍。然后在资源.rc文件里把TOOLBAR 16, 16改成和位图匹配的尺寸。我一般做法是用画图软件把整套按钮图标按 32x32 网格画好存成 24 位 BMP然后让LoadToolBar按 32x32 切MFC 会在显示时自动缩放比手工画格子里的小图好看很多。如果目标机器 DPI 是 125% 或 150%建议位图直接画成 48x48配合SetButtonSize指定显示尺寸避免系统拉伸。4.4 工具栏宽度失控强制 SetWindowPos 之后又回弹现象你希望工具栏宽度跟随对话框宽度变化于是自己在OnSize里写了SetWindowPos强行拉伸。拖动对话框边框时肉眼能看到工具栏先变宽但一松鼠标就立刻缩回原来的宽度或者干脆重叠盖住了右侧的按钮。原因这是 MFC 控件条的经典“所有者尺寸”问题。CToolBar在停靠状态下其宽度由CalcDynamicLayout或CalcFixedLayout决定不在OnSize里重新布局。你手动SetWindowPos设置的是一个外部约束但内部Layout()在下一次RecalcLayout时会把它重新拉回按钮总和宽度。同时对话框的RepositionBars会按CBRS_TOP的停靠区自动调整子窗口你的手动设置和它互相打架。解决不要和框架抢布局。在对话框的OnSize里不要调用SetWindowPos去改工具栏宽度而是调用RecalcLayout()让 MFC 自己根据客户区宽度重新计算。如果你确实需要让工具栏填满整个客户区宽度可以在工具栏属性里给最后一个按钮放一个“弹性空白”或者重写CalcDynamicLayout让CDockBar计算时把宽度拉伸。对新手来说最简单的做法是去掉DockControlBar改用m_wndToolBar.MoveWindow(0, 0, rectClient.Width(), rectToolbar.Height())并加一个标志位在OnSize里重复设置同时把CBRS_SIZE_DYNAMIC去掉这样框架不会主动去重算。但注意这样工具栏就完全失去停靠能力了。4.5 在对话框模式下工具栏盖住文本框Z 序与客户区计算现象对话框上有一个输入框和一个按钮工具栏创建后位置在对话框顶部但输入框没有下移工具栏直接把输入框上半部分挡住了点击输入框时焦点会被工具栏截获。原因对话框的客户区布局默认是从(0,0)开始的而DockControlBar停靠后工具栏占据的是客户区顶部一条但对话框不会自动重新计算其他控件的坐标。主框架窗口有RecalcLayout联动机制而CDialog的OnSize里默认没有这样的联动所以所有基于对话框资源的控件位置都停留在设计时坐标。解决根据工具栏的实际高度在OnInitDialog或OnSize中手动偏移下面控件。一个通用做法是用一个静态控件或变量记录工具栏高度然后遍历子窗口把矩形top加上这个高度。还可以用RepositionBars的组合RepositionBars(AFX_IDW_CONTROLBAR_FIRST, AFX_IDW_CONTROLBAR_LAST, 0, reposQuery, rcClient);这时返回的rcClient就是去掉停靠栏之后的客户区再用rcClient去定位你的输入框。这个方案比手动偏移健壮也兼容用户把工具栏拖到左侧或右侧的情况。我在实际项目里就是用这个办法解决盖控件的代码就几行CRect rcClient; RepositionBars(AFX_IDW_CONTROLBAR_FIRST, AFX_IDW_CONTROLBAR_LAST, 0, reposQuery, rcClient); // rcClient 顶部会避让工具栏用它重排输入框和按钮 m_editInput.MoveWindow(rcClient.left 10, rcClient.top 10, rcClient.Width() - 20, 24); m_btnGo.MoveWindow(rcClient.left 10, rcClient.top 44, rcClient.Width() - 20, 32);注意reposQuery是一个枚举标志它只查询停靠区域占用情况不实际移动控件。整段代码必须放在对话框已经显示之后OnInitDialog末尾最合适如果你放在OnCreate里控件还没被资源管理器创建操作对象不存在。5. 让工具栏更像样停靠、提示条与按钮状态的管理技巧如果你已经做到按钮能显示、点击能响应、控件不被遮挡恭喜一个基础版对话框工具栏已经可交付了。但离真正“能用”还差几步按钮的灰色态、鼠标悬停提示、以及图标在高 DPI 屏幕上不糊。这些才是让使用者觉得你不是在交课程设计的关键。按钮状态管理靠ON_UPDATE_COMMAND_UI宏它在工具栏绘制前被调用你可以在里面根据业务条件动态置灰。例如一个“保存”按钮只有编辑框内容有改动才应可点void CMyDlg::OnUpdateFileSave(CCmdUI* pCmdUI) { pCmdUI-Enable(m_bDirty); // m_bDirty 在编辑框内容变化时置 true }对应的消息映射是ON_UPDATE_COMMAND_UI(ID_FILE_SAVE, CMyDlg::OnUpdateFileSave)。这个机制在对话框里同样有效而且比在按钮点击时手工改EnableWindow省事得多——当工具栏按钮和菜单项共享同一个 ID 时这个映射会同时控制两者的状态避免了两处状态不同步的尴尬。ToolTip 提示在Create时已经用CBRS_TOOLTIPS | CBRS_FLYBY打开了但按钮的文字提示从哪里来MFC 默认会查找字符串资源里与按钮 ID 同 ID 的字符串。比如按钮 ID 是ID_FILE_OPEN提示文字来自字符串资源表里ID_FILE_OPEN对应的那一行。这也是为什么我建议用有语义的 ID 而不是IDC_BUTTON1——给工具栏按钮一个同 ID 的字符串资源悬停提示自然就有了。如果你不想在字符串表里加东西也可以直接在TTN_NEEDTEXT消息里手动返回文字但那个要ON_NOTIFY_EX映射代码量翻倍不值。最后说下高 DPI 与“看起来专业”的问题。老 MFC 的工具栏在小缩放比例下很精致一上 150% 就明显发虚。我用过的成熟方案是把位图按 64x64 绘制在工具栏创建后调用m_wndToolBar.GetToolBarCtrl().SetButtonSize(CSize(32, 32))和SetBitmapSize(CSize(32, 32))利用按钮控件的缩放能力或者干脆为每个按钮单独设置SetImageList用ImageList_Create载入 32x32 的真彩位图。后者的兼容性稍差但效果最好。如果你只是想要个不太费事的折中在OnCreate里加一句SetWindowFont换成Microsoft YaHei UI再给对话框EnableVisualStyle设置按钮的立体感会好很多。我自己的习惯是凡是交给客户的工具工具栏按钮固定用32x32提示文字写清楚并且强制停靠在顶部不允许拖动——因为对话框里的工具栏拖动停靠后按钮和客户区的相对位置经常乱成一团客户可不会像你一样开着 Spy 去调布局。版本再多结构不变这套地基打好了后续换图标、加按钮都只是资源修改而已。希望这些步骤和踩坑记录能帮你在下一台机器上少走几步弯路直接把那个 zip 里的示例做成能交付的成品。本文还有配套的精品资源点击获取