ARTICLE DETAIL

资讯详情

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

MFC控件字体颜色背景设置与高分屏适配实战

MFC控件字体颜色背景设置与高分屏适配实战 在MFC里改一个控件的文本字体、字号、颜色和背景几乎每个做Windows桌面开发的人都躲不过去。这事儿听着简单真动手才发现坑不少字体设了没变化、颜色改了没反应、背景刷上去出现了灰色方块、切到高分屏上文字糊成一团。这篇东西就是把我这些年反复折腾MFC控件视觉样式攒下来的经验整理一遍从消息机制讲起把字体创建、字号换算、文本色与背景色的处理路径、按钮和列表这些特殊控件的玩法、闪烁与DPI适配一路写到常见问题的排查表。不管你是刚开始摸MFC的学生还是维护着一套十年老代码的工程师都能从里面找到能直接拿过去用的代码片段和判断依据。全文用的都是标准Win32消息和GDI接口不依赖第三方界面库编译环境覆盖VS2010到VS2022的各个MFC版本。1. MFC里改字体颜色为什么会失效1.1 消息分发路径决定了谁说了算很多人第一反应是去找控件的属性面板或者调SetFont、SetTextColor这类看起来最直观的接口。实际上一半的失效案例都栽在同一个认知误区上MFC的控件是独立的子窗口它画自己的时候会向父窗口要一堆参数父窗口不回话子控件就用系统默认值。你在子控件对象上调SetTextColor对某些控件比如静态文本、分组框压根没用因为这些控件根本没有缓存文字颜色的成员变量它们在每次重绘时都靠WM_CTLCOLOR消息向父窗口现要。这套机制的设计初衷是让整个对话框的颜色风格能统一管理。父窗口收到WM_CTLCOLORSTATIC、WM_CTLCOLOREDIT、WM_CTLCOLORBTN、WM_CTLCOLORLISTBOX等消息后可以从CDC*参数里拿到目标控件的绘制上下文调用SetTextColor、SetBkColor、SetBkMode最后返回一个HBRUSH告诉系统用哪个刷子填背景。MFC把这套东西包装成了OnCtlColor虚函数签名是HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)。这里有个容易被忽略的细节nCtlColor参数给的是控件类别不是具体控件。一个对话框上可能有十几个静态文本它们全都发CTLCOLOR_STATIC。要让标题用蓝色、说明文字用灰色只能靠pWnd-GetDlgCtrlID()或者pWnd GetDlgItem(...)去区分。判断完再决定给什么颜色、返回什么刷子。提示OnCtlColor在对话框每次重绘时都会被调用别在里面做CreateFont、CreateSolidBrush这类创建GDI对象的操作否则句柄数会一路涨上去跑一天下来进程直接卡死。另外一个高频误区是把WM_CTLCOLOR和WM_DRAWITEM搞混。前者管的是文本色和背景刷后者管的是自绘控件的整体绘制。按钮、列表这类控件在标准样式下会走WM_CTLCOLORBTN但如果你给按钮加了BS_OWNERDRAW消息就变成WM_DRAWITEM了OnCtlColor里返回什么都不管用。1.2 CFont对象活不过函数栈这个坑字体设置失效的另一个大头是GDI对象生命周期。新手最常见的写法是这样void CMyDlg::SetTitleFont() { CFont font; font.CreatePointFont(120, _T(微软雅黑)); GetDlgItem(IDC_STATIC_TITLE)-SetFont(font); }函数一返回font析构底层HFONT被DeleteObject释放控件手里攥着一个已经失效的句柄。此时控件的文字要么退回系统默认字体要么在某些Windows版本上直接显示成方块。这个问题的隐蔽之处在于它有时候看起来是正常的因为句柄被删除后一段时间内系统的句柄表还没复用那个槽位等你再创建别的GDI对象把它占了界面才开始花掉。正确做法是把CFont声明成对话框类的成员变量在OnInitDialog或对话框构造完成后创建随对话框一起销毁// MyDlg.h class CMyDlg : public CDialogEx { // ... protected: CFont m_fontTitle; CFont m_fontBody; CBrush m_brBk; // ... };销毁顺序也要注意。对话框的OnDestroy里子控件会先被销毁之后才是成员变量析构这个顺序是安全的。但如果你在成员变量析构之前手动调了GetDlgItem(...)-SetFont(nullptr)之类的操作就得确认控件还活着。还有一种情况是字体设了但看着没变。这通常发生在CreatePointFont传的字体名在当前系统上不存在比如写了微软雅黑但跑在精简版系统上。GDI找不到就静默替换成系统默认字体不报错也不提示。解决办法是用EnumFontFamiliesEx先枚举一遍确认字体存在或者干脆挑宋体黑体这种长期存在的字体打底。2. 字体与字号的设置细节2.1 三套创建字体的接口怎么选MFC和Win32一共给了三层字体创建接口各有各的适用场景。最底层的是::CreateFont十几个参数lfHeight、lfWeight、lfItalic、lfCharSet、lfQuality全都要自己填。优点是控制粒度最细缺点是参数写错一个就可能出怪问题。中间层是CFont::CreateFontIndirect直接吃一个LOGFONT结构体适合把字体配置抽出来做成结构体或者从配置文件读。最上层是CFont::CreatePointFont和CreateFontIndirect的便捷包装只要求给点数、字体名和可选DC。实际项目里我的习惯是界面上一两个特殊字体用CreatePointFont图省事需要批量配置、支持用户自定义主题的统一走LOGFONTCreateFontIndirect。这样字体信息可以序列化可以跟皮肤文件一起走。LOGFONT lf { 0 }; lf.lfHeight -MulDiv(10, GetDeviceCaps(hdcScreen, LOGPIXELSY), 72); lf.lfWeight FW_NORMAL; lf.lfCharSet DEFAULT_CHARSET; lf.lfQuality CLEARTYPE_QUALITY; lf.lfPitchAndFamily DEFAULT_PITCH | FF_DONTCARE; _tcscpy_s(lf.lfFaceName, _T(微软雅黑)); m_fontBody.CreateFontIndirect(lf);lfQuality这个字段值得单独说。早期代码里常见DEFAULT_QUALITY字体边缘发毛改成CLEARTYPE_QUALITY后文字明显锐利代价是绘制时多一点点开销现代机器上完全可以忽略。如果你的程序还要跑在XP之类的老系统上可以考虑ANTIALIASED_QUALITY作为折中。2.2 负高度、DPI与字号换算lfHeight的正负号是新手最容易翻车的点。正值表示字体的字符单元格高度包含内部行距 ascenderdescender负值表示字符本身的逻辑高度。绝大多数设计工具、网页里说的字号对应的都是负值语义。UI程序里想把视觉字号调准一律用负值。换算公式是pixel -MulDiv(pointSize, dpiY, 72)其中dpiY通常取屏幕的LOGPIXELSY。96 DPI下10磅对应-MulDiv(10, 96, 72) -13.33取整成-13。12磅对应-16。很多人直接写-12、-14这种数字在96 DPI下和10磅、11磅差不多看着能用一换到144 DPI的笔记本上就全乱套了。磅值96 DPI (100%)120 DPI (125%)144 DPI (150%)192 DPI (200%)9-12-15-18-2410-13-17-20-2712-16-20-24-3214-19-23-28-3716-21-27-32-43这张表里的数字是用公式MulDiv(point, dpi, 72)再加四舍五入算出来的直接抄进代码没问题。但更稳的做法是运行时算别写死。CreatePointFont内部其实也是这套公式但它的DC参数如果传NULL用的是屏幕DC在多显示器、不同缩放组合的场景下会取错DPI。所以高帽子程序里我建议要么显式传一个和控件同DPI的DC要么自己算lfHeight。还有一个坑LOGFONT.lfFaceName的长度限制是LF_FACESIZE也就是32个字符含结尾0。中文字体名一般够用但如果用户在主题配置里填了特别长的名字会静默截断。用_tcsncpy_s而不是_tcscpy。2.3 一次性给整页控件换字体对话框上控件多了以后一个个SetFont太啰嗦而且容易漏。我通常写一个递归函数从对话框往下遍历子控件按控件ID或控件类型批量设置void CMyDlg::ApplyFontToChildren(CWnd* pParent, CFont* pFont) { if (pParent nullptr || pFont nullptr) return; CWnd* pChild pParent-GetWindow(GW_CHILD); while (pChild ! nullptr) { pChild-SetFont(pFont); ApplyFontToChildren(pChild, pFont); // 处理嵌套容器 pChild pChild-GetWindow(GW_HWNDNEXT); } }这里传的是CFont*而不是值也不要在函数里创建临时CFont。同时注意SetFont的第二个参数bRedraw默认是TRUE会立刻触发重绘控件多的时候会有一次可见的闪烁。批量操作时可以先传FALSE全部设完之后对对话框调一次Invalidate视觉上干净很多。另外不是所有控件都吃SetFont。编辑框、按钮、静态文本、列表控件、树控件、组合框这些都支持但组合框有个细节SetFont只设编辑框部分和下拉列表部分下拉列表如果还没创建没点开过可能设不上。稳妥做法是在OnInitDialog里先调一次GetDlgItem(IDC_COMBO)-SetFont(m_fontBody)再配合WM_SETFONT消息直接发::SendMessage(hCombo, WM_SETFONT, (WPARAM)(HFONT)m_fontBody.GetSafeHandle(), MAKELPARAM(FALSE, 0));WM_SETFONT是Win32原生消息绕开MFC的封装对下拉列表这类延迟创建子窗口的控件更可靠。3. 文本颜色与背景色的实战处理3.1 WM_CTLCOLOR家族的分工MFC把WM_CTLCOLOR系列消息映射成了不同的虚函数但实际处理都是重写OnCtlColor一个函数。不同控件类别传进来的nCtlColor值决定了默认行为。CTLCOLOR_DLG是对话框自己的背景CTLCOLOR_STATIC管静态文本、分组框、只读编辑框CTLCOLOR_EDIT管可编辑的编辑框CTLCOLOR_LISTBOX管列表框CTLCOLOR_BTN管按钮的文字CTLCOLOR_SCROLLBAR管滚动条。关键判断逻辑写起来大概是这样HBRUSH CMyDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { HBRUSH hbr CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor); switch (nCtlColor) { case CTLCOLOR_STATIC: pDC-SetTextColor(RGB(60, 60, 60)); pDC-SetBkMode(TRANSPARENT); hbr (HBRUSH)m_brBk.GetSafeHandle(); break; case CTLCOLOR_EDIT: pDC-SetTextColor(RGB(20, 20, 20)); pDC-SetBkColor(RGB(255, 252, 235)); hbr (HBRUSH)m_brEditBk.GetSafeHandle(); break; } return hbr; }注意SetBkMode(TRANSPARENT)只对文本背景生效意思是文字背后的底色不填充让父窗口先画好背景再叠文字。但它不改变控件本身的窗口背景静态文本的窗口背景仍然由返回的刷子决定。这就是为什么很多人设了TRANSPARENT之后文字周围还是一块灰色——返回的刷子还是系统默认的按钮面色。要让静态文本真正融进对话框背景得把返回的刷子换成对话框自己的背景刷。这样静态文本窗口的擦除用的就是同一张图或者同一个纯色视觉上连成一片。注意CTLCOLOR_STATIC也会被只读编辑框触发。如果你的编辑框在只读和可编辑之间切换得在判断里加pWnd-GetStyle() ES_READONLY来区分否则切换状态时颜色会突然跳变。3.2 静态文本真透明背景怎么做上面说的返回父窗口背景刷只解决了纯色背景。如果对话框背景是一张图片或者渐变静态文本窗口擦除时还是会留下一个矩形色块。这时候需要三步走。第一步给静态文本加WS_EX_TRANSPARENT扩展样式让它在绘制时不擦除自己的背景CWnd* pStatic GetDlgItem(IDC_STATIC_TITLE); pStatic-ModifyStyleEx(0, WS_EX_TRANSPARENT, SWP_FRAMECHANGED);第二步在OnCtlColor里给CTLCOLOR_STATIC返回NULL_BRUSH也就是GetStockObject(NULL_BRUSH)意思是不用刷子填背景并且把SetBkMode设成TRANSPARENT。第三步确保父窗口在OnEraseBkgnd或OnPaint里把背景画完整。因为静态文本已经不擦自己的背景了那块区域上显示的就是父窗口之前画的内容。BOOL CMyDlg::OnEraseBkgnd(CDC* pDC) { CRect rc; GetClientRect(rc); if (m_bmpBk.GetSafeHandle() ! nullptr) { CDC memDC; memDC.CreateCompatibleDC(pDC); CBitmap* pOld memDC.SelectObject(m_bmpBk); pDC-StretchBlt(0, 0, rc.Width(), rc.Height(), memDC, 0, 0, m_nBmpW, m_nBmpH, SRCCOPY); memDC.SelectObject(pOld); return TRUE; } return CDialogEx::OnEraseBkgnd(pDC); }这三步缺一不可。只做第一步会出现残影只做第二步会出现黑底或者花屏只做第三步背景图会被静态文本的灰块挡住。我在项目里踩过最典型的一次是漏了第一步结果窗口拖动时文字后面拖出一条长长的灰色尾巴看起来像显卡坏了。3.3 各控件颜色响应差异对照表不同控件对文本色和背景色的响应程度差别很大这张表是我实际测出来的结论列出来省得大家一个个试。控件类型文本色是否响应背景色是否响应备注静态文本 Static是是需返回刷子走 CTLCOLOR_STATIC分组框 GroupBox部分部分边框颜色难改建议自绘编辑框 Edit可编辑是是走 CTLCOLOR_EDIT编辑框 Edit只读是是走 CTLCOLOR_STATIC按钮 Button标准是有限否背景基本不可控需自绘复选框/单选钮是否文字色可改勾选框颜色不可控列表框 ListBox是是走 CTLCOLOR_LISTBOX组合框 ComboBox是是编辑区与下拉列表需分别处理列表控件 ListCtrl否否必须走 NM_CUSTOMDRAW树控件 TreeCtrl否否必须走 NM_CUSTOMDRAW进度条 Progress部分是前景色需发 PBM_SETBARCOLOR表格里否的那些控件OnCtlColor里的设置会被吞掉因为它们的绘制根本不走这条路径。列表和树是Common Controls里的公共控件在comctl32.dll里自己画只能通过NM_CUSTOMDRAW通知去干预。进度条则是靠PBM_SETBARCOLOR、PBM_SETBKCOLOR这两个特定消息。4. 按钮、列表、树这些硬骨头的处理方法4.1 按钮从BS_OWNERDRAW到CMFCButton标准按钮的WM_CTLCOLORBTN只影响按钮上的文字颜色而且从Windows XP引入视觉样式之后连文字颜色都经常被主题覆盖设了也不生效。想要完全掌控按钮的外观只有自绘一条路。最传统的做法是给按钮加BS_OWNERDRAW样式然后在父窗口响应WM_DRAWITEM。MFC里可以派生一个CButton子类重写DrawItemvoid CFlatButton::DrawItem(LPDRAWITEMSTRUCT lpDIS) { CDC dc; dc.Attach(lpDIS-hDC); CRect rc lpDIS-rcItem; UINT state lpDIS-itemState; COLORREF clrFace m_clrNormal; if (state ODS_SELECTED) clrFace m_clrPressed; else if (state ODS_DISABLED) clrFace RGB(200, 200, 200); dc.FillSolidRect(rc, clrFace); dc.Draw3dRect(rc, RGB(180, 180, 180), RGB(180, 180, 180)); CString strText; GetWindowText(strText); dc.SetBkMode(TRANSPARENT); dc.SetTextColor((state ODS_DISABLED) ? RGB(130, 130, 130) : RGB(40, 40, 40)); CFont* pOldFont dc.SelectObject(m_fontBtn); dc.DrawText(strText, rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE); dc.SelectObject(pOldFont); dc.Detach(); }几个实战注意点。第一DrawItem里不要ReleaseDC因为hDC是系统借给你的用Attach/Detach配对就行DeleteDC会导致后续绘制全部失败。第二ODS_FOCUS状态要单独处理键盘操作时用户需要看到焦点框画一个虚线矩形dc.DrawFocusRect是最省事的做法。第三按钮的圆角、渐变这些效果全部靠FillSolidRect、GradientFill、RoundRect自己拼系统不会帮你。如果项目用的是VS2008 SP1以后的版本可以直接用CMFCButton。它内置了圆角、边框色、背景色、文字色、鼠标悬停和按下状态的配置接口几行代码就能出来一个像样的扁平按钮m_btnOk.m_bTransparent FALSE; m_btnOk.m_bDrawFocus FALSE; m_btnOk.m_nFlatStyle CMFCButton::BUTTONSTYLE_FLAT; m_btnOk.SetFaceColor(RGB(0, 120, 215), TRUE); m_btnOk.SetTextColor(RGB(255, 255, 255)); m_btnOk.SetMouseCursorHand();SetFaceColor的第二个参数决定是否同时改文字颜色以外的边框具体效果不同版本略有差异建议实测。CMFCButton的局限是样式偏少做复杂交互比如带图标的胶囊按钮还是得回到自绘。4.2 ListCtrl与TreeCtrl的行级着色CListCtrl想让某一行变红、隔行变色、或者高亮特定状态必须用NM_CUSTOMDRAW。这个通知消息分多个阶段发送你必须先把CDDS_PREPAINT阶段的返回值设成CDRF_NOTIFYITEMDRAW系统才会继续给你发CDDS_ITEMPREPAINT。void CMyListCtrl::OnNMCustomdraw(NMHDR* pNMHDR, LRESULT* pResult) { LPNMLVCUSTOMDRAW pLVCD reinterpret_castLPNMLVCUSTOMDRAW(pNMHDR); *pResult CDRF_DODEFAULT; switch (pLVCD-nmcd.dwDrawStage) { case CDDS_PREPAINT: *pResult CDRF_NOTIFYITEMDRAW; break; case CDDS_ITEMPREPAINT: { int nRow (int)pLVCD-nmcd.dwItemSpec; if (nRow % 2 1) { pLVCD-clrText RGB(50, 50, 50); pLVCD-clrTextBk RGB(245, 248, 252); } else { pLVCD-clrTextBk RGB(255, 255, 255); } if (m_bAlarmRow nRow) { pLVCD-clrText RGB(200, 30, 30); } *pResult CDRF_DODEFAULT; } break; default: break; } }注意事项有几条。消息映射要用ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, CMyListCtrl::OnNMCustomdraw)因为通知是发给控件自身的光用ON_NOTIFY挂在对话框上收不到。另外如果列表用了LVS_REPORT并且开了双缓冲LVS_EX_DOUBLEBUFFER自定义绘制会更稳定不容易出现拖动滚动条时的残影。树控件的接口基本一致LPNMTVCUSTOMDRAW结构里同样有clrText和clrTextBk阶段判断逻辑一模一样。差别在于树控件是按节点句柄走dwItemSpec里是HTREEITEM强转过来的整数取行号得自己GetItemData。如果只想改表头颜色CListCtrl还提供了SetTextColor、SetTextBkColor、SetBkColor这几个成员函数但它们只对表头部分生效数据区不管用。这个设计挺让人费解我第一次用的时候调了半天以为是没刷新。4.3 GroupBox与Tab控件的背景处理分组框是MFC里出了名的难伺候。它的文字位置有偏移、背景经常带一块灰色、边框颜色在开启视觉样式后根本改不动。基础处理还是走OnCtlColor判断pWnd-GetDlgCtrlID()是分组框的ID然后SetTextColor改文字色、SetBkMode(TRANSPARENT)让文字背景透明返回父窗口的背景刷。这样至少灰色块能消掉。但边框颜色和圆角就只能自绘了。常见做法是给分组框的父窗口响应WM_CTLCOLORSTATIC时返回NULL_BRUSH让分组框完全不画自己的边框然后在父窗口的OnPaint里用RoundRect或者Rectangle自己画一个带标题的框void CMyDlg::DrawGroupFrame(CDC* pDC, CWnd* pGroup, LPCTSTR lpszTitle) { CRect rc; pGroup-GetWindowRect(rc); ScreenToClient(rc); CPen pen(PS_SOLID, 1, RGB(200, 205, 212)); CPen* pOldPen pDC-SelectObject(pen); CBrush* pOldBrush (CBrush*)pDC-SelectStockObject(NULL_BRUSH); CRect rcFrame rc; rcFrame.top 8; pDC-RoundRect(rcFrame, CPoint(8, 8)); // 标题区域用底色挖掉一段边框 CRect rcTitle rc; rcTitle.left 10; rcTitle.right rcTitle.left pDC-GetTextExtent(lpszTitle).cx 8; rcTitle.bottom rcFrame.top 9; pDC-FillSolidRect(rcTitle, m_clrDlgBk); pDC-SetBkMode(TRANSPARENT); pDC-SetTextColor(RGB(70, 78, 90)); CFont* pOldFont pDC-SelectObject(m_fontBody); pDC-DrawText(lpszTitle, rcTitle, DT_CENTER | DT_VCENTER | DT_SINGLELINE); pDC-SelectObject(pOldFont); pDC-SelectObject(pOldPen); pDC-SelectObject(pOldBrush); }这段代码的核心思路是先画框再用背景色盖掉标题位置的那截边框最后写标题。rcFrame.top 8是为了让标题压在框的上边缘上视觉上像标准分组框。这个偏移量得根据字体高度微调字体越大偏移越多。Tab控件稍微友好一点。CTabCtrl本身有SetTextColor和SetBkColor但设置之后必须在TCN_SELCHANGE通知里调一次Invalidate否则标签页切换时颜色会跳回默认。另外Tab控件的标签区域和内容区域是两回事内容区域实际上是另外的子对话框颜色要各自处理。5. 闪烁、重绘与高分屏适配5.1 双缓冲与WM_ERASEBKGND界面一复杂最直观的问题就是拖动窗口或者切换页面时闪。闪烁的根源是先擦背景再画内容这套默认流程两次绘制之间有一帧空隙。缩小空隙的办法是让擦除和绘制合并到一次完成。第一步OnEraseBkgnd里不做实际绘制只返回TRUE告诉系统背景我处理了BOOL CMyDlg::OnEraseBkgnd(CDC* pDC) { return TRUE; }第二步所有背景绘制挪到OnPaint并且用内存DC做双缓冲void CMyDlg::OnPaint() { CPaintDC dc(this); CRect rcClient; GetClientRect(rcClient); CDC memDC; memDC.CreateCompatibleDC(dc); CBitmap bmp; bmp.CreateCompatibleBitmap(dc, rcClient.Width(), rcClient.Height()); CBitmap* pOldBmp memDC.SelectObject(bmp); memDC.FillSolidRect(rcClient, m_clrDlgBk); if (m_bmpBk.GetSafeHandle() ! nullptr) { CDC imgDC; imgDC.CreateCompatibleDC(dc); CBitmap* pOldImg imgDC.SelectObject(m_bmpBk); memDC.StretchBlt(0, 0, rcClient.Width(), rcClient.Height(), imgDC, 0, 0, m_nBmpW, m_nBmpH, SRCCOPY); imgDC.SelectObject(pOldImg); } DrawGroupFrame(memDC, GetDlgItem(IDC_GROUP_INFO), _T(设备信息)); dc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldBmp); }写这段有两点必须注意。第一bmp和memDC要声明成局部变量CreateCompatibleBitmap用的是dc而不是memDC写成CreateCompatibleBitmap(memDC, ...)出来的是单色位图画面会变成黑白。这个坑几乎每个人都踩过一次。第二对话框要有WS_CLIPCHILDREN样式否则父窗口画背景时会连子控件区域一起画子控件再画一遍闪烁更严重。这个样式在对话框属性里勾选Clip Children即可。对于列表、树这类本身不闪烁但滚动时会有残影的控件加上扩展样式就够m_list.SetExtendedStyle(m_list.GetExtendedStyle() | LVS_EX_DOUBLEBUFFER); m_tree.SetExtendedStyle(m_tree.GetExtendedStyle() | TVS_EX_DOUBLEBUFFER);5.2 高分屏下的字体与坐标适配程序在4K屏上跑起来字体糊、控件挤成一团本质原因是DPI没适配。Manifest里声明了dpiAware之后系统不再帮你拉伸所有坐标和字号都得自己按比例放大。获取当前窗口DPI推荐用GetDpiForWindowWindows 10 1607起老系统上用GetDeviceCaps(hdc, LOGPIXELSX)兜底int CMyDlg::GetDpiScale() { int dpi 96; HMODULE hUser32 GetModuleHandle(_T(user32.dll)); if (hUser32 ! nullptr) { typedef UINT (WINAPI *PFN_GetDpiForWindow)(HWND); PFN_GetDpiForWindow pFn (PFN_GetDpiForWindow)GetProcAddress(hUser32, GetDpiForWindow); if (pFn ! nullptr) { dpi (int)pFn(GetSafeHwnd()); return MulDiv(dpi, 100, 96); } } CDC* pDC GetDC(); if (pDC ! nullptr) { dpi pDC-GetDeviceCaps(LOGPIXELSY); ReleaseDC(pDC); } return MulDiv(dpi, 100, 96); }拿到的百分比再分两条路用。字体用-MulDiv(磅值, dpi, 72)算lfHeight坐标和控件尺寸用MulDiv(原始值, dpi, 96)缩放。字体这条千万不要直接对像素值乘比例因为磅值和像素之间还有个72的换算关系混用会导致125%缩放下字体偏小。还有一种更省事的方案整个界面用CMFCFontComboBox之类的控件让用户自己选或者干脆在程序启动时按DPI选一套预置字号表。这种做法牺牲一点精准度换来的是代码简单适合界面不复杂的工具类软件。需要提醒的是OnInitDialog里做的坐标调整要在对话框已经按DPI创建之后进行用SetWindowPos逐个调整子控件位置。如果调整前用户已经看见了窗口会看到一次明显的跳动所以建议在OnInitDialog返回TRUE之前完成或者对话框先用SW_HIDE创建。6. 问题速查表与踩坑记录6.1 常见现象速查表下面这张表是我这些年被问得最多的问题按现象列出来方便对着排查。现象最可能的原因处理方向字体设了没变化CFont是局部变量已析构改为类成员变量字体在别人机器上变样字体名不存在被静默替换先枚举字体或选通用字体静态文本颜色不生效没重写OnCtlColor或没返回刷子在OnCtlColor中处理CTLCOLOR_STATIC静态文本背景是灰块返回的是系统默认按钮面色刷返回对话框背景刷并加WS_EX_TRANSPARENT文字背景透明但有残影父窗口没画背景或没设透明样式补WS_EX_TRANSPARENT并完善OnEraseBkgnd按钮背景改不动标准按钮不支持背景色改BS_OWNERDRAW自绘或用CMFCButton列表某行颜色不生效未返回CDRF_NOTIFYITEMDRAW在CDDS_PREPAINT阶段正确返回拖动窗口时闪擦除与绘制分两次完成双缓冲加WS_CLIPCHILDREN高分屏文字发糊DPI未适配或lfQuality为DEFAULT按DPI算lfHeight并设CLEARTYPE_QUALITY句柄数一直涨在OnCtlColor里创建GDI对象把创建移到OnInitDialog组合框下拉列表字体不对下拉列表尚未创建发WM_SETFONT或先展开一次编辑框只读时颜色跳变只读编辑框走CTLCOLOR_STATIC用ES_READONLY区分处理6.2 几条实战心得关于GDI对象的监控我建议在调试阶段挂一个句柄计数。GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS)能拿到当前进程的GDI对象数量在定时器里打一下日志跑个几小时看曲线是不是平的。只要持续上涨基本就是在某个会被频繁调用的函数里漏了DeleteObject或者重复Create。这个习惯帮我抓出过好几个藏得很深的泄漏。字体缓存的粒度也值得琢磨。界面上十几种字号、四五种字体如果每个控件都CreateFont一份句柄数会浪费。我一般按字体名字号粗体斜体做一个key用CMap缓存CFont对象取值时查缓存没有才创建。这样既省句柄也避免了同一个字符串在磁盘上被反复加载。还有一个是颜色常量集中管理。项目里散落着几百个RGB(...)或者#FF5722改主题的时候找都找不全。我的做法是单独建一个头文件把所有颜色定义成const COLORREF命名按用途走比如CLR_TEXT_PRIMARY、CLR_TEXT_SECONDARY、CLR_BG_PANEL。这样换皮肤时只需要替换这一个文件或者做成从配置文件读编程体验会好很多。最后提一句静态文本的对齐问题。静态文本窗口的宽度是固定的DT_LEFT对齐下文字超出会被裁掉加SS_ENDELLIPSIS让系统自动加省略号更合适。如果静态文本要跟着字数动态变宽得先用GetTextExtent算宽度再SetWindowPos而且要考虑DPI缩放算出来的像素值还得乘一遍比例。这块麻烦归麻烦但做好了界面上不会出现半个字被切掉的尴尬场面。关于OnCtlColor的返回值还有个容易被忽略的规则返回的HBRUSH必须是长期存在的句柄。如果你在函数里CreateSolidBrush然后直接返回那个刷子会立刻泄漏而且下次重绘可能已经失效。要么用GetStockObject拿系统预置刷要么用类成员变量保存自己创建的刷子。踩过这个坑之后我现在看到有人在OnCtlColor里写return (HBRUSH)CreateSolidBrush(...);就会本能地提醒一句。控件自绘还有一个细节是鼠标交互。自绘按钮如果只处理DrawItem鼠标悬停、按下、离开这三种状态都不会触发重绘按钮点下去没有任何视觉反馈。要补上OnMouseMove、OnLButtonDown、OnMouseLeave在状态变化时调Invalidate。OnMouseLeave需要通过WM_MOUSELEAVE消息配合TrackMouseEvent才能收到光靠OnMouseMove判断坐标是否越界会漏掉快速移出的情况。
返回列表