ARTICLE DETAIL

资讯详情

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

MFC Tab Control实战:子对话框嵌入与切换详解

MFC Tab Control实战:子对话框嵌入与切换详解 简介这是一份面向Windows开发者的VC2010源码示例工程由两个相互关联的对话框程序构成集中演示Tab Control控件的多页签搭建与切换。工程内同时包含DLL注入模式外挂框架的参考实现展示如何把功能模块以动态库形式注入目标进程适合掌握C基础、对Win32界面控件和进程注入技术感兴趣的读者深入学习。压缩包共55个文件核心为14个cpp源文件和20个h头文件并配有rc资源脚本、ico图标、VS工程配置等辅助文件整体仅174KB代码规模适合快速通读。已有258人学习浏览。解压后用VS2010打开解决方案文件可查看基于MFC对话框的多页签界面Dlg1至Dlg4分别对应不同选项卡完整包含控件初始化、页签切换、数据交互等逻辑wulin模块则展示了DLL注入框架的工程组织方式便于对照界面操作与后台处理流程也可作为自研Tab式工具窗口或注入框架的起点。 不知道你有没有遇到过这种需求主窗口左边要放一块实时曲线区右边放一组参数配置页用户点不同的选项卡在“基本参数”“警告设置”“串口配置”之间来回切。很多人第一反应是直接用CPropertySheet结果弹出来一个独立的模态对话框怎么也嵌不进主窗口。这种场景下Tab Control才是正解。我最近用VC2010整理一批老的MFC上位机工程顺手把Tab Control控件的挂载逻辑、切换处理、焦点细节全部过了一遍整理成一套完整可跑的演示源码。这篇就围绕这套源码来拆适合正在学MFC、或者像我一样要长期维护老工程的朋友。1. Tab Control到底适合干什么先别急着写代码1.1 Tab Control和CPropertySheet的使用边界Tab Control在MFC里属于通用控件本质就是一组标签按钮加一个容器区域。它本身不管理页面生命周期所有“页面的创建、显示、隐藏、销毁”都要由程序自己控制。这一点和CPropertySheet完全不同。CPropertySheet是高层封装内部用属性页机制管理调用DoModal弹出一个带标签栏的模态对话框。它省事但代价是它是个独立的顶层窗口没法把属性页嵌进主窗体的某个区域。比如你要做一个设备调试面板左边是波形显示、右边是参数配置页这种布局CPropertySheet根本做不了。Tab Control没有这个限制它可以作为普通控件被放在任意位置子对话框也只当成子窗口贴进去能和旁边其他控件自由共存。所以选型其实很简单能接受弹出窗口用CPropertySheet开发速度最快必须内嵌到主界面就老老实实用Tab Control。我见过不少人非要用CPropertySheet做嵌入结果在父子窗口关系、样式互斥上折腾两天最后还是要推翻重来。不如一开始就选对。1.2 演示工程里我用它做了什么这套演示源码的规划很克制没有堆花活。主对话框上只放一个Tab Control和几个占位说明文字Tab里挂两个子页面一个叫“基本参数”放几个编辑框和下拉框模拟设备参数录入另一个叫“警告设置”放若干复选框和静态文本模拟告警阈值配置。这样的规划是为了让读者把精力集中在“Tab Control怎么玩”这件事上而不是被业务逻辑干扰。工程结构也简单清晰主对话框类是CTabDemoDlg两个子页面类分别是CBasicPage和CAlarmPage全部继承CDialogEx。源码分包组织头文件、源文件、资源文件分离后续要加新页面照葫芦画瓢就行。2. 工程初始化里没做对这些后面全是白忙活2.1 新建工程和放置控件的几个细节打开VC2010新建MFC应用程序项目类型选“基于对话框”语言选中文完成。工程默认会生成一个CMainDlg类和一个主对话框资源。这些步骤都很常规但有三个细节我建议在一开始就做掉不然中途会难受。第一个细节确认工程字符集是Unicode。在项目属性-常规-字符集里设置如果工程是“未设置”或多字节后面InsertItem传_T字符串容易出乱码我还是习惯一开始就统一到Unicode。第二个细节给Tab Control单独起一个语义明确的ID比如IDC_TAB_MAIN不要留着默认IDC_TAB1到处飞等代码写多了一看ID就知道是哪个控件。第三个细节在资源编辑器里把Tab Control拉到合适大小别指望后面全靠代码重新框位置先拉个大概能省不少事。然后右键Tab Control选择“添加变量”变量名m_tabMain类别Control类型CTabCtrl类向导会自动生成关联代码。CTabCtrl是MFC对Windows Tab Control的标准封装几乎所有操作都通过这个对象完成。2.2 OnInitDialog里必须先做的两件事OnInitDialog是对话框初始化入口。Tab Control的初始化逻辑我建议全放在里面按固定顺序做两件事插入标签项和设置默认选中项。插入标签用InsertItem原型是int InsertItem(int nItem, LPCTSTR lpszItem)按页签顺序逐个插入。比如演示源码里这样写的BOOL CTabDemoDlg::OnInitDialog() { CDialogEx::OnInitDialog(); m_tabMain.InsertItem(0, _T(基本参数)); m_tabMain.InsertItem(1, _T(警告设置)); m_tabMain.SetCurSel(0); // 后面子对话框的创建挂载逻辑也在这里 return TRUE; }SetCurSel(0)这一行特别容易漏。不调用SetCurSelTab Control虽然显示了两个标签但当前没有选中任何一项GetCurSel()会返回-1后面所有依赖当前选中索引的切换逻辑全会错乱。这几乎是Tab Control演示工程新手必踩的一个坑。还有一个隐藏问题如果标签很多或者标签文本需要动态变化建议把InsertItem集中放在OnInitDialog里不要分散到各个子页面的初始化函数中不然调试时很难追踪标签从哪里来的。3. 子对话框挂载把页面“安装”进Tab客户区3.1 子对话框资源属性设置这几项必须改Tab Control本身不提供页面功能所谓的内容页实际是子对话框。你需要在资源编辑器里插入两个Dialog然后生成对应的对话框类。但插进来之后子对话框的默认属性是顶层可移动的弹窗如果不改属性它创建出来会飞在主窗口外面。这就是很多演示源码第一次跑起来最让人懵的问题。选中子对话框资源在属性栏里必须改三项Style改成ChildBorder改成NoneVisible建议保持勾选或取消都行反正创建后会用ShowWindow统一控制。StyleChild意味着这个窗口被创建为子窗口由父窗口决定位置和显示BorderNone去掉对话框的标题栏和边框让它看起来是一个干净的页面。这两项不改子窗口就不会“贴”在Tab Control里而是变成一个带标题栏的独立窗口肉眼可见地失败。生成类也简单在子对话框资源上右键选择“添加类”类名填CBasicPage基类选CDialogEx完成。VC2010会自动生成头文件和源文件。这里要注意不要勾选“创建可停靠窗格”之类的选项我们需要的是一个最普通的对话框类。3.2 Create、MoveWindow、ShowWindow的标准连贯操作子对话框类生成好之后要在主对话框类头文件里声明两个成员对象CBasicPage m_basicPage; 和CAlarmPage m_alarmPage;。注意用对象成员而不是指针这样生命周期跟着主对话框走程序退出时自动析构省去手动delete的麻烦。然后回到OnInitDialog在InsertItem之后补充创建逻辑。核心代码如下CRect rcTab; m_tabMain.GetWindowRect(rcTab); ScreenToClient(rcTab); m_tabMain.AdjustRect(FALSE, rcTab); m_basicPage.Create(IDD_DIALOG_BASIC, m_tabMain); m_basicPage.MoveWindow(rcTab); m_basicPage.ShowWindow(SW_SHOW); m_alarmPage.Create(IDD_DIALOG_ALARM, m_tabMain); m_alarmPage.MoveWindow(rcTab); m_alarmPage.ShowWindow(SW_HIDE);这段代码的关键是计算客户区矩形。AdjustRect(FALSE, rcTab)这行要重点讲它把rcTab从“Tab Control的窗口矩形”转换成“去掉标签栏后真正用来显示内容的客户区矩形”。如果这里直接拿GetClientRect的结果去MoveWindow你会发现子页面区域会偏小底部空出一截标签栏高度的空白。因为GetClientRect返回的是整个控件客户区标签栏也占着这个区域必须再通过AdjustRect做一次修正。Create的第二个参数传m_tabMain这个父窗口指针是必须的。跳过MoveWindow直接ShowWindow也能显示但子页面默认位置会在(0,0)正好被标签栏盖住表现为“页面怎么也看不见”。所以MoveWindow一起做掉比较靠谱。4. TCN_SELCHANGE切换逻辑与焦点细节4.1 用类向导还是手写消息映射Tab Control在用户点击标签的时候会向父窗口发送WM_NOTIFY通知码TCN_SELCHANGE。处理这个通知就用ON_NOTIFY宏固定拦下来。VC2010的类向导操作方法是在主对话框上选中Tab Control右键“添加事件处理程序”消息类型选TCN_SELCHANGE函数名默认OnTcnSelchangeTabMain完成后类向导自动在消息映射表中添加一行ON_NOTIFY(TCN_SELCHANGE, IDC_TAB_MAIN, CTabDemoDlg::OnTcnSelchangeTabMain)这个方法最不容易出错。如果非要在代码里手写消息映射格式一模一样但手写顺序错了比如把IDC_TAB_MAIN写成别的控件的ID通知就不会进到这个函数表现出来是点击标签完全没有反应。我在演示源码里用的是类向导生成降低阅读门槛。4.2 切换处理的核心思路先取得当前选中页再做页面显隐处理TCN_SELCHANGE时Tab Control已经把内部选中状态更新了所以直接GetCurSel()拿到当前标签索引即可。然后按索引切换子页面的显示和隐藏。演示源码里的处理方式如下void CTabDemoDlg::OnTcnSelchangeTabMain(NMHDR *pNMHDR, LRESULT *pResult) { int nCur m_tabMain.GetCurSel(); m_basicPage.ShowWindow(nCur 0 ? SW_SHOW : SW_HIDE); m_alarmPage.ShowWindow(nCur 1 ? SW_SHOW : SW_HIDE); *pResult 0; }用三元运算符一行搞定逻辑很干净。如果页面超过两个sinwi不建议继续叠加三元改用switch或者映射表更清晰。这里有一个很多人会问的问题要不要在切换时调用SetFocus把焦点放进子页面我建议处理一下。演示源码里页面上的编辑框较多点击标签切到新页面后如果焦点还留在Tab Control上用户按Tab键不会进入页面的控件体验很怪异。基本做法是切换后用GetDlgItem取到当前子页面上第一个要获得焦点的控件然后调用SetFocus比如if (nCur 0) { m_basicPage.GetDlgItem(IDC_EDIT_NAME)-SetFocus(); }如果不做这步大部分情况下影响不大但有键盘操作习惯的使用者会明显感到不顺手。另外说一个进阶点如果要在切换之前做一些校验拦截比如当前页面有未保存数据时禁止离开需要处理的是TCN_SELCHANGING通知而不是TCN_SELCHANGE。TCN_SELCHANGING在选中项改变之前触发处理函数里可以对切换合法性做判断不合法就返回阻止切换。演示源码没有展开这层逻辑但接口是同一个思路按ON_NOTIFY增加一个处理函数就行。5. 实际调试中反复出现的几个“页面不显示”类问题5.1 页面变成独立顶层窗口压根没嵌进Tab这是最常见的现象一运行弹出的小对话框带着标题栏左上角出现在屏幕上而Tab控件区域却一片空白。出现这个现象基本是子对话框资源的Style属性没有设成Child或者根本没有设。新建对话框默认Style是Popup这种窗口一Create就作为顶层窗口存在。改法很简单回到资源编辑器选中子对话框属性把Style从Popup改成Child再把Border从Dialog Frame改成None重新编译运行。5.2 页面显示出来但位置偏了半个标签栏高度如果你看到页面区域整体往下偏移顶部被标签栏挡住一截问题几乎都出在MoveWindow之前没有用AdjustRect修正矩形。我之前反复强调的AdjustRect(FALSE, rcTab)就是干这个的。有人会写出GetClientRect之后直接MoveWindow的代码这在标签栏高度不算特别大时看着还行但标签栏一旦加高或者加图标底部就会多出一块空白页面和标签栏还会重叠。建议老老实实GetWindowRect ScreenToClient AdjustRect三步走。另外一个相关问题是子页面尺寸不对导致控件被裁掉一半。这种情况通常是MoveWindow的宽高参数写死导致的比如直接传500x300没跟Tab客户区对齐。用AdjustRect算出来的矩形天然就是客户区的实际尺寸不必关心具体数值。5.3 点击页面上的按钮第一次没反应第二次才触发这个坑比较隐蔽。我调试时遇到过第一次点击子页面上的按钮完全没反应再用鼠标点一下才触发像是控件响应慢了半拍。排查到最后原因是切换页面时我用了SetWindowPos调整Z序把主对话框上某个覆盖层窗口顶到了子页面之上第一次点击实际上点到的是覆盖层第二次点击穿透才点到按钮。解决方法很简单切换页面后调用BringWindowToTop把子页面窗口提到前台或者干脆不要引入多余的覆盖窗口。如果子页面上存在特殊自定义控件、或者将来要做Page内的复杂动画别忘检查Z序。5.4 程序退出时崩溃或者反复切换后内存不断增长崩溃一般在主对话框关闭时发生原因是子页面对象被重复创建或者手动释放了两次。演示源码里用对象成员来管理子页面主对话框析构会自然销毁子页面对象不需要在OnDestroy里额外delete。如果你用指针new出来的页面一定要确保在主对话框销毁之前的时机Delete正常执行否则容易二次释放。内存增长则多和动态Create有关。如果切换页面时每次都新建子页面、再DestroyWindow忘了删除旧页面内存就会一路涨上去。我的建议是最简单的场景里坚持“预创建全部页面、切换只改显隐”的策略这套演示就是这种实现稳定且思路清楚。如果页面数量实在多到预创建浪费资源再做动态创建也不迟。到那时需要维护一套页面管理结构按需Create、按需Destroy配合缓存策略保证不会反复创建。这个属于进阶话题以后可以单独写一篇。最后再分享一个经验页面多了以后别让主对话框里的切换代码无限膨胀。我一般会给所有子页面定义一个统一的基类里面放两个虚函数InitPage和OnPageVisible分别处理页面初始化和页面可见性变化。Tab Control切页时循环调用当前页的OnPageVisible(true)和其他页的OnPageVisible(false)这样加新页面只需继承基类、实现两个函数主对话框的逻辑一行都不用改。这套演示源码还是用最直白的页面类写法方便入门理解一旦你决定把它应用到实际工程建议尽早引入这层抽象。本文还有配套的精品资源点击获取
返回列表