MFC控件字体颜色设置:OnCtlColor与CFont避坑指南
做 MFC 界面开发的人大概率都经历过这样一幕对话框上摆了一排静态文本和一两个编辑框想把标题文字换成深一点的蓝、字号放大两号再把输入框的背景改成浅灰。代码加进去编译零警告运行也正常界面上却什么都不变——字体还是那副老宋体颜色还是黑压压一片。查了半天发现 OnCtlColor 也写了SetFont 也调了就是没反应。这类代码写了但界面纹丝不动的问题是 MFC 控件样式设置里最典型的一类也是新手卡住最久的一类。这篇就来把 MFC 里给控件设置文本字体、字号、文字颜色和背景色的完整套路捋清楚从 WM_CTLCOLOR 消息族的归属机制讲起再落到 Static、Edit、Button、ListCtrl、TreeCtrl 这些具体控件的差异最后给出一套可以直接抄进项目的封装方案。不管你是刚开始接触 MFC 的窗口程序还是写了几年但一直被字体颜色搞得头疼的老手下面的内容应该都能对上号。1. WM_CTLCOLOR 消息族颜色设置半生效的根源很多人第一次写颜色设置是在对话框类里加个ON_WM_CTLCOLOR然后在OnCtlColor里一通SetTextColor结果要么全变要么全不变要么一部分生效一部分失效。问题基本都出在对这套消息机制理解得不够透。这一块必须先讲清楚后面所有坑都能归到这几条规则上。1.1 七条 CTLCOLOR 消息各管一摊Windows 并不是用一个消息统一处理所有控件的颜色而是按控件类型拆成了七条独立消息MFC 把它们统一收口到了OnCtlColor的nCtlColor参数里消息MFC 常量主要覆盖的控件WM_CTLCOLORDLGCTLCOLOR_DLG对话框自身的背景填充WM_CTLCOLORMSGBOXCTLCOLOR_MSGBOX系统消息框WM_CTLCOLORSTATICCTLCOLOR_STATIC静态文本、GroupBox、只读编辑框、禁用状态的编辑框和按钮WM_CTLCOLOREDITCTLCOLOR_EDIT可编辑状态的编辑框WM_CTLCOLORLISTBOXCTLCOLOR_LISTBOX列表框、组合框展开后的下拉列表WM_CTLCOLORBTNCTLCOLOR_BTN普通按钮但启用视觉样式后基本被忽略WM_CTLCOLORSCROLLBARCTLCOLOR_SCROLLBAR独立滚动条控件这张表是后面所有排查的基础。最容易被忽略的是 CTLCOLOR_STATIC 的覆盖范围——它不只是静态文本GroupBox、只读编辑框、被EnableWindow(FALSE)禁用的编辑框全都走这条路。所以你在 CTLCOLOR_EDIT 分支里写得再漂亮一旦编辑框设了ES_READONLY代码就全部作废。这个坑我在实际项目里踩过不止一次注释里写一句只读框走 STATIC 分支能省下后来人半小时。1.2 返回值必须是画刷句柄不是颜色值OnCtlColor的原型是afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);返回值类型是HBRUSH不是COLORREF也不是BOOL。这个设计的意思是系统需要你提供一个画刷用来把控件的背景区域刷一遍至于文字颜色和背景填充模式是通过传入的pDC去设置的。所以一个完整的设置动作其实是三件事pDC-SetTextColor(...)决定文字颜色pDC-SetBkColor(...)决定文字背后那块底色前提是背景模式不透明return 画刷句柄决定整个控件矩形怎么被填充如果你的SetBkMode设成了TRANSPARENT那么SetBkColor设定的颜色其实用不上文字直接压在已有背景上这时候返回什么画刷就很关键。返回一个实色画刷文字周围会被这个颜色填满返回NULL_BRUSH系统就不填充保留下层已经绘制好的内容。静态文本放在有渐变背景或位图背景的对话框上时想让文字浮在背景上就必须走NULL_BRUSH这条路。HBRUSH CMyDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { HBRUSH hbr CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor); if (pWnd-GetDlgCtrlID() IDC_STATIC_TITLE) { pDC-SetTextColor(RGB(0x1F, 0x4E, 0x79)); pDC-SetBkMode(TRANSPARENT); return (HBRUSH)::GetStockObject(NULL_BRUSH); } return hbr; }注意最后那个return hbr;不能省。如果你在所有分支里都直接 return 自己的画刷等于把 MFC 默认的行为全部覆盖掉了某些控件比如带边框的静态文本可能出现边框残留或者闪烁稳妥的做法是先把默认返回值存下来改完自己关心的控件后再原样还回去。1.3 消息发给谁决定了你的代码有没有机会执行这是整个机制里最隐蔽的一条CTLCOLOR 消息发给控件的直接父窗口不是发给对话框本身也不是发给顶层窗口。绝大多数情况下这两者是同一个所以看不出来区别。但只要界面结构稍微复杂一点问题立刻暴露。典型的三个场景属性页CPropertyPage消息发给属性页窗口所以重写必须写在属性页类里写在主对话框里一点用没有。Tab 控件上的子对话框把 Tab 页做成子对话框CDialog的Child属性设为 true嵌入那么页内控件的 CTLCOLOR 全部发给这个子对话框。很多人把颜色代码写在主对话框上然后纳闷为什么只有主对话框上的控件生效。自定义容器面板如果你继承CWnd自己做了个面板当容器控件挂在这个面板上消息就发给面板而不是对话框。至于 GroupBox它只是视觉上的分组框架并不是控件的父窗口把编辑框拖到 GroupBox 框里父窗口仍然是对话框。这一点跟直觉不太一样但确实如此也正因如此 GroupBox 内的控件颜色设置通常不出问题。1.4 别忘了先调用基类的默认实现MFC 生成向导里给出的OnCtlColor骨架第一行就是调基类HBRUSH CMyDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { HBRUSH hbr CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor); // 你的定制代码 return hbr; }这一行不是摆设。基类实现会处理一些默认行为比如给控件返回正确的系统画刷、处理视觉样式的边缘情况。把它删掉自己从头返回看起来更干净实际上会引入一批说不清的小毛病。我在一个老项目里见过有人把这行删了结果禁用状态的按钮背景变成纯黑查了很久才定位到这儿。2. CFont 的生命周期字体设置失效的头号原因字体设置这部分代码本身不难难的是对象生命周期管理。我见过的字体设置失败案例里十有八九跟CFont对象的存活时间有关而不是参数写错了。2.1 CreateFont 的十三个参数里真正要管的只有四个CFont::CreateFont的参数长得吓人十三个但日常真正需要调的只有下面这几个m_fontTitle.CreateFont( -MulDiv(12, GetDpiY(), 72), // 1. 高度负值表示字符高度 0, // 2. 宽度0 表示按高度自动算 0, 0, // 3. 4. 倾斜角和基线角度写 0 FW_BOLD, // 5. 字重400 常规 / 700 加粗 FALSE, FALSE, FALSE, // 6. 7. 8. 斜体、下划线、删除线 DEFAULT_CHARSET, // 9. 字符集中文环境用 DEFAULT 最稳 OUT_DEFAULT_PRECIS, // 10. 输出精度 CLIP_DEFAULT_PRECIS, // 11. 裁剪精度 CLEARTYPE_QUALITY, // 12. 渲染质量中文建议 ClearType DEFAULT_PITCH | FF_DONTCARE, // 13. 字距和字体族 _T(Microsoft YaHei)); // 字体名第一个参数nHeight的正负号含义一定要搞清楚负值表示字符本身的逻辑高度正值表示包含内部行距的单元格高度。同样是 -16 和 16前者看起来明显更大。微软文档里解释过绝大多数情况下应该传负值因为字体设计者给出的尺寸是按字符本身算的。很多人直接写-16觉得挺好但那个 16 是从哪来的没人说得清跟实际字号没对应关系。2.2 局部变量 CFont 是字体失效的头号元凶看这段代码void CMyDlg::SetupFont() { CFont font; font.CreateFont(-16, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE, _T(Microsoft YaHei)); GetDlgItem(IDC_STATIC_TITLE)-SetFont(font); } // font 在这里析构DeleteObject 被调用SetupFont一返回font析构函数执行内部的HFONT句柄被DeleteObject释放掉。控件那边还记着这个句柄但句柄已经无效了。接下来的绘制请求会失败系统直接回退到默认字体。表现出来就是程序刚启动时可能一瞬间是雅黑然后闪一下就变成宋体或者在某些机器上干脆一直是宋体让人以为是字体没装。正确的做法是把CFont提升为对话框类的成员变量让它跟对话框同生共死// 头文件里 class CMyDlg : public CDialogEx { // ... private: CFont m_fontTitle; CFont m_fontBody; CBrush m_brInput; };成员变量的析构顺序刚好在对话框窗口销毁之后控件不再需要这个字体DeleteObject也就安全了。如果一定要用指针就得在OnDestroy或者PostNcDestroy里手工 delete漏掉一次就是 GDI 对象泄漏。GDI 对象泄漏在任务管理器里看不到但任务管理器切换到详细信息标签加上GDI 对象列就能观察数值只涨不跌就说明有泄漏。2.3 CreatePointFont 的写法更适合按号调字如果你习惯按 Word 里12 号字的思维调尺寸CreatePointFont更顺手m_fontBody.CreatePointFont(100, _T(Microsoft YaHei)); // 10 点 m_fontTitle.CreatePointFont(140, _T(Microsoft YaHei)); // 14 点第一个参数是十分之一点100 就是 10 点。它内部会自动折算成逻辑高度还会用屏幕上实际 DPI 换算所以在 96 DPI 和 144 DPI 下显示的物理大小是一致的——这一点比手工填-16靠谱得多。手里没有特别需求的话我一般优先用CreatePointFont只在需要指定字重加粗和渲染质量的时候才退回CreateFont。另外提一句lfCharSet。中文字体写成CHINESEBIG5_CHARSET会在简体系统上直接乱码写成DEFAULT_CHARSET让系统按区域设置去挑是最省事的。热搜词里出现的字体冲突大多指的是同一段文本里混用了不同字符集的字体界面上表现为部分字符变成方框或问号根源就出在这个参数上。2.4 SetFont 之后控件尺寸不会自动跟着变这是个隐蔽的坑。SetFont只换字体不调整控件大小。原来 9 号字用的静态文本高度可能是 16 像素换成 14 号字之后实际需要 24 像素但控件矩形还是 16 像素文字下半截直接被裁掉。解决方法有两个在OnInitDialog里SetFont之后调用GetDlgItem(id)-GetWindowRect()拿到当前矩形用CDC::GetTextExtent量出新字体的尺寸再SetWindowPos重新摆位或者更省事把静态文本的高度在资源编辑器里留足比如统一留到 24 像素小字号也占这个高度视觉上稍微空一点但不会出问题。我一般用第一种配合一个通用函数来做具体实现放在第 4 节。3. 按控件逐类拆解Static、Edit、Button 的处理差异上一节讲的是通用规则这一节进入实操层面。同样是设文字颜色和背景色不同控件类型的路径完全不同混着写必然出错。3.1 静态文本透明与不透明是两种效果静态文本最常见也最容易做过头。三种典型需求的写法需求一在对话框灰底上换个文字颜色。这种最简单只设文字色背景保持默认if (pWnd-GetDlgCtrlID() IDC_STATIC_HINT) { pDC-SetTextColor(RGB(0x88, 0x88, 0x88)); return hbr; // 返回默认画刷背景仍由系统填充 }需求二放在有背景图的对话框上文字要透出背景。必须设透明模式并返回空画刷pDC-SetTextColor(RGB(0xFF, 0xFF, 0xFF)); pDC-SetBkMode(TRANSPARENT); return (HBRUSH)::GetStockObject(NULL_BRUSH);需求三给文本块一块实色底。返回一个实色画刷同时让背景模式保持不透明pDC-SetTextColor(RGB(0x2C, 0x3E, 0x50)); pDC-SetBkColor(RGB(0xEC, 0xF0, 0xF1)); return (HBRUSH)m_brHint.GetSafeHandle();第三种情况下m_brHint必须是长期存在的画刷对象画刷句柄在OnCtlColor返回后要被系统继续使用局部画刷同样是析构即失效。还有个细节静态文本的矩形通常是紧贴文字的如果文字颜色和背景色对比度不够看起来会像没生效。建议先把颜色调得夸张一点比如纯红配纯黄验证代码通路确认没问题再换成正式的配色。这个调试习惯帮我省了不少来回折腾的时间。3.2 编辑框编辑态、只读态、禁用态走三条路编辑框是差异最多的控件。普通可编辑状态走CTLCOLOR_EDIT。设了ES_READONLY的只读状态走CTLCOLOR_STATIC。被EnableWindow(FALSE)禁用的状态也走CTLCOLOR_STATIC。只读和禁用都落到 STATIC 分支但它们需要区分对待——禁用态一般想用灰字灰底表示不可操作只读态则希望看起来跟普通编辑框差不多。区分方法是先判断控件类型再判断启用状态if (nCtlColor CTLCOLOR_STATIC) { if (pWnd-IsKindOf(RUNTIME_CLASS(CEdit))) { if (!pWnd-IsWindowEnabled()) { pDC-SetTextColor(RGB(0xA0, 0xA0, 0xA0)); pDC-SetBkColor(RGB(0xF5, 0xF5, 0xF5)); return (HBRUSH)m_brDisabled.GetSafeHandle(); } pDC-SetTextColor(RGB(0x33, 0x33, 0x33)); pDC-SetBkColor(RGB(0xFF, 0xFF, 0xFF)); return (HBRUSH)m_brReadOnly.GetSafeHandle(); } }IsKindOf(RUNTIME_CLASS(CEdit))需要 MFC 的运行时类型信息这个机制要求控件是通过DDX_Control绑定的成员变量或者在运行时做过SubclassDlgItem。如果只是资源里的一个 IDIsKindOf可能返回 false这时候可以用GetClassName拿类名比较。另外编辑框的边框颜色不受OnCtlColor控制WS_BORDER的边框是系统画的。想改边框色只能去掉WS_BORDER自己在父窗口的OnPaint里画一圈矩形或者用WS_EX_CLIENTEDGE换成凹陷边框。这是很多人卡住的地方颜色都调好了就是边上那圈黑线去不掉。3.3 按钮WM_CTLCOLORBTN 为什么形同虚设启用视觉样式也就是程序带 manifest 或者#pragma comment(linker, ...)引入 comctl32 v6之后普通CButton会交给主题引擎绘制WM_CTLCOLORBTN发过去被主题引擎忽略掉了。这就是为什么在CTLCOLOR_BTN分支里写SetTextColor完全没反应。字体设置倒是完全生效的SetFont对按钮有效所以按钮改字号、改字体名没问题。只有文字颜色和背景色不行。要做按钮的颜色定制有三条路路线一CMFCButton。这是 MFC Feature PackVS2008 SP1 起带的增强按钮直接用就行CMFCButton* pBtn (CMFCButton*)GetDlgItem(IDC_BTN_SUBMIT); pBtn-SetFaceColor(RGB(0x1F, 0x4E, 0x79), TRUE); pBtn-SetTextColor(RGB(0xFF, 0xFF, 0xFF));前提是资源里按钮的类要换成CMFCButton做法是在对话框头文件里声明一个CMFCButton成员用DDX_Control绑定或者直接SubclassDlgItem。SetFaceColor的第二个参数传 TRUE 会立即重绘。路线二自绘按钮。给按钮加BS_OWNERDRAW样式重写父窗口的DrawItemvoid CMyDlg::DrawItem(LPDRAWITEMSTRUCT lpDIS) { CDC dc; dc.Attach(lpDIS-hDC); CRect rc lpDIS-rcItem; bool bPressed (lpDIS-itemState ODS_SELECTED) ! 0; dc.FillSolidRect(rc, bPressed ? RGB(0x16, 0x3A, 0x5C) : RGB(0x1F, 0x4E, 0x79)); dc.SetTextColor(RGB(0xFF, 0xFF, 0xFF)); dc.SetBkMode(TRANSPARENT); dc.SelectObject(m_fontBody); dc.DrawText(_T(提交), rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE); dc.Detach(); }自绘的麻烦之处在于要自己处理按下、悬停、焦点、禁用这些状态工作量比想象中大。项目里按钮不多还行多了建议直接上路线一。路线三放弃改按钮改成自绘的静态文本加点击处理。有些设计感比较强的界面干脆不用标准按钮全用静态文本或自绘窗口模拟灵活度最高代价是键盘交互和可访问性要自己补。3.4 列表和树控件绕开 CTLCOLOR 走专用接口CListCtrl、CTreeCtrl这些通用控件不走 CTLCOLOR 体系它们有自己的一套接口控件接口作用范围CListCtrlSetTextColor所有行的文字色CListCtrlSetTextBkColor所有行的文字底色CListCtrlSetBkColor列表空白区域底色CTreeCtrlSetTextColor节点文字色CTreeCtrlSetBkColor树的空白区域底色CTreeCtrlSetLineColor节点之间的连接线颜色这几个接口设置的是全局值想让不同行显示不同颜色就得用NM_CUSTOMDRAW通知void CMyListCtrl::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) { LPNMLVCUSTOMDRAW pCD reinterpret_castLPNMLVCUSTOMDRAW(pNMHDR); *pResult CDRF_DODEFAULT; switch (pCD-nmcd.dwDrawStage) { case CDDS_PREPAINT: *pResult CDRF_NOTIFYITEMDRAW; break; case CDDS_ITEMPREPAINT: *pResult CDRF_NOTIFYSUBITEMDRAW; break; case CDDS_ITEMPREPAINT | CDDS_SUBITEM: if (pCD-iSubItem 0) { pCD-clrText RGB(0xC0, 0x39, 0x2B); pCD-clrTextBk RGB(0xFF, 0xF5, 0xF5); *pResult CDRF_NEWFONT; } else { *pResult CDRF_DODEFAULT; } break; } }这里的iSubItem就是列索引可以按列做不同的配色。要按行按数据内容配色用pCD-nmcd.dwItemSpec拿行号或者用GetItemText拿到文本再判断。需要提醒的是列表控件的表头Header是独立的控件SetTextColor管不到它表头颜色得单独处理一般是给 Header 加HDF_OWNERDRAW然后自绘或者在NM_CUSTOMDRAW里拦截CDDS_ITEMPREPAINT并判断这个通知是不是来自 Header。这块内容单独展开能写一篇这里先略过。4. 一套能同时管字体、颜色、背景的封装方案上面讲的是原理和单点写法。真到项目里几十个控件一个一个写if判断代码很快就没法维护了。我一般会在项目里放一套轻量的样式管理用一个映射表把控件 ID 和样式对应起来。4.1 表驱动把样式定义从判断逻辑里抽出来先定义一个简单的结构体描述控件样式struct CtrlStyle { COLORREF crText; // 文字颜色 COLORREF crBack; // 背景颜色 bool bTransparent; // 是否透明背景 bool bHasText; bool bHasBack; }; std::mapUINT, CtrlStyle m_mapStyle;在OnInitDialog里集中配置CtrlStyle stTitle { RGB(0x1F, 0x4E, 0x79), 0, true, true, false }; CtrlStyle stHint { RGB(0x88, 0x88, 0x88), 0, true, true, false }; CtrlStyle stInput { RGB(0x2C, 0x3E, 0x50), RGB(0xF5, 0xF7, 0xFA), false, true, true }; m_mapStyle[IDC_STATIC_TITLE] stTitle; m_mapStyle[IDC_STATIC_HINT] stHint; m_mapStyle[IDC_EDIT_INPUT] stInput;然后OnCtlColor变成纯粹的查表HBRUSH CMyDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { HBRUSH hbr CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor); auto it m_mapStyle.find(pWnd-GetDlgCtrlID()); if (it ! m_mapStyle.end()) { const CtrlStyle st it-second; if (st.bHasText) pDC-SetTextColor(st.crText); if (st.bTransparent) pDC-SetBkMode(TRANSPARENT); else if (st.bHasBack) pDC-SetBkColor(st.crBack); if (st.bTransparent) return (HBRUSH)::GetStockObject(NULL_BRUSH); if (st.bHasBack) return (HBRUSH)GetBrush(st.crBack); } return hbr; }这样改配色只需要动配置那几行不用在if里翻来翻去。项目里控件一多这套结构的价值立刻体现出来。4.2 画刷缓存用多少颜色建多少个别每次都建上面用的GetBrush(COLORREF)是个小工具内部用 map 缓存已经创建过的画刷避免每次重绘都CreateSolidBrush新建一个HBRUSH CMyDlg::GetBrush(COLORREF cr) { auto it m_mapBrush.find(cr); if (it ! m_mapBrush.end()) return (HBRUSH)it-second.GetSafeHandle(); CBrush br m_mapBrush[cr]; br.CreateSolidBrush(cr); return (HBRUSH)br.GetSafeHandle(); }这里m_mapBrush是std::mapCOLORREF, CBrush成员变量随对话框一起销毁。之所以强调缓存是因为OnCtlColor的调用频率跟重绘挂钩鼠标划过、窗口尺寸变化、控件内容更新都可能触发。如果每次都新建画刷而不释放GDI 对象数目几秒钟就能从几十冲到几千系统 GDI 对象上限一到整个界面就开始画不出来了。几十种颜色以内缓存方案的收益很明显。颜色种类特别多比如按数据值动态算色的时候缓存反而会撑爆内存这时候应该预先建立一套固定色阶把颜色映射到最近的一档。4.3 字体统一注册按角色命名字体也一样不要按控件 ID 命名而应该按角色命名。一个典型界面里字体角色就那么几种m_fontTitle.CreatePointFont(140, _T(Microsoft YaHei)); m_fontBody.CreatePointFont(100, _T(Microsoft YaHei)); m_fontMono.CreateFont(-MulDiv(9, GetDpiY(), 72), 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, FIXED_PITCH | FF_MODERN, _T(Consolas));标题字体、正文字体、等宽字体用于日志、代码、编号显示。用FIXED_PITCH | FF_MODERN指定等宽族Windows 会自动在 Consolas、Courier New 里挑一个可用的比硬编码一个名字更耐移植。然后统一应用GetDlgItem(IDC_STATIC_TITLE)-SetFont(m_fontTitle); GetDlgItem(IDC_STATIC_HINT)-SetFont(m_fontBody); GetDlgItem(IDC_EDIT_INPUT)-SetFont(m_fontBody); GetDlgItem(IDC_LIST_LOG)-SetFont(m_fontMono);一个细节CListCtrl的SetFont生效后行高会跟着字体高度自动调整但已经插入的数据不会重排视觉上可能有点错位插入数据之前就把字体设好可以避免这个问题。5. 高 DPI、动态改字和闪烁运行期才暴露的四个坑界面在 96 DPI 的开发机上看着挺好一到 150% 缩放或者 4K 屏上就露馅。这类问题跟字体、颜色都有关而且只在特定环境下出现排查起来最费劲。5.1 按 DPI 换算字号别写死逻辑高度前面代码里出现的MulDiv(12, GetDpiY(), 72)就是为了解决缩放问题。MultDiv内部按 64 位乘再除避免中间结果溢出。GetDpiY的实现int CMyDlg::GetDpiY() { CClientDC dc(this); return dc.GetDeviceCaps(LOGPIXELSY); }在 96 DPI 下MulDiv(12, 96, 72)得到 16也就是 12 点字对应的逻辑高度 16在 144 DPI 下变成 24物理尺寸保持不变。如果直接写死-16在 144 DPI 屏幕上字会显得只有原来的三分之二大看起来像是缩放没生效。另外Win10 之后还有GetDpiForWindow可以直接拿窗口 DPI比从 DC 拿更准。如果程序声明了 Per-Monitor V2 的 DPI 感知级别窗口在不同显示器之间拖动时 DPI 会变需要响应WM_DPICHANGED在里面重新创建字体并重新应用到控件afx_msg LRESULT CMyDlg::OnDpiChanged(WPARAM wParam, LPARAM lParam);处理这个的时候要注意不仅要重建字体还得按新 DPI 调整控件位置和大小否则字体变大了但控件还是旧尺寸文字被裁。这一整套东西跟 MFC 的对话框布局机制配合起来有点绕如果项目对多屏缩放没硬要求声明成 System DPI Aware 也能用代价是跨屏拖动时会有一次模糊重绘。5.2 改完颜色不刷新多半是没触发重绘在OnCtlColor之外的地方改颜色比如响应某个按钮点击后把编辑框的文字变红m_editResult.SetTextColor(RGB(0xC0, 0x39, 0x2B)); // CEdit 没有这个方法编辑框没有SetTextColor这个操作只能通过OnCtlColor里的状态判断来做。做法是给对话框类加个成员标志在颜色需要变化时改标志并触发重绘m_bError true; GetDlgItem(IDC_EDIT_RESULT)-Invalidate();然后在OnCtlColor的对应分支里根据m_bError选颜色。只调Invalidate不够的话加上UpdateWindow()强制立即重绘。有个坑要避开不要在OnCtlColor里调用Invalidate或者SetWindowText。前者会导致无限重绘循环后者会触发一次新的绘制请求同样可能绕回OnCtlColor。需要更新显示的时候把动作放在定时器或者PostMessage的自定义消息里脱离当前的绘制流程。5.3 减少闪烁几个立竿见影的措施改完字体和背景之后如果发现窗口拖动时闪得厉害可以试这几个手段。给对话框加WS_CLIPCHILDREN。这个样式让父窗口绘制时跳过被子窗口覆盖的区域重绘量能降一大截。在OnInitDialog里改ModifyStyle(0, WS_CLIPCHILDREN);静态文本加SS_NOTIFY之外背景和文字一起设。只改文字色不改背景模式系统仍会先用默认背景刷一遍再画文字两次填充之间就有闪的机会。透明模式配合NULL_BRUSH能省掉一次填充。自绘背景时用双缓冲。如果对话框背景不是纯色需要在OnEraseBkgnd里画渐变或位图那就必须双缓冲先在内存 DC 里画完再一次性 BitBlt 到屏幕BOOL CMyDlg::OnEraseBkgnd(CDC* pDC) { CRect rc; GetClientRect(rc); CDC dcMem; dcMem.CreateCompatibleDC(pDC); CBitmap bmp; bmp.CreateCompatibleBitmap(pDC, rc.Width(), rc.Height()); CBitmap* pOld dcMem.SelectObject(bmp); // 在这里画背景 dcMem.FillSolidRect(rc, RGB(0xF0, 0xF4, 0xF8)); pDC-BitBlt(0, 0, rc.Width(), rc.Height(), dcMem, 0, 0, SRCCOPY); dcMem.SelectObject(pOld); return TRUE; }注意返回TRUE表示背景已经处理完了系统不用再擦一遍。这一步做错返回 FALSE系统会再擦一次闪烁反而更严重。5.4 系统主题和高对比度会覆盖你的设置有两个外部因素会让你精心调的颜色失效系统启用了高对比度主题Windows 会强制用系统色覆盖程序里的配色SetTextColor设置的深蓝色可能变成纯黑或者纯白。程序里可以用SystemParametersInfo(SPI_GETHIGHCONTRAST, ...)检测到这个状态检测到之后切回一套系统色配色保证可读性。远程桌面或虚拟机环境某些渲染模式会忽略 ClearType 设置字体看起来毛边比较重。把lfQuality改成ANTIALIASED_QUALITY有时会有改善代价是中文小字号下不如 ClearType 清晰。这些都是环境相关的本地跑不出问题不代表客户机器上没问题如果有条件最好在 125%、150% 两种缩放和两台不同 DPI 的显示器上各过一遍界面。6. 排查清单颜色字体不生效时依次看这几项最后把我这些年攒下来的一份排查清单列出来遇到代码写了没效果的时候按顺序往下看基本能定位到问题。现象可能原因确认方法所有控件颜色都没变OnCtlColor没在消息映射里注册或消息发给了别的窗口在函数入口打断点看有没有进来部分控件变色部分不变控件父窗口不是当前对话框属性页、Tab 子对话框用 Spy 看控件的 Parent 句柄只读编辑框颜色不生效只读状态走 CTLCOLOR_STATIC代码写在 EDIT 分支打印nCtlColor的值确认按钮文字颜色不生效视觉样式下 CTLCOLOR_BTN 被忽略换成 CMFCButton 或自绘验证字体启动时对、闪一下变回默认CFont 是局部变量出了作用域被析构把 CFont 改成类成员变量中文显示成方块或问号字符集参数选错或系统没装对应字体把lfCharSet改成 DEFAULT_CHARSET文字下半截被裁掉换字体后控件高度不够量一下字体的实际文本高度对比控件高度界面卡顿、越用越慢每次重绘都新建画刷/字体GDI 对象泄漏任务管理器加 GDI 对象列观察实际排查中我建议在OnCtlColor的开头加一行临时的TRACE把控件 ID 和nCtlColor打出来TRACE(_T(CtlColor ctrl%u type%u\n), pWnd-GetDlgCtrlID(), nCtlColor);输出一片空白就说明函数根本没被调用是消息归属的问题输出了但类型值跟你预期的不一致就是分支写错了。这一行临时日志花十秒钟加往往比盯着代码看十分钟管用。还有个小习惯值得养成调试配色的时候先在OnCtlColor里把所有控件都刷成刺眼的品红配黄确认整条链路通了再逐个改成正式配色。这样能把代码路径问题和配色审美问题彻底分开少走很多弯路。我个人在项目里的做法是把这套样式表配置放在OnInitDialog的最后一段前面先把所有控件的字体设好后面再统一注册颜色规则两者都在同一个函数里完成方便对照。等到界面需要换肤或者适配深色模式的时候只需要重新生成一份样式表再调一次应用函数就行不用动OnCtlColor里的逻辑。这个结构在后期改版的时候省的时间比前期多花的那点功夫多得多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →