简介面向MFC初学者及需要在VS2010中完善对话框界面的开发者这份工程实例演示在CDialog对话框中嵌入并驱动状态栏的完整做法拖放StatusBar控件并拆分多栏在OnInitDialog中Create与SetIndicators用SetTimer、OnTimer配合CTime刷新左栏时间以SetPaneText更新提示文本可直接编译运行。压缩包共47个文件、约29.05MB以h/cpp源码、rc与res资源、vcxproj与sln工程配置为主另含manifest、图标及调试中间文件解压后可用VS2010打开StatusBar.sln对照修改。已有738人学习。读者可掌握状态栏创建、分栏宽度调整、定时刷新与点击响应的实现思路并迁移到自己的MFC项目中。1. 从「拖个控件上去」开始说起给一个 dialog 加状态栏最省事的想法是在资源编辑器里拖一个 Status Control 上去编译运行然后发现它老老实实待在对话框模板里画的那个位置既不贴底也不跟着窗口缩放。MFC 里 dialog 状态栏这件事的难点从来不在创建而在于对话框没有框架窗口那套控制条布局CStatusBar 是给 CFrameWnd 准备的直接丢进对话框里调 Create 大概率返回失败于是只能退回通用控件自己算矩形、自己处理 WM_SIZE、自己管字体生命周期。下面把这条链路拆开走一遍先讲清楚三条可选路线为什么只剩下一条再落到具体代码、分区宽度、刷新时机最后把几个能让人加班到半夜的坑摆出来。2. 三条路线选一条状态栏为什么绕不开手算矩形动手之前先把路线定死比写代码更重要。对话框程序加状态栏市面上能见到的做法基本就三种选错了后面每一步都在还债。2.1 CStatusBar 在 CDialog 里为什么 Create 就失败CStatusBar 的继承链是 CStatusBar → CControlBar → CWnd。CControlBar 这一层专门为框架窗口服务它的定位依赖 CFrameWnd::RecalcLayout()父窗口必须能把控制条塞进自己的 m_listControlBars 链表里统一布局。CDialog 不是 CFrameWnd没有 RecalcLayout也没有那套停靠逻辑所以 CStatusBar::Create 在对话框里基本走不通调试版会直接断言失败。就算你绕过创建这一关后面还有一道坎CStatusBar 的自动菜单提示依赖 AFX_IDW_STATUS_BAR 这个固定 ID 和 ON_UPDATE_COMMAND_UI 的命令路由而对话框没有菜单栏、没有命令更新机制那套「鼠标划过菜单项、状态栏自动显示提示」的功能照样不会生效。结论很直接在对话框场景下CStatusBar 提供的能力你一个都用不上它带来的约束你一个都躲不开。2.2 路线对比CStatusBarCtrl、资源里的 Status Control、自绘伪状态栏真正可用的三条路线差异集中在「谁负责布局」这一件事上。路线 A用 CStatusBarCtrl 手动 Create。它是通用状态栏控件MSCTLS_STATUSCLASS的 MFC 封装本质就是一个子窗口创建时指定父窗口为对话框坐标自己算。SB_SETPARTS、SB_SETTEXT、SB_SETMINHEIGHT 这些消息原样可用不受控制条框架的任何约束。布局责任在你手上也意味着完全可控。路线 B在对话框资源里放一个 Status Control用 DDX_Control 绑定成员。看起来省事实际上位置照样要手算——资源模板里记录的是设计时的坐标和尺寸对话框一缩放它就脱节。而且它会被当成普通控件参与 Tab 顺序得手动去掉 WS_TABSTOP类向导对这种控件也不一定自动生成绑定代码多数时候还是要自己补 DDX_Control。路线 C自绘伪状态栏。用一个带凹边框的 CStatic 当底板上面摆若干 CStatic 做分区或者干脆在 OnPaint 里用 DrawText 自己画。这种做法唯一的理由是需要皮肤化、渐变背景、图标加文字混排这类通用控件给不了的效果代价是所有文本刷新、分区计算、重绘失效全得自己维护。维度CStatusBarCtrl 手动创建资源里的 Status Control自绘伪状态栏创建成本中需要写 Create 和布局代码低但绑定代码要手补高重绘逻辑自写布局责任自己算完全可控自己算且受模板坐标干扰自己算且要处理无效区分区能力SB_SETPARTS 原生支持同左需自行实现换肤能力弱只能改背景色和字体弱强高 DPI 改造改分区宽度即可同左另需处理模板坐标全部自算适用场景常规工具类对话框一次性小工具有 UI 规范要求的成品2.3 工程约束谁负责布局谁负责避让选定路线 A 之后还有两个约束必须在设计阶段就想清楚否则代码写到一半会推倒重来。第一个是布局归属。对话框本身没有布局管理器MFC 也不会替你重排子窗口状态栏贴底这件事必须由你在 OnSize 里完成。常见做法是OnSize 里先 GetClientRect 拿到客户区再把状态栏 MoveWindow 到 (0, bottom - nBarHeight, right, bottom)。这一句看着简单但它决定了后面所有坐标计算的基准。第二个是控件避让。状态栏是浮在对话框客户区上的子窗口不是把客户区分成两半的布局元素。也就是说如果对话框底部原本有「确定」「取消」按钮状态栏会直接盖在它们上面。解决办法只有两个要么在 OnInitDialog 里先把对话框整体高度撑大一个 nBarHeight让按钮区整体上移要么在 OnSize 里手动把底部控件往上挪。前者改动小后者适合需要自由缩放的对话框。提示对话框允许缩放时务必把「撑大高度」和「OnSize 重排」两件事一起做只做前者会在用户拖动边框后重新露出被盖住的按钮。3. 落地实现在 CDialogEx 派生类里挂一条能吃消息的状态栏路线定了接下来是最容易出错的创建阶段。顺序错了症状往往不是报错而是「看起来正常但哪里不对」排查成本极高。3.1 成员声明、控件 ID 与消息映射先补头文件。三个成员状态栏对象、字体对象、高度缓存。字体必须是成员变量不能是局部变量原因在第 5 章会展开。// MainDlg.h class CMainDlg : public CDialogEx { DECLARE_DYNAMIC(CMainDlg) public: CMainDlg(CWnd* pParent nullptr); enum { IDD IDD_MAIN_DIALOG }; protected: virtual BOOL OnInitDialog(); virtual void DoDataExchange(CDataExchange* pDX); CStatusBarCtrl m_wndStatusBar; // 状态栏控件对象 CFont m_fontStatus; // 必须做成成员生命周期要覆盖控件 int m_nBarHeight; // 按字体度量算出的高度缓存起来 int m_nDpi; // 创建时记录的 DPI分区宽度按它缩放 afx_msg void OnSize(UINT nType, int cx, int cy); afx_msg void OnTimer(UINT_PTR nIDEvent); DECLARE_MESSAGE_MAP() };控件 ID 建议在 resource.h 里手工加一个#define IDC_STATUSBAR 2000之类避开系统保留区间。不要图省事复用 IDC_STATIC那会导致 GetDlgItem 拿到错误的句柄。消息映射只需要两条ON_WM_SIZE 和 ON_WM_TIMER。BEGIN_MESSAGE_MAP(CMainDlg, CDialogEx) ON_WM_SIZE() ON_WM_TIMER() END_MESSAGE_MAP()注意这里没有 ON_WM_CREATE状态栏的创建放在 OnInitDialog 里做因为在 OnCreate 阶段对话框客户区尺寸还没最终确定此时算出来的位置在后续 SetWindowPos 之后会失效。3.2 OnInitDialog 里的五步创建顺序创建顺序是这套实现里最讲究的部分顺序错了会出现「高度不生效」「文字被裁」「分区数量对不上」这类玄学问题。我一般固定成五步建字体 → 建控件 → 设字体 → 设最小高度 → 设分区。BOOL CMainDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 第一步先建字体。状态栏的行高完全由它决定所以必须在算高度之前就绪 m_nDpi ::GetDeviceCaps(::GetDC(nullptr), LOGPIXELSX); // 96 为基准 int nFontHeight -MulDiv(9, m_nDpi, 72); // 9pt 转逻辑单位 m_fontStatus.CreateFont(nFontHeight, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_SWISS, _T(Microsoft YaHei)); // 第二步创建状态栏控件父窗口是对话框本身 if (!m_wndStatusBar.Create(WS_CHILD | WS_VISIBLE | SBARS_SIZEGRIP, CRect(0, 0, 0, 0), this, IDC_STATUSBAR)) { TRACE(_T(status bar create failed, err%d\n), ::GetLastError()); return TRUE; } // 第三步把字体交给控件bRedraw 传 TRUE 立即生效 m_wndStatusBar.SetFont(m_fontStatus, TRUE); // 第四步按字体度量算高度再告诉控件最小高度 m_nBarHeight CalcStatusBarHeight(); m_wndStatusBar.SendMessage(SB_SETMINHEIGHT, m_nBarHeight, 0); // 第五步划三个分区最后一栏用 -1 吃掉剩余宽度 int parts[3] { MulDiv(120, m_nDpi, 96), MulDiv(260, m_nDpi, 96), -1 }; if (!m_wndStatusBar.SetParts(3, parts)) TRACE(_T(SetParts failed\n)); m_wndStatusBar.SetPaneText(0, _T(就绪)); m_wndStatusBar.SetPaneText(1, _T(行 0)); m_wndStatusBar.SetPaneText(2, _T(列 0)); // 首次定位。此时客户区已经是模板设计尺寸OnSize 可能先于这一步触发过 RepositionStatusBar(); // 撑大对话框给状态栏腾出空间避免盖住底部按钮 CRect rcWnd, rcClient; GetWindowRect(rcWnd); GetClientRect(rcClient); int nNonClient rcWnd.Height() - rcClient.Height(); SetWindowPos(nullptr, 0, 0, rcWnd.Width(), rcWnd.Height() m_nBarHeight, SWP_NOMOVE | SWP_NOZORDER); return TRUE; }五步各自的含义建字体排第一因为它决定了后面所有跟高度相关的数值。CreateFont 的 nHeight 传负值表示字符高度不含内部行距传正值表示字符单元格高度状态栏这种场景用负值更精确。字体名可以换成系统默认的 UI 字体中文环境下雅黑的行高比较稳。建控件排第二样式里 WS_CHILD | WS_VISIBLE 是必须的SBARS_SIZEGRIP 会在右下角画一个缩放把手——只有在对话框允许缩放时才加不允许缩放就把它去掉否则用户会拖一个拖不动的把手。SetFont 排第三bRedraw 传 TRUE 会立刻重绘。这一步必须在 SetMinHeight 和 SetParts 之前因为通用控件内部会用当前字体重算一切。SB_SETMINHEIGHT 排第四它是唯一能让状态栏长高的方式。通用状态栏有自己的默认高度约等于系统状态字体的行高不显式设置的话你在 OnSize 里 MoveWindow 到更高也只会看到一条空白文字照样贴着顶。SetParts 排第五必须在 SetPaneText 之前否则对多栏的写入不会生效。3.3 高度不用写死按字体度量算 SB_SETMINHEIGHT高度写死成 22 或 24 是最常见的偷懒做法代价是换字体、换 DPI、换系统主题之后文字被裁掉一半。正确做法是用 CDC 拿到字体的 TEXTMETRIC。int CMainDlg::CalcStatusBarHeight() { CClientDC dc(this); // 借用对话框的 DC CFont* pOldFont dc.SelectObject(m_fontStatus); TEXTMETRIC tm { 0 }; dc.GetTextMetrics(tm); dc.SelectObject(pOldFont); // 一定要还原否则 DC 析构会断言 // tmHeight 是字符高度上下各留 3px 内边距加 1px 分隔线 int nPad MulDiv(3, m_nDpi, 96); return tm.tmHeight nPad * 2; }说明几点。CClientDC 只是借来测量不需要绑定到状态栏控件SelectObject 之后必须还原旧字体不然调试版会弹 GDI 对象泄漏断言——这是新手最常撞的一个报警。nPad 用 MulDiv 按 DPI 缩放高 DPI 下 3px 太小实际按比例放大后才不会显得局促。算出来的高度还要同时用于两处SB_SETMINHEIGHT 和 OnSize 里的 MoveWindow 顶边坐标。两处不一致就会出现「状态栏比预期高出一截但文字贴在顶部」的怪现象。3.4 OnSize 重排与底部控件避让OnSize 是整个流程里最容易被忽略的地方。对话框在创建过程中会触发 WM_SIZE而那时候状态栏还没建出来成员句柄是空的直接 MoveWindow 就是一次空指针崩溃。void CMainDlg::OnSize(UINT nType, int cx, int cy) { CDialogEx::OnSize(nType, cx, cy); // 关键防护WM_SIZE 可能早于 OnInitDialog 触发 if (!::IsWindow(m_wndStatusBar.GetSafeHwnd())) return; RepositionStatusBar(); // 把底部按钮组上移避免被状态栏压住 CWnd* pOk GetDlgItem(IDOK); if (pOk ::IsWindow(pOk-GetSafeHwnd())) { CRect rcBtn; pOk-GetWindowRect(rcBtn); ScreenToClient(rcBtn); int nBtnBottom cy - m_nBarHeight - MulDiv(8, m_nDpi, 96); pOk-MoveWindow(rcBtn.left, nBtnBottom - rcBtn.Height(), rcBtn.Width(), rcBtn.Height()); } } void CMainDlg::RepositionStatusBar() { CRect rcClient; GetClientRect(rcClient); CRect rcBar(0, rcClient.bottom - m_nBarHeight, rcClient.right, rcClient.bottom); m_wndStatusBar.MoveWindow(rcBar); // 宽度变了必须重发 WM_SIZE让控件内部重新按分区重排 m_wndStatusBar.SendMessage(WM_SIZE, 0, 0); }三个参数说明nType 是缩放类型一般不用管cx、cy 是新的客户区宽高直接可用不需要再 GetClientRect但为了 RepositionStatusBar 能被别处复用我还是让它在内部取一次。有一个细节值得单说MoveWindow 之后补发 WM_SIZE。状态栏控件收到 WM_SIZE 才会按现有分区边界重新对齐右端那一栏不补发的话窗口拉宽之后最后一栏的右边界会停在旧位置右侧露出一条空白。这个坑在只做模态小工具时不明显一旦对话框可缩放就会露馅。按钮避让那段代码用「按当前高度反推顶边」的写法重复调用不会累积偏移比直接- m_nBarHeight的写法安全。4. 分区与刷新SB_SETPARTS 的坐标语义和三个刷新入口状态栏能显示只是第一步能不能稳定显示、显示得对不对全看分区和刷新这两块。4.1 parts 数组怎么填绝对右边界与 -1 的含义SetParts 的第二个参数是一个 int 数组语义是「每个分区的右边界相对状态栏客户区的 x 坐标」不是每个分区的宽度。这是最容易理解错的一点。// 三栏第 0 栏占 0~120第 1 栏占 120~260第 2 栏占 260~最右 int parts[3] { 120, 260, -1 }; m_wndStatusBar.SetParts(3, parts); // 两栏左栏 0~200右栏吃掉剩余全部 int parts2[2] { 200, -1 }; m_wndStatusBar.SetParts(2, parts2);最后一栏必须传 -1它表示「一直延伸到状态栏右边界」。不写 -1 而写一个固定值状态栏右侧就会留出一块没有归属的空白区域视觉上像是少画了一截。如果中间某栏你希望它自适应可以在每次 OnSize 里重新计算 parts 再 SetParts但频繁调用会引起闪烁非必要不做。分区数上限受系统限制常规场景三到四栏足够。分区索引从 0 开始SetPaneText 的索引必须小于 SetParts 里声明的数量越界时函数返回 FALSE 但不抛异常静默失败——这就是为什么很多人「设了文本但界面上没变」。4.2 SetPaneInfo 的样式位与宽度覆盖问题SetParts 只能控制边界分区的边框样式要靠 SetPaneInfo 补。// 让第 1 栏文字凹陷显示第 2 栏去掉边框 m_wndStatusBar.SetPaneInfo(1, 1, SBT_POPOUT, 140); m_wndStatusBar.SetPaneInfo(2, 2, SBT_NOBORDERS, -1);签名是 SetPaneInfo(nIndex, nID, nStyle, nWidth)。nID 是分区的字符串资源 ID纯文本场景随便填一个非零值即可。nStyle 取 SBT_NOBORDERS无边框、SBT_POPOUT凹陷、SBT_OWNERDRAW自绘的组合。nWidth 特别注意传具体值会覆盖 SetParts 算出来的边界传 -1 表示「保持当前右边界不动」。这条规则的副作用是如果先调 SetPaneInfo 再调 SetParts前者的宽度会被后者覆盖反过来则是 SetPaneInfo 覆盖 SetParts。我一般固定成 SetParts 在前、SetPaneInfo 在后需要改样式时只动后半段边界逻辑不被污染。4.3 刷新入口定时器、工作线程、UpdateData 的边界状态栏上的内容无非三类来源界面即时变化光标行列、周期性数据时间、连接状态、后台任务进度。三类来源对应三种刷新入口用错会出问题。界面即时变化最直接在消息响应函数里直接 SetPaneText 就行。唯一要注意的是别在高频消息比如鼠标移动里无脑写先比较一下内容是否相同再更新。void CMainDlg::UpdateCursorPane(int nLine, int nCol) { CString strNew; strNew.Format(_T(行 %d 列 %d), nLine, nCol); CString strOld; m_wndStatusBar.GetPaneText(0, strOld); // 索引按实际布局调整 if (strOld strNew) return; // 内容没变就不刷避免闪烁 m_wndStatusBar.SetPaneText(0, strNew); }周期性数据用 SetTimer。周期别设太短状态栏不是进度动画500ms 到 1000ms 的粒度完全够用再密就是白白吃 CPU。SetTimer(1, 1000, nullptr); // OnInitDialog 里启动 void CMainDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1) { CString strTime CTime::GetCurrentTime().Format(_T(%H:%M:%S)); m_wndStatusBar.SetPaneText(2, strTime); } CDialogEx::OnTimer(nIDEvent); // 别忘了交还给基类 }后台任务的进度是最容易翻车的一类。工作线程不能直接碰控件跨线程调用 SetPaneText 在调试版会触发断言发布版行为未定义。正确姿势是工作线程 PostMessage 一个自定义消息回 UI 线程在消息响应里再更新。// 工作线程侧 ::PostMessage(hMainDlg, WM_APP 10, nPercent, 0); // 消息映射 ON_MESSAGE(WM_APP 10, CMainDlg::OnProgress) LRESULT CMainDlg::OnProgress(WPARAM wParam, LPARAM /*lParam*/) { CString str; str.Format(_T(处理中 %d%%), (int)wParam); m_wndStatusBar.SetPaneText(1, str); return 0; }WM_APP N 是自定义消息的安全区间别用 WM_USER 以下的号段容易和控件内部消息撞车。PostMessage 不阻塞线程比 SendMessage 更合适——SendMessage 在工作线程里等 UI 线程处理UI 线程一旦卡住就是死锁。5. 避坑排查状态栏九成翻车都在这五处以下五条是按我实际踩过的频率排的每一条都写清楚现象、原因和解决方式遇到问题时可以顺着症状对号入座。5.1 创建与初始化阶段的三条坑一程序一启动就崩在 OnSize 里。现象调试版断点停在 OnSize 的第一行 MoveWindow或者直接弹访问冲突。发布版偶尔能跑但窗口一缩放出错。原因对话框创建时会先发 WM_SIZE此时 OnInitDialog 还没执行m_wndStatusBar 的窗口句柄是 NULLMoveWindow 内部解引用空指针。解决在 OnSize 开头加if (!::IsWindow(m_wndStatusBar.GetSafeHwnd())) return;。不要用 m_hWnd 判断要用控件自己的句柄因为成员对象本身在构造时就存在只有句柄能说明它是否已经创建。坑二高度设了但状态栏还是原来那么高文字被裁掉一半。现象SB_SETMINHEIGHT 明明传了 26界面上状态栏仍是默认的约 20px中文文字下半截看不见。原因有两种。一是 SetMinHeight 调用发生在 SetFont 之前控件用系统默认字体重算了一遍高度把你后面设的值冲掉了。二是 MoveWindow 的矩形高度小于 SetMinHeight 的值控件的实际绘制区被父窗口裁剪。解决固定顺序为创建 → SetFont → CalcStatusBarHeight → SB_SETMINHEIGHT → MoveWindow且 MoveWindow 的高度必须等于 m_nBarHeight。改完顺序后如果还不对用 Spy 类工具看一下控件的实际窗口矩形基本上一眼就能定位。坑三CFont 用了局部变量运行一会儿状态栏文字变成系统字体或者乱码。现象程序刚启动字体正常弹过一次模态对话框或者切换几次界面后状态栏文字字体变了严重时文字重叠。原因SetFont 只保存 HFONT 句柄不接管字体对象的生命周期。局部 CFont 在函数返回时析构底层 HFONT 被销毁控件手里的句柄变成悬空的系统降级到默认字体继续画。解决CFont 一定做成对话框类的成员保证生命周期覆盖控件。如果确实需要动态换字体先 SetFont(nullptr) 断开旧句柄再删除旧字体对象顺序反了同样会崩。5.2 布局与显示阶段的两条坑四底部按钮被状态栏压住点不着。现象对话框底部的「确定」「取消」按钮只露出上半截点击下半截没反应。原因状态栏是浮在客户区上的子窗口Z 序在普通控件之后创建天然盖在按钮上面。对话框本身的客户区并没有因为状态栏而缩小。解决两种做法二选一。要么在 OnInitDialog 里把对话框整体高度增加 m_nBarHeight本文第 3 章的做法让按钮在设计时就位于状态栏上方要么在 OnSize 里显式把底部控件逐个上移。前者简单后者适合需要频繁缩放窗口的场景。两种做法都不要和SetWindowPos的 SWP_NOSIZE 混用容易把调整抹掉。坑五SetPaneText 返回 TRUE但界面上文字没变。现象调试时看返回值是 TRUE断点也进去了状态栏上显示的还是旧内容只有第 0 栏偶尔会变。原因SetPaneText 的 nIndex 是分区索引必须小于 SetParts 声明的分区数。如果 SetParts 只设了两栏去写索引 2 会静默失败。另一种情况是 SetParts 根本没调用成功——parts 数组的元素不是严格递增接口会返回 FALSE 但很多人不检查返回值。解决在初始化之后加一次断言ASSERT(m_wndStatusBar.GetParts(3, parts) 3);把实际分区数和边界打印出来核对。写索引之前先确认这个索引在你声明的范围内比事后查半天强。注意调试这类问题时不要只看代码用打印把 GetParts 的实际返回值、GetPaneText 读回的内容都打出来两分钟就能分清是「没写进去」还是「写进去了没重绘」。6. 高 DPI 与自检把写死的像素换成 MulDiv前面所有代码里我一直在用MulDiv(值, m_nDpi, 96)这不是凑数。状态栏是最容易暴露 DPI 问题的控件之一因为它只有一行文字栏宽差 20px 就会把「行 128 列 36」这种稍微长一点的内容挤断。具体做法是维护一份 96 DPI 下的设计值运行时统一换算。分区宽度、内边距、按钮避让间距、字体高度全部走这条路径任何一处直接写裸像素都会在 150% 缩放的机器上出问题。// 设计基准96 DPI 下第一栏 120px第二栏右边界 260px int ScaleDpi(int nValueAt96) const { return MulDiv(nValueAt96, m_nDpi, 96); } void CMainDlg::RebuildParts() { int parts[3] { ScaleDpi(120), ScaleDpi(260), -1 }; m_wndStatusBar.SetParts(3, parts); m_wndStatusBar.SetPaneInfo(1, 1, SBT_POPOUT, -1); // 宽度传 -1 保住边界 }m_nDpi 的取法优先用::GetDpiForWindow(m_hWnd)拿不到就退回GetDeviceCaps(::GetDC(nullptr), LOGPIXELSX)。后者在进程 DPI 感知模式下拿到的是系统 DPI在多显示器混合缩放的场景下不够准但作为兜底够用。窗口在不同 DPI 显示器之间拖动时会收到 WM_DPICHANGED如果想做得更细可以在响应里更新 m_nDpi、重建字体、重算高度再 RebuildParts 一次。对大多数内部工具来说启动时取一次 DPI 已经能覆盖九成场景跨屏拖动的情况少见不做也不算错。下面这张自检表我每次交付前都会过一遍五条全绿基本就不会有现场返工。检查项判断方法不通过的表现创建顺序断点看 SetFont 是否在 SetMinHeight 之前高度不生效、文字被裁句柄判空OnSize 首行是否有 IsWindow 检查启动即崩分区数量GetParts 返回值与索引最大值核对SetPaneText 静默失败字体生命周期CFont 是否为类成员文字字体漂移或乱码DPI 换算搜索代码里是否有裸像素常量高缩放下分区错位最后说一个我自己的教训。早期做这类对话框工具我习惯把高度写成#define STATUS_HEIGHT 22因为当时的开发机是 100% 缩放怎么看都是对的。发给用笔记本的同事之后反馈说「状态栏上的字只剩上半截」我远程看了半天代码没找出问题最后发现是他系统缩放 150%字体行高早就超过 22 了。从那以后凡是涉及控件几何的数值我一律不写常量全部走「先算字体度量、再按 DPI 换算」这条路宁可多写十行代码也不给现场留一个只能靠猜的 bug。希望帮到你。本文还有配套的精品资源点击获取