作为常年跟 Win32 打交道的人我始终觉得字体这块是 GUI 开发里最容易被低估的环节。很多界面看着别扭问题并不出在布局算法上而是对“系统字体与字符大小”的理解还停留在“选个字号就行”的层面。这一章我把这些年积累的字体处理经验完整梳理一遍从字体枚举、字符度量到高分屏适配和缓存机制把那些文档里语焉不详的地方一次讲透。1. 字体体系概览与核心概念1.1 正确理解 Win32 字体机制的设计逻辑Win32 里的字体处理是一个容易让人产生割裂感的话题GDI 逻辑上把字体当作一种“对象”来管理但你真正拿到手里的是一个 HFONT 句柄而监听系统设置变化时你又会发现字体其实和 NONCLIENTMETRICS、系统颜色、DPI 这些外壳参数绑定在一起。这套机制并不复杂但如果不理解它的分层逻辑写代码的时候就会处处碰壁。第一层是字体资源本身。Windows 通过 Font Family字体族和 Font Face字体面两级结构来组织字体比如“微软雅黑”是族名而“微软雅黑 Regular”“微软雅黑 Bold”则是这个族下的具体字体面。GDI 的字体匹配引擎会根据你传入的 LOGFONT 结构体按照字符集、字重、样式逐项匹配最合适的物理字体。第二层是 GDI 的字体对象管理。CreateFont 或 CreateFontIndirect 返回的 HFONT 是逻辑字体对象它并不直接对应某个物理字体文件而是描述了一组“期望特征”。真正去磁盘上读取字体文件、做轮廓解析是当你把这个字体选入 DC设备上下文时才发生的也就是 SelectObject 触发的字体解析流程。第三层是系统 UI 字体。Windows 外壳使用系统字体绘制标题栏、菜单、消息框等元素这套字体通过 SystemParametersInfo 配合 SPI_GETNONCLIENTMETRICS 获取存储在 NONCLIENTMETRICS 结构体的 lfMessageFont、lfStatusFont、lfCaptionFont 字段里。跟用户直接交互的自绘界面同样应该从这套参数出发而不是硬编码一个字体名称。我见过不少项目把“微软雅黑9号”直接写死在代码里结果用户把系统字体设置为“等线”或“宋体”之后界面字体风格就和系统脱节了。根源就在于没有走系统字体查询的路子而是自己拍脑袋定了一个值。正确的思路是把系统字体当作基准在此基础上按比例适配界面中的不同元素。1.2 字体信息的三种获取途径及适用场景字体信息获取有三条路分别对应不同的信息粒度第一条路是获取当前 DC 的字体度量信息核心函数是 GetTextMetrics。它返回的 TEXTMETRIC 结构体包含字符高度、宽度、内外留白等精确数值。这条路的适用场景是文本布局计算你要画一行文字、算控件高度、做垂直居中都需要 tmHeight、tmAscent、tmDescent 这些数据。第二条路是获取特定字符串所占的像素空间用 GetTextExtentPoint32 或 DrawText 配合 DT_CALCRECT。前者适合逐字符测量比如实现简易的文本编辑器光标定位后者适合一整段文本的测量能直接处理换行逻辑。第三条路是系统级的字体策略查询核心是 SystemParametersInfo。它拿到的是“系统建议的字体是什么、多大”而不是某个字体实际的度量值。这条路的出发点不是“这个字体长什么样”而是“系统认为 UI 应该用什么字体”。控件绘制、窗口初始化都应该以这里的数据为基准。这三条路不是互斥的。实际开发中经常是先用第三条路拿到基准字体和大小再用第一条路去取这个字体的精确度量信息最后在绘制时用第二条路做区间计算。我画自定义列表控件时就是先用 SPI_GETNONCLIENTMETRICS 拿到系统字体创建 HFONT 后选入 DC 调 GetTextMetrics 得到行高和基线然后按行高预分配绘制区域最后用 GetTextExtentPoint32 处理每行的省略号逻辑。2. 字符大小的精确度量体系2.1 TEXTMETRIC 结构体逐字段解读与实际应用TEXTMETRIC 是字符度量中最核心的数据结构我把重点字段逐一说明tmHeight 是字体总高度它等于 tmAscent 加 tmDescent。tmAscent 是基线以上的部分包括大写字母高度和上半部分留白tmDescent 是基线以下的部分主要容纳 g、y、p、q 这些字母的下伸部分。计算控件高度时如果想让文本上下各留出一点呼吸感最稳的做法是直接用 tmHeight 作为基础行高再额外加几个像素的 padding而不是自己猜测一个“看起来差不多”的数值。tmInternalLeading 是字体内部留白它其实是大写字母顶部到 em 方框顶部的空隙。有些字体这个值是 0有些字体是正数它决定了文字在行内“视觉上偏上还是偏下”。做文本垂直居中时不能简单地把文字区域高度减半再减 tmHeight 的一半正确做法是textY (rectHeight - tmHeight) / 2 - tmInternalLeading。这个修正量初看莫名其妙但实测效果差别明显。tmExternalLeading 是字体外部留白即行与行之间的推荐间距。多行文本排版时把行高设置为 tmHeight tmExternalLeading文本行距最自然。强行用固定行高比如 20 像素遇到外部留白大的字体就会出现行与行黏连的视觉效果。tmMaxCharWidth 是字符最大宽度对等宽字体来说它就是所有字符的宽度对比例字体来说这个值没有布局意义不要拿它当字符平均宽度用。需要平均宽度时最佳方案是对一段代表字符集调用 GetTextExtentPoint32用总宽度除以字符数。我用过所有字符中取样一段英文加数字加标点的文本这样得到的平均值明显比 tmAvgCharWidth 准确因为 tmAvgCharWidth 本身的计算方式在不同字体上有明显差异。2.2 GetTextMetrics 调用的前置条件与 DC 状态管理GetTextMetrics 不是拿到 HFONT 就能调的它的前置条件是字体已经被选入 DC。也就是说正确顺序是创建 HFONT → SelectObject 到 DC → GetTextMetrics → 处理完再 SelectObject 换回原字体。这里有一个在实际开发中坑过不少人的点SelectObject 的返回值。它返回之前被选入的字体句柄这个句柄必须在后续恢复现场时用否则就会泄漏 GDI 对象。正确做法是HGDIOBJ hOldFont SelectObject(hdc, hFont);然后在用完后SelectObject(hdc, hOldFont);把旧字体选回去。还有一个容易忽略的小细节内存 DCCreateCompatibleDC 创建的和窗口 DC 对字体的渲染模式不完全一致。内存 DC 刚创建时默认是 1:1 映射字体度量拿到的就是逻辑单位值但窗口 DC 如果经过了 SetMapMode 调整映射模式GetTextMetrics 返回的值就是映射后的结果。所以度量字体时最好在同一个 DC 状态下进行。我的习惯是单独创建一个内存 DC 专门做文本度量不跟绘制 Windows 混用避免映射模式被其他地方改动影响计算结果。另外字体渲染的质量和 ClearType 是否开启会影响实际显示效果但不会影响 GetTextMetrics 返回的度量值。字形像素的填充方式改变不会让字体框体变化这一点在做像素级布局时要知道避免把渲染问题误判成度量问题。3. 系统字体获取与创建实操3.1 从系统参数获取 UI 字体基准获取系统 UI 字体的标准方式是调用 SystemParametersInfo 函数以 SPI_GETNONCLIENTMETRICS 为参数。这里有一个关键的结构体大小坑NONCLIENTMETRICS 结构体在不同 Windows 版本中尺寸不同如果没有正确设置 cbSize函数会返回失败错误码是 ERROR_INSUFFICIENT_BUFFER。在较新的 Windows 版本中NONCLIENTMETRICS 结构体末尾多了一个 iScrollWidth 字段仅当定义了 WINVER 0x0600 时存在。如果你的项目编译时 WINVER 设置较低结构体实例会比系统预期的短调用就会失败。解决方式是确保在编译前定义合适的 WINVER。代码层面的调用方式如下NONCLIENTMETRICS ncm { sizeof(ncm) }; SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(ncm), ncm, 0); LOGFONT lf ncm.lfMessageFont;拿到 LOGFONT 之后可以直接用它创建 HFONTHFONT hFont CreateFontIndirect(lf);lfMessageFont 代表系统消息框和对话框使用的字体也是大多数应用程序主界面应该沿用的字体。相比 lfCaptionFont标题栏字体和 lfStatusFont状态栏字体lfMessageFont 在字号和字重上最中性适合作为应用的主字体基准。还有一个细节值得注意如果你在 DPI 感知模式下运行这个函数返回的字体大小已经是经过 DPI 缩放后的像素值。也就是说在 96 DPI 下可能是 12 磅在 144 DPI 下就是 18 磅的效果。如果你的程序没有声明 DPI 感知系统会做虚拟化缩放返回的值仍然是虚拟化前的值界面就会模糊。这里先埋个伏笔第四节展开讲。3.2 利用 LOGFONT 精细控制字体特征LOGFONT 结构体在设计上允许你只填写关心的字段其余置零让系统字体匹配引擎根据默认逻辑去补齐。但工程上我建议尽量显式地赋值减少不确定性。关键字段的作用如下lfHeight 指定字体高度单位是逻辑单位。这里有个很常见的误解——它表示的是字符总高度em height而不是某些开发工具里理解的“字号”。如果你从 DTPoint 或磅值转换公式是lfHeight -MulDiv(pointSize, GetDeviceCaps(hdc, LOGPIXELSY), 72);。注意前面的负号传正值表示“我想让字体高度是这个值”传负值表示“我想让字符高度接近这个值”两者的匹配逻辑有区别。习惯上取负值因为它接近传统排印里的点阵字模高度。lfWeight 指定字重范围是 0 到 1000400 是常规FW_NORMAL700 是粗体FW_BOLD。这里有一个值得了解的点不是所有字体族都实现了中间字重比如 500、600 这种半粗不细的级别。当系统发现某个字重没有对应物理字体时会做合成加粗或者降到最近的可用字重。所以如果你需要中等字重的视觉效果但字体族不支持直接用 FW_BOLD 然后调小号反而更可控。lfCharSet 指定字符集这是最容易出问题的地方。中文环境下必须设置为 DEFAULT_CHARSET值为 1让系统根据 lfFaceName 自动判断字符集。如果你误设了 ANSI_CHARSET某些中文字体可能直接匹配失败系统会悄悄回退到默认字体而你的界面文字就全部变成宋体。lfFaceName 是字体族名称注意这个字段是 TCHAR 数组在 Unicode 编译环境下必须用宽字符串。复制字体名时建议使用 StringCchCopy 这类安全拷贝函数防止越界。实际开发中我见过太多因字体名拼写错误导致回退到默认字体的现象比如“微软雅黑”写成“微软雅黑 UI”“等线”写成“等线 Light”——这两组是完全不同的字体族显示效果和度量都不一样。4. 字体枚举与筛选机制4.1 系统字体枚举回调机制详解有些场景需要枚举系统已安装的字体比如做一个字体选择下拉框或者在运行时检测某个字体是否真的存在。Win32 通过 EnumFontFamiliesEx 配合回调函数实现。基本调用方式EnumFontFamiliesEx(hdc, lf, EnumFontProc, context, 0);回调函数的签名是int CALLBACK EnumFontProc(const LOGFONT* lpelfe, const TEXTMETRIC* lpntme, DWORD fontType, LPARAM lParam);回调中第一个参数是字体的 LOGFONT 信息其中包含了字体的完整名称、字重、字符集等。回调用返回值控制枚举行为返回非零值继续枚举返回 0 停止枚举。一个容易踩的坑是枚举同一个字体族的不同字体面Regular、Bold、Italic 等时回调会被调用多次每次传入的 LOGFONT 的 lfFaceName 相同但其他字段不同。如果你想做一个字体族列表需要在回调里做去重处理只保留第一次遇到的族名。另一个容易踩的坑与字符集有关同一个字体族在枚举时可能会带上多个字符集条目比如 GB2312_CHARSET 和 DEFAULT_CHARSET 各回调一次。如果你对每个回调都添加到列表里界面上就会看到大量重复的字体名。我的做法是只保留 lfCharSet 为 DEFAULT_CHARSET 的条目或者在添加时比较字体名是否重复。4.2 屏幕字体与打印字体fontType 的工程意义枚举回调的第三个参数 fontType 指示字体类型常见值是 DEVICE_FONTTYPE、RASTER_FONTTYPE、TRUETYPE_FONTTYPE 的组合。在做字体选择器时这个字段的价值在于区分字体来源。RASTER_FONTTYPE 指位图字体缩放会失真不建议用于 UIDEVICE_FONTTYPE 指打印机等设备自带的字体仅对特定输出设备有效TRUETYPE_FONTTYPE 是指 TrueType 或 OpenType 字体可以无损缩放。实际操作中字体选择器里通常只保留 TRUETYPE_FONTTYPE 的字体这能过滤掉一大堆屏幕位图字体。否则用户会在列表里看到一些名字很怪、选完以后界面效果很糟糕的老式字体。如果你需要做字体预览可以用 CreateFont 以枚举到的 LOGFONT 创建字体选中到内存 DC 里绘制几个字符再 BitBlt 到预览区域。这里我建议预览时把字体大小固定为一个较大的值比如 12 磅或 14 磅否则小号字体下很多字形细节根本看不出来用户没法判断美观度。5. DPI 感知与字体缩放适配5.1 DPI 虚拟化与程序声明感知的时机选择字体大小和屏幕 DPI 是一对分不开的关系。Windows 的 DPI 虚拟化机制在程序未声明 DPI 感知时会把整个界面当作 96 DPI 来布局然后由系统整体拉伸。这种做法最直接的影响就是文字发虚尤其是小字号文本拉伸后的毛边肉眼可见。破坏 DPI 虚拟化需要在进程启动早期调用 SetProcessDPIAware或者通过 manifest 声明 PerMonitorV2 感知。建议尽早执行比如在 WinMain 的第一行就做任何窗口创建之前的调用都算早。声明感知之后字体处理就会暴露更多细节。最主要的变化是SystemParametersInfo 返回的字体大小会随实际 DPI 变化而不是永远 96 DPI 基准。GetDeviceCaps(hdc, LOGPIXELSY) 返回的垂直分辨率也会反映真实的 DPI这让自绘控件能根据实际像素密度调整布局比例。需要注意的是即使声明了 DPI 感知不同 Windows 版本的行为仍然存在细微差异。在较老版本上系统只能处理进程级 DPI 感知在较新版本上才完整支持 Per-Monitor DPI即每个显示器可以有不同的缩放比例。如果你的程序要支持多显示器不同缩放比例的方案不能把这部分只放在初始化阶段还需要监听 WM_DPICHANGED 消息来动态调整字体。5.2 倍率响应式字体大小计算方案DPI 响应式字体计算的核心思路是以 96 DPI 为设计基准在运行期按实际 DPI 换算像素值。公式int ScaleFontSize(int baseSize, int dpi) { return MulDiv(baseSize, dpi, 96); }MulDiv 函数先做乘法再除法避免中间值溢出也比直接写浮点乘除可靠。当 DPI 从 96 变成 144 时基准字号 12 会变成 18视觉比例保持一致。你可能会问为什么不直接用 SystemParametersInfo 拿到的系统字体大小而是自己再乘一遍系数原因在于系统字体大小已经反映了 DPI如果你再乘一次就是双重缩放。正确做法是基础字体直接沿用 lfMessageFont它已经经过了 DPI 适配然后对于界面上的辅助元素比如图标的辅助说明文字、分组标题通过 MulDiv 在 lfMessageFont 的基础上调整而不是从原始磅值重新换算。这里还有一个不少项目踩过的问题创建字体时lfHeight 的单位是像素但在 DPI 变化后你重新创建的字体也必须使用新的像素值。如果窗口跨屏拖拽、DPI 变化后只重建了窗口布局而没有重建字体文字尺寸就会和位置尺寸脱节界面看起来就会显得僵硬变形。6. 常见问题与排查技巧实录6.1 字体回退问题中文环境下的字体匹配陷阱在中文系统上跑 Win32 程序最常见的字体问题是界面大多数字体正常但某些字符显示成方块或变成另一种字体风格。这通常是字体回退Font Fallback在起作用GDI 在当前的字体族中找不到某个字符对应的字形就会到系统字体表中去找另一个包含该字符的字体来渲染。字体回退之所以容易造成混乱是因为它发生在你完全不知情的情况下。你在代码里指定“微软雅黑”里面没有一个全角波浪线“”的字形系统就可能用一个完全不同的字体来画这个字符。最直观的排查方法是把可疑字符单独绘制出来用 GetTextFace 看看实际渲染用的字体族名。规避手段有几种。第一尽量使用系统默认的字体族搭配让字体匹配逻辑自行处理第二统一使用官方中文字体作为主字体这类字体覆盖的字符范围广大部分符号都有字形第三如果必须使用第三方字体建议混排时对非覆盖范围内的字符做检测在 UI 层面标注出来避免用户在界面上看到方块。6.2 GDI 对象泄漏与字体句柄管理HFONT 是 GDI 对象受系统 GDI 对象数量上限约束。早期 Windows 版本上 GDI 对象总数限制是每个进程 10000 个虽然新版本放宽了不少但泄漏多了程序迟早会崩在 CreateFont 失败上。字体句柄泄漏的高发场景有三个。第一SelectObject 返回的旧字体句柄没有保存或没有恢复后面 DeleteObject 删了不该删的字或者新字体频繁选入但旧字体一直没返回给系统。第二CreateFont 创建的字体没有配对的 DeleteObject 调用。第三在循环或回调里创建字体循环体结束时没有销毁每一轮都新建一个 HFONT。这种问题表现起来很隐蔽程序可能运行数小时才崩溃Windows 任务管理器里 GDI 对象数持续上涨是典型的信号。我建议养成一种习惯凡是在一个函数里创建 HFONT就一定在退出的路径上销毁凡是 SelectObject 选择新字体一定保存旧的句柄并在函数结束前恢复。这样虽然在代码上增加了一两行但能避免绝大多数难以定位的资源问题。另外在排查 GDI 泄漏时可以在任务管理器详细信息里添加“GDI 对象”列观察程序运行前后的对象数量变化。如果在界面反复操作后 GDI 对象数稳步增长基本可以断定是字体句柄或者其他 GDI 对象泄漏。6.3 字体度量值异常单位混淆与映射模式字体度量值异常很多时候不是函数调错而是单位没对齐。GetTextMetrics 返回的值是“当前映射模式下的逻辑单位”。默认映射模式 MM_TEXT 下逻辑单位等于像素但如果你调用了 SetMapMode 改成 MM_ANISOTROPIC 或 MM_HIENGLISHGetTextMetrics 的结果单位就变成了 0.001 英寸等这时候直接拿它去做像素布局就会完全错位。如果项目里有多个模块共享同一个 DC有的模块设置了自定义映射模式有的模块按像素处理字体度量值就会“一会儿正常一会儿异常”。最稳妥的办法是绘制前调 GetMapMode 检查当前映射模式或者把绘制逻辑严格限定在 MM_TEXT 模式内。如果确实需要自定义映射那就要把所有字体操作都放进一个明确的映射上下文中进行。另外一个单位混淆的常见场景是把磅值Point直接当成像素值用。磅是排印单位1 磅等于 1/72 英寸。在 96 DPI 下 9 磅字体的像素高度是 9 × 96 / 72 12 像素左右但如果你直接把 9 当像素高度那字体渲染出来就会很小。跟设计师对接时他们提供的字号通常是磅值在代码里使用时必须通过 DPI 换算成像素。6.4 字体与文本颜色的对比度问题字体和字符大小不只是度量问题最终显示效果和颜色搭配紧密相关。经常看到有些界面在浅灰背景上用浅灰文字或者在深色背景上用深蓝文字信息几乎不可读。Win32 自绘控件中字体颜色的选择最好与系统主题保持一致。如果你在自绘控件中手动绘制文本建议不要硬编码文本颜色而是用系统主题颜色例如 GetSysColor(COLOR_WINDOWTEXT) 或 COLOR_MENUTEXT。在深色模式下COLOR_WINDOWTEXT 会自动变为适合深色背景的浅色文字不需要自己在代码里判断。字体渲染质量和 ClearType 效果也跟文字颜色的对比度相关。ClearType 是通过液晶屏像素的亚像素结构来增强清晰度的但只有在文字颜色和背景颜色对比度较高时才有效果。如果你把灰色文字画在灰色背景上ClearType 的亚像素渲染往往会产生彩色边缘。7. 字体缓存与性能优化方案7.1 高频绘制场景下的字体缓存策略在自绘控件、编辑器、聊天界面等高频绘制场景中频繁创建和销毁 HFONT 会带来不小的性能损耗。虽然单次 CreateFont 的开销并不大但在每帧绘制中反复调用累积起来就可能拖累帧率并且增加 GDI 对象管理的压力。我常用的做法是维护一个缓存表以关键属性组合作为键——通常是 (字体名, 字重, 字号像素值, 字符集)——把已经创建的 HFONT 缓存起来下次绘制时直接查表复用。这里再强调一次同样的键值组合创建两次 HFONT得到的字体对象是可以相互替换的因为逻辑字体是由这些属性唯一确定的。缓存表必须配套清理策略。项目退出时统一销毁所有缓存中的字体句柄窗口关闭时如果缓存是窗口级的也要逐一清理。如果缓存是全局的要注意线程同步因为 GDI 对象不保证跨线程使用安全一个线程里创建的字体句柄在另一个线程里使用必要时需要加锁保护或做线程局部存储。7.2 字体缓存实现中的线程安全与失效更新字体缓存要处理两种失效场景第一是进程退出时的统一销毁第二是系统字体设置变化时的主动刷新。很多程序常年开着用户切换系统 DPI 或更新字体设置后如果不刷新缓存界面字体就一直停留在旧状态。监听系统设置变化的标准方式是处理 WM_SETTINGCHANGE 消息。收到该消息时需要清除字体缓存并重新从 SystemParametersInfo 读取字体基准。这里有一个容易忽略的问题WM_SETTINGCHANGE 可能不是发给你的窗口的如果你的窗口因为某种原因没收到缓存就不会失效。稳妥的做法是在收到该消息时无条件清空整个字体缓存而不是试图判断具体是哪个设置发生了变化。线程方面如果界面线程在绘制时使用了某个字体而另一个后台线程正在销毁它理论上存在窗口重绘使用悬空句柄的风险。尽量避免跨线程共享字体缓存或者用一个锁来包住缓存的查询和引用计数操作。从工程角度来看字体缓存的代码量不大带来的性能收益却很直观。我在一个高频刷新的监控面板里实践过优化前每次绘制都会新建和销毁两三个字体句柄帧率敏感时拖慢了近三成加了缓存后单次绘制只是查 hash 表资源开销降到基本可以忽略。8. 从字符度量到文本布局的完整链路在真正落笔写界面代码前把字符度量的链路拉通一遍能避开很多后期改动成本的坑。我个人习惯按下面这个次序处理字体相关的初始化第一步在进程启动阶段尽早声明 DPI 感知。第二步通过 SystemParametersInfo 获取系统字体基准得到 LOGFONT。第三步用 CreateFontIndirect 创建基准字体并选入 DC紧接着调用 GetTextMetrics 取得精确的行高、基线和留白参数。第四步将计算结果存入布局上下文供后续控件尺寸计算和绘制使用。在控件尺寸计算阶段一个直接而有效的公式是控件高度 tmHeight tmExternalLeading 垂直方向 padding。这里要注意如果把控件高度设置成刚好等于 tmHeight很多字体的文字会“顶天立地”缺少视觉呼吸感增加一点 padding 后观感才接近系统原生控件。在绘制阶段需要区分“单行文本”和“多行文本”两种场景。单行文本使用 DrawText 的 DT_SINGLELINE 配合 DT_VCENTER 做垂直居中基线位置用 tmAscent 校正多行文本需要自己根据 tmHeight tmExternalLeading 计算行高再逐行调用 ExtTextOut 或 TextOut 绘制。这里常见的毛病是在多行文本中直接用固定行高遇到外部留白大的字体行与行之间就会显得拥挤阅读体验明显变差。文本的垂直布局还有一个细节值得注意在做文本和图形混排时比如图标旁边跟一行文字图标尺寸经常会按控件高度拉伸而文本实际渲染高度往往小于控件高度。正确的做法是让图标的垂直中心对齐到文本的视觉中心而不是对齐到控件矩形中心。文本的视觉中心大约在 tmAscent 的中间偏上位置用这个位置做基准才能让混排看起来协调。最后聊一下字符宽度的使用。比例字体比如微软雅黑下“用户输入区宽度应该容纳多少字符”这类需求没法用 tmMaxCharWidth 或 tmAvgCharWidth 精确计算因为每个字符宽度不一样。务实的做法是用一段最坏场景的示例文本比如连续的数字和字母混合调用 GetTextExtentPoint32 量出实际像素宽度再算出单位字符的平均宽度用于估算。这样的结果比任何理论公式都接近实际。字符大小和字体处理这块看起来是 Win32 里非常基础的议题但把这一层吃透了你会发现很多界面布局的疑难杂症其实都根植在这里。我踩过不少坑也踩出了上面这些经验。代码写多了以后我最大的体会是字体处理没有银弹唯一靠谱的办法是把每一个数值的来历都搞清楚——它来自哪一次系统调用、经历了怎样的换算、在什么 DPI 下有效。能做到这一点你的 Win32 界面在字体层面的问题就基本可控了。