资讯详情

资讯详情

VC2008 MFC老工程分页配置页:CTabSheet实现与避坑

简介围绕 VC2008 与 MFC 的 CTabSheet 窗口选项卡示例面向需在桌面程序中实现多页面切换、希望掌握多标签对话框组织方式的开发者与学习者。示例以属性表承载多个 CTabSheet 派生页面覆盖选项卡创建、页面标题设置、页面添加、消息映射、数据交换及对话框显示等环节能帮助读者理解多页面窗口的资源布局与交互流程。压缩包共 23 个文件约 200KB包含 8 个 h 头文件、6 个 cpp 源文件以及可执行程序、解决方案、工程配置和资源脚本等便于打开解决方案后对照编译、运行和修改。已有 307 人学习下载适合作为 VC2008/MFC 多选项卡界面的入门范例、课程示例与二次开发参考对于刚接触 MFC 的读者可据此梳理对话框与子页面之间的组织关系对有一定基础的开发者也可作为改造旧版工程、整理多选项卡界面代码的参考模板。1. 一个 VC2008 老工程要加配置页我为什么选了 CTabSheet手上接过一个 VC2008 编译的 MFC 对话框工程界面是 2009 年前后的风格需求很简单把散在三个模态对话框里的参数收拢到一个窗口。第一反应是 CPropertySheet但属性表自带按钮条、默认模态和主窗口的菜单栏还打架改起来比重画还费劲。后来翻出一个 CTabSheet 类的例子工程——它把 CTabCtrl 包了一层每个 tab 页当成一个无边框子对话框来养InsertItem 管标签文字ShowWindow 管页面切换几十行就能跑起来。这类东西适合手上压着老 MFC 工程、要低成本加一个分页配置界面的人不适合想上 Ribbon、或者准备用别的框架重做界面的场景。下面从它的结构一路拆到能编译的接线步骤再讲清楚那几个必踩的坑。2. CTabSheet 的骨架CTabCtrl 派生类怎么把对话框塞进标签页2.1 CTabCtrl 只画标签条客户区是个空壳先把一个常见误解拆掉。CTabCtrl 封装的是 Win32 的 SysTabControl32它负责的只有标签条、选中态、标签的增删改以及一个客户区——这个客户区是空的你往里放什么它都不管。很多人第一次用 CTabCtrlInsertItem 之后点标签期待页面自己出现结果什么都没变就是栽在这个认知上。tab 控件本质上是个文件夹的脊背里面的纸得你自己塞。CTabSheet 的解法很朴素继承 CTabCtrl同时持有一个 CDialog 指针数组。AddPage 的时候只做两件事——把指针存进数组、调用 InsertItem 插一条标签页面的窗口创建、尺寸摆放、显示隐藏交给调用方和后续方法。头文件大致长这样// TabSheet.h #pragma once #define MAX_TAB_PAGES 16 class CTabSheet : public CTabCtrl { DECLARE_DYNAMIC(CTabSheet) public: CTabSheet(); virtual ~CTabSheet(); // 注册一个页面: 只记录指针 插入标签文字, 不创建窗口 BOOL AddPage(CDialog* pPage, LPCTSTR lpszTitle); // 切换页面: 隐藏旧页, 显示新页 void SelectPage(int nItem); // 取当前页面索引(注意不要和 CTabCtrl::GetCurSel 重名) int GetCurrentPage() const { return m_nCurrentPage; } // 按 AdjustRect 的结果重新摆放所有页面, 在 OnSize 里调用 void ResizePages(); protected: CDialog* m_pPages[MAX_TAB_PAGES]; // 只存地址, 不接管所有权 int m_nNumOfPages; int m_nCurrentPage; void ShowPage(int nItem); afx_msg void OnSize(UINT nType, int cx, int cy); DECLARE_MESSAGE_MAP() };这里有两个刻意的设计。第一m_pPages只存地址不接管所有权析构函数绝不 delete 这些指针——页面对象通常是主对话框的成员变量生命周期比 tab 控件长谁 delete 谁出事后面第 5 章会专门讲这条。第二GetCurrentPage没有沿用GetCurSel这个名字。CTabCtrl 自己就有GetCurSel派生类定义同名函数会把基类的版本完全遮住调用方以为在问控件选中了第几个标签实际拿到的是我上次记录的是第几个两个值在异常路径下会不一致。这种同名遮挡在 MFC 派生类里非常常见编译器不报错运行时表现诡异。2.2 AddPage 和 Create 的时序比代码本身更重要这个类的绝大部分故障都出在时序上。页面窗口的父窗口必须是 tab 控件本身不能是 tab 所在的主对话框而子窗口的父窗口在 Create 那一刻就定死了之后再想改只能销毁重建。所以正确的顺序是固定的四步tab 控件本体先 Create或者由 DDX_Control 绑定完成逐个 AddPage内部依赖m_hWnd有效才能 InsertItem逐个调用页面的Create(IDD_X, m_tab)父窗口传 tab 控件SelectPage(0)把首页显示出来其余页保持隐藏写成代码就是下面这样注意 AddPage 里的防御性判断// TabSheet.cpp - 注册与切换 #include stdafx.h #include TabSheet.h IMPLEMENT_DYNAMIC(CTabSheet, CTabCtrl) BEGIN_MESSAGE_MAP(CTabSheet, CTabCtrl) ON_WM_SIZE() END_MESSAGE_MAP() CTabSheet::CTabSheet() : m_nNumOfPages(0) , m_nCurrentPage(-1) { ::ZeroMemory(m_pPages, sizeof(m_pPages)); } CTabSheet::~CTabSheet() { // 只清空记录, 不 delete: 页面对象的所有权在调用方 ::ZeroMemory(m_pPages, sizeof(m_pPages)); m_nNumOfPages 0; } BOOL CTabSheet::AddPage(CDialog* pPage, LPCTSTR lpszTitle) { ASSERT(pPage ! NULL); ASSERT(m_nNumOfPages MAX_TAB_PAGES); if (pPage NULL || m_nNumOfPages MAX_TAB_PAGES) return FALSE; if (m_hWnd NULL) // tab 控件还没建, InsertItem 必然失败 return FALSE; if (pPage-GetSafeHwnd() ! NULL) // 已经建过窗口, 不要重复登记 return FALSE; m_pPages[m_nNumOfPages] pPage; InsertItem(m_nNumOfPages, lpszTitle); // nItem 即插入后的索引 m_nNumOfPages; return TRUE; }ASSERT在 Release 配置下会被编译器整条删掉所以每个 ASSERT 后面都得跟一个真正的 if 判断。这是老 MFC 代码里最常见的自欺欺人写法调试版一路断言通过发到现场 Release 版直接走空指针。AddPage 返回 FALSE 的三种情况分别对应三种现场故障——没建控件、重复登记、数组越界调用方必须检查返回值不能当它是 void。2.3 切换逻辑放在哪里别指望 ON_NOTIFY_REFLECT页面的显示切换本身没什么技术含量遍历数组选中项 ShowWindow(SW_SHOW)其余 SW_HIDEvoid CTabSheet::ShowPage(int nItem) { if (nItem 0 || nItem m_nNumOfPages) return; for (int i 0; i m_nNumOfPages; i) { CDialog* p m_pPages[i]; if (p NULL || p-GetSafeHwnd() NULL) continue; p-ShowWindow(i nItem ? SW_SHOW : SW_HIDE); } m_nCurrentPage nItem; } void CTabSheet::SelectPage(int nItem) { if (m_hWnd NULL) return; SetCurSel(nItem); // 程序设置选中项, 不会反过来触发 TCN_SELCHANGE ShowPage(nItem); }麻烦的是用户点了标签这个事件的接收方。tab 控件的 TCN_SELCHANGE 通知是发给父窗口的不是发给控件自己所以在 CTabSheet 内部写ON_NOTIFY_REFLECT往往不生效——反射消息链要求父窗口在 OnNotify 里主动调用ReflectLastMsg中间任何一环没接上就静默失败排查起来相当费时间。常见做法是在父对话框里接这条通知再转发给 CTabSheet// MainDlg.cpp - 消息映射片段 BEGIN_MESSAGE_MAP(CMainDlg, CDialog) ON_NOTIFY(TCN_SELCHANGE, IDC_TAB_MAIN, CMainDlg::OnTcnSelchangeTab) END_MESSAGE_MAP() void CMainDlg::OnTcnSelchangeTab(NMHDR* pNMHDR, LRESULT* pResult) { // 这里的 GetCurSel 是 CTabCtrl 的版本, 返回控件真实选中索引 m_tab.SelectPage(m_tab.GetCurSel()); *pResult 0; }SetCurSel由程序调用时不会触发 TCN_SELCHANGE所以 SelectPage 内部再调 SetCurSel 不会形成递归这一点可以放心。真正需要小心的是通知里的索引和m_nCurrentPage可能不同步——比如外部代码直接调了m_tab.SetCurSel(2)却没调 SelectPage界面选中了第三页但内容还停在第一页。约束只有一个所有切换都走 SelectPage别绕过它。3. 在 VC2008 里跑起来从对话框资源到可编译的 tab 例子3.1 资源准备子对话框必须改掉的六处属性这一步是新手翻车率最高的地方。用资源编辑器新建一个 Dialog 资源默认属性是弹出式、带标题栏、可见这六项如果不改运行起来就是各种怪象。逐项过一遍资源属性必须值不改的后果ControlWS_CHILDTrue页面变成独立浮窗主窗口一激活它就沉到后面Title barFalse页面顶出一条标题栏正好压住 tab 标签条StyleChild与 Control 对应资源脚本里是 WS_CHILD 标志位VisibleFalse首页之外的页面在创建时就抢着显示互相叠在一起Clip childrenTrue切页时上一页的控件边缘残留在新页上BorderNone 或 ThinDialog Frame 会画一圈立体边框和 tab 的视觉冲突改完之后的对话框资源本质就是一个没有任何窗口装饰的空盒子位置和大小在资源里随便画运行时会被 ResizePages 重新摆到 tab 客户区。资源 ID 建议命名成IDD_PAGE_BASE、IDD_PAGE_ADV这种带 PAGE 前缀的形式跟普通对话框区分开后面在 ClassWizard 里生成类的时候也不容易选错基类。每个页面配一个 CDialog 派生类用类向导生成即可基类选 CDialog不要选 CPropertyPage——CPropertyPage 是 CPropertySheet 体系的和这里的用法不搭混用会带上属性表的一堆默认消息处理。3.2 工程接线成员变量、Create 参数和 OnInitDialog 顺序主对话框里声明三个成员tab 控件对象和两个页面对象。注意 tab 控件是值成员而不是指针这样它的生命周期跟着主对话框走省掉一堆 new/delete 的心智负担。// MainDlg.h #include TabSheet.h #include PageBase.h #include PageAdv.h class CMainDlg : public CDialog { // ... 标准部分省略 protected: CTabSheet m_tab; // tab 控件本体 CPageBase m_pageBase; // 页面对象, 所有权在这里 CPageAdv m_pageAdv; afx_msg void OnTcnSelchangeTab(NMHDR* pNMHDR, LRESULT* pResult); DECLARE_MESSAGE_MAP() };然后是 OnInitDialog。这里我用了一个取巧但很实用的办法在资源里放一个静态框当占位符代码里读它的矩形来 Create tab 控件这样设计视图里看到的位置和运行时完全一致不用手写CRect(10, 10, 400, 300)这种后期改布局就要重算的魔数。BOOL CMainDlg::OnInitDialog() { CDialog::OnInitDialog(); // 1) 读占位控件的矩形, 换算成主对话框客户区坐标 CWnd* pPlace GetDlgItem(IDC_TAB_PLACEHOLDER); CRect rcTab(0, 0, 0, 0); if (pPlace ! NULL) { pPlace-GetWindowRect(rcTab); ScreenToClient(rcTab); pPlace-ShowWindow(SW_HIDE); // 占位符用完就藏起来 } // 2) 创建 tab 控件本体 if (!m_tab.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP | TCS_HOTTRACK, rcTab, this, IDC_TAB_MAIN)) { return FALSE; } // 3) 注册页面(此时只插标签) 创建页面窗口(父窗口是 tab 控件) if (!m_tab.AddPage(m_pageBase, _T(基本设置))) return FALSE; if (!m_tab.AddPage(m_pageAdv, _T(高级设置))) return FALSE; if (!m_pageBase.Create(IDD_PAGE_BASE, m_tab)) return FALSE; if (!m_pageAdv.Create(IDD_PAGE_ADV, m_tab)) return FALSE; // 4) 让 Tab 键能在页内控件之间循环 m_pageBase.ModifyStyleEx(0, WS_EX_CONTROLPARENT); m_pageAdv.ModifyStyleEx(0, WS_EX_CONTROLPARENT); // 5) 摆尺寸 显示首页 m_tab.ResizePages(); m_tab.SelectPage(0); return TRUE; }Create的第一个参数是窗口样式。WS_CHILD | WS_VISIBLE是必需项WS_TABSTOP让 tab 控件本身能被 Tab 键选中否则键盘用户永远跳不到标签条上TCS_HOTTRACK是鼠标悬停时高亮标签纯观感去掉也不影响功能。第四个参数IDC_TAB_MAIN必须是个在 resource.h 里真实存在的控件 ID因为第 2 章那条ON_NOTIFY消息映射要靠它匹配ID 对不上通知就收不到表现为点标签毫无反应。3.3 VS2008 里的三处配置不改就编不过VS2008 对新建源文件有几个固定要求第一次在这个环境里加类基本都会撞上。第一处是预编译头。新加的TabSheet.cpp第一行必须#include stdafx.h否则编译报 C1010「意外的文件尾是否忘记了向源中添加 #include stdafx.h」。这个报错信息其实写得很明白但新手常常以为是文件损坏。如果确实不想用预编译头就在该文件的属性里把使用预编译头改成不使用但一个工程里混着两种设置会拖慢整体编译不推荐。第二处是字符集。项目属性 → 配置属性 → 常规 → 字符集VS2008 新建工程默认是使用 Unicode 字符集。所有传给InsertItem、SetWindowText的字符串都要用_T(...)包起来或者直接用CString。如果工程是老代码迁移过来的 MBCS那就要反过来别把_T()和const char*混着用混用的结果是中文标签显示成一串问号或者乱码。第三处是安全 CRT 警告。VS2008 起strcpy、sprintf、tcscpy这一批函数会报 C4996 并建议改用_s后缀版本。正确的做法是换成_tcscpy_s、_stprintf_s参数多一个缓冲区大小。全局定义_CRT_SECURE_NO_WARNINGS把警告压掉能编过但等于把整个工程的安全检查关掉不建议在要交付的代码里这么干。这三处处理完按 F7 应该能直接编出一个可执行的对话框程序点标签能切页。4. 页面尺寸、焦点与 DDX让 tab 里的控件像正常对话框一样工作4.1 客户区对齐AdjustRect 才是正确算法页面窗口的尺寸不能照抄 tab 控件的GetClientRect。tab 控件的客户区是包含标签条那一圈的直接铺满标签条就会被页面盖住一半看起来像是标签只剩一条缝。Win32 给 tab 控件提供了TC_GETITEMRECT之外的另一个消息MFC 封装成CTabCtrl::AdjustRect专门做整体矩形 ↔ 内容矩形的换算。void CTabSheet::ResizePages() { if (m_hWnd NULL) return; CRect rcClient; GetClientRect(rcClient); // bLarger FALSE: 把整体矩形换算成标签条以内的内容矩形 CRect rcPage rcClient; AdjustRect(FALSE, rcPage); // 留 2px 内边距, 避免页面边框压住标签条下沿的分隔线 rcPage.InflateRect(-2, -2); for (int i 0; i m_nNumOfPages; i) { CDialog* p m_pPages[i]; if (p ! NULL p-GetSafeHwnd() ! NULL) { // 父窗口就是 tab 控件, 所以 rcPage 可以直接用, 不需要再转换坐标系 p-MoveWindow(rcPage); } } } void CTabSheet::OnSize(UINT nType, int cx, int cy) { CTabCtrl::OnSize(nType, cx, cy); ResizePages(); // 主窗口拉伸时页面跟着拉伸 }AdjustRect的第二个参数是个输入输出参数传入时要先填好整体矩形函数返回时被改写成内容矩形。第一个参数bLarger传 FALSE 求内容区传 TRUE 求整体区方向别搞反反了的结果是页面比标签区大一圈。最后的InflateRect(-2, -2)是我个人的习惯不加也能用但页面里如果放了 GroupBox边框会和 tab 的分隔线贴在一起视觉上很挤。这里还有一个坐标系问题值得单独说MoveWindow的坐标是相对父窗口客户区的而页面的父窗口就是 tab 控件所以rcPage从 tab 控件算出来的值可以直接用。如果页面 Create 时父窗口错传成了主对话框同样的代码就会把页面摆到主对话框的左上角去把整个 tab 盖住——这是第 5 章第二个坑的根源。4.2 焦点与 Tab 键序WS_EX_CONTROLPARENT 不能省页面窗口建起来之后鼠标点进去是正常的但键盘操作经常出问题在主对话框里按 Tab焦点从 tab 控件跳到某个页面内的编辑框之后再按 Tab 就跳出整个页面回到主对话框的按钮上页内剩下的控件全被跳过。原因是 MFC 的IsDialogMessage在遍历子窗口时把页面这个子对话框当成一个整体不会自动钻进去。解决办法是给每个页面加上WS_EX_CONTROLPARENT扩展风格告诉对话框管理器这是个容器进去继续遍历。第 3 章的接线代码里那两行ModifyStyleEx(0, WS_EX_CONTROLPARENT)就是干这个的放在 Create 之后调用即可改扩展风格会触发SetWindowPos窗口已创建时也能生效。另一个更隐蔽的焦点问题是隐藏页面里的控件仍然能拿到焦点。正常情况下ShowWindow(SW_HIDE)之后IsDialogMessage会跳过不可见窗口不会把焦点给它。但如果为了省一次创建开销把页面挪到屏幕外而不是真正隐藏那些控件在系统眼里依然是可见的Tab 键就会跳到一个看不见的编辑框上用户只能看到光标消失。所以切页就老老实实用 SW_HIDE别自作聪明用SetWindowPos把窗口挪到负坐标。4.3 DDX 与页面生命周期OnInitDialog 的返回值决定生死页面第一次Create时会走一遍标准对话框的初始化流程WM_INITDIALOG →OnInitDialog→DoDataExchange由CDialog::OnInitDialog内部调UpdateData(FALSE)。这意味着页面类的OnInitDialog里必须调用基类版本而且必须return TRUE。返回 FALSE 的语义是初始化失败请销毁这个窗口MFC 会立刻结束对话框tab 上那一页就变成一块空白区域点击标签什么都不显示看起来像是页面根本没创建。如果页面里有必须校验的输入项正确的做法不是 return FALSE而是在OnInitDialog里把主对话框的确定按钮禁用或者把校验挪到OnOK里做。数据回写也一样要覆盖。主对话框点确定时页面里的控件值不会自动汇总得逐个页面调UpdateData(TRUE)void CMainDlg::OnOK() { // 逐个页面把控件值刷回成员变量 if (!m_pageBase.UpdateData(TRUE)) return; // 校验失败, 停在当前页让用户改 if (!m_pageAdv.UpdateData(TRUE)) return; CDialog::OnOK(); }UpdateData(TRUE)的返回值来自DoDataExchangeDDV 校验失败时 MFC 会弹一个消息框并把焦点定位到出错的控件上然后返回 FALSE。这里直接 return 不调基类对话框就不会关闭。如果想让用户回到出问题的那一页可以在 return 之前加一句m_tab.SelectPage(索引)把对应页面切出来——这比弹完框让用户自己找要友好得多。5. 踩坑记录CTabSheet 在 VC2008 下的五类故障与排查5.1 创建阶段页面盖住标签条、变成独立浮窗现象一程序跑起来tab 标签条被一整块白色区域压住只露出最上面一行标签文字标签下方的分隔线完全看不到。原因子对话框资源没有勾 Control 属性Style 还是默认的 Overlapped。这样创建出来的窗口带 WS_POPUP 标志MoveWindow之后它按屏幕坐标摆放正好覆盖在 tab 控件上方而且因为 Z 序更高把标签条盖住了。解决资源编辑器里把 Control 设成 True、Title bar 设成 False、Style 选 Child、Border 选 None。改完重新编译页面会自动缩进到标签条以内。判断是否改对的最快方法是在Create之后打印一句pPage-GetStyle() WS_CHILD非零才算对。现象二页面不覆盖标签条了但主窗口一失去焦点再拿回来页面就跑到主窗口后面去了只剩一块空白。原因Create时样式只传了WS_CHILD没传WS_VISIBLE但资源里 Visible 又是勾上的两者打架更常见的是资源里 Visible 勾着页面在 Create 瞬间就显示此时ResizePages还没执行位置是资源里的原始坐标跑到了 tab 客户区外面。解决资源里 Visible 取消勾选所有显示统一由SelectPage控制。第一页的显示也必须走SelectPage(0)不要指望 Create 时自动可见。5.2 运行期切换残影与退出断言现象三点第二个标签第一页的控件没有完全消失边缘上留着几条残留的边框鼠标划过才刷新掉。原因页面 Create 时父窗口传成了主对话框的this而不是m_tab。坐标基准错了页面被摆到主对话框客户区的某处和 tab 控件只有部分重叠重叠区之外的部分在切页时不会被 tab 控件的重绘区域覆盖于是留下残影。解决Create(IDD_X, m_tab)父窗口必须是 tab 控件。已经写错的话没法靠SetParent补救——子窗口换父窗口后坐标系、Z 序、消息路由全乱正确做法是销毁重建。现象四关闭主对话框时Debug 版弹出一个 Debug Assertion Failed位置在 wincore.cpp 里提示某个 CWnd 对象的窗口句柄无效。原因CTabSheet 的析构函数里写了delete m_pPages[i]。而这些页面对象是主对话框的成员变量存放在栈上delete 一个非堆对象会直接破坏内存即使页面是用 new 创建的也会被析构两次。解决析构函数里只清空指针数组绝不 delete。所有权规则要写在头文件注释里让后来改代码的人一眼看到。这条属于血泪经验一个 delete 能让人查一下午。5.3 编译与字符集C1010、乱码和只在 Release 出现的空指针现象五新加的 TabSheet.cpp 一编译就报 C1010提示意外的文件尾是否忘记了向源中添加 #include stdafx.h或者编过了但中文标签显示成问号。原因前者是新文件没加预编译头后者是工程用 Unicode 字符集而标签字符串写成了基本设置这种窄字符字面量隐式转换时按当前代码页解释转成宽字符就丢了内容。解决源文件第一行补#include stdafx.h所有字符串用_T(基本设置)包裹源文件另存为带 BOM 的 UTF-8 或者 GB2312两者选一个并保持全工程一致。另外提醒一句 Release 专属的坑把所有判断逻辑写在ASSERT后面、靠断言保证条件成立Release 下断言被编译掉后面直接解引用空指针。断言是给开发者看的返回值才是给程序用的两者不能互相替代。6. 从能跑到好维护把页面注册收进一张表并做上线前自检页面数量一旦超过三个OnInitDialog里那一串 AddPage、Create、ModifyStyleEx 就开始变成复制粘贴的地狱加一页要改四处漏一处就是空白页或者没有标签。我一般会把页面描述收进一张局部表用一个循环统一处理加页只加一行。6.1 用描述表驱动注册与创建void CMainDlg::BuildTabPages() { // 加一页只需要在这里加一行 加一个成员变量 一个对话框资源 struct PAGE_DESC { CDialog* pPage; UINT nIDD; LPCTSTR lpszTitle; }; const int nCount 3; PAGE_DESC pages[nCount] { { m_pageBase, IDD_PAGE_BASE, _T(基本设置) }, { m_pageAdv, IDD_PAGE_ADV, _T(高级设置) }, { m_pageNet, IDD_PAGE_NET, _T(网络参数) }, }; for (int i 0; i nCount; i) { // 注册失败就跳过这一页, 不要让后面的 Create 把窗口建出来变孤儿 if (!m_tab.AddPage(pages[i].pPage, pages[i].lpszTitle)) continue; if (!pages[i].pPage-Create(pages[i].nIDD, m_tab)) continue; pages[i].pPage-ModifyStyleEx(0, WS_EX_CONTROLPARENT); } }这张表不能写成文件级静态数组因为里面存的是成员对象地址静态初始化时this还不存在编译器会直接拒绝。放在成员函数里当局部数组是安全的每次调用重建一次的开销可以忽略。循环里的两个continue是刻意保留的注册失败时如果继续 Create会建出一个没有标签对应、永远显示不出来的窗口退出时还得额外清理不如直接跳过。6.2 上线前走一遍这张自检表界面类的问题大多不是逻辑错而是资源属性和创建时序没对上。我一般在交版本前按下面这张表过一遍每条都能在一分钟内验证完检查项判断方法不通过的表现tab 控件已创建m_tab.GetSafeHwnd()非空AddPage 返回 FALSE一个标签都没有页面父窗口正确pPage-GetParent() m_tab页面跑到左上角或盖住标签条页面尺寸走 AdjustRect拉大主窗口看页面是否跟随页面尺寸固定不变右下角露出空白页面对象未被 delete析构函数里搜 delete退出时断言或入口点崩溃字符串统一用 _T()全局搜索开头的标签文字中文显示成 ?? 或乱码OnInitDialog 返回 TRUE逐页检查返回值某一页点开是空白区域切换只走 SelectPage搜索本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →