☰
TextOut与wsprintf组合实战:Win32客户区动态文本绘制全攻略
2026/10/17 8:16:18 网站建设 项目流程

1. 先搞懂这套组合拳解决什么问题

做 Win32 桌面开发的朋友,尤其是刚啃完《Windows 程序设计》前几章的人,一定绕不开一个场景:你想在窗口客户区里“画”点东西,但又不只是画个固定的字符串,而是想显示当前鼠标坐标、计时器秒数、某种计数器的实时数值。这时候只靠 TextOut 是不够的,因为 TextOut 的第三个参数要的是一个现成的字符串指针,你没法直接把 int 丢给它。于是 wsprintf 就派上用场了:先把变量“翻译”成文本,再交给 TextOut 上屏。

这套组合在 Win32 编程里极其常见,几乎算得上是入门级“动态文本显示”的标准答案。我当年自己写第一个画图工具时,想在状态栏和客户区角落显示鼠标坐标,翻遍了手头的教程,最后发现核心就是两个 API 的配合:wsprintf 负责格式化,TextOut 负责绘制。搞清楚这两个函数各自的脾气、参数的坑、以及它们和 WM_PAINT 消息的协作关系,后面再学 GDI 文本输出、自定义控件绘制都会顺畅很多。

这篇文章适合谁看?正在学 Win32 编程但卡在文字输出这块的人,或者已经会用 MessageBox 显示文字、但想在窗口客户区做更灵活输出的人。我会把 TextOut 的每个参数、wsprintf 的格式化规则、两者配合的完整案例,以及我踩过的内存和刷新相关的坑都拆开讲一遍。看完你不仅能跑通一个动态文本显示的程序,还能举一反三地应付“在窗口上画各种运行时信息”的需求。

2. TextOut 不是“打印文字”,而是“画文字”

2.1 TextOut 的完整签名与参数解读

先看 TextOut 的函数签名:

BOOL TextOut( HDC hdc, // 设备环境句柄 int x, // 起始 x 坐标(逻辑坐标) int y, // 起始 y 坐标(逻辑坐标) LPCTSTR lpString, // 要绘制的字符串 int cchString // 字符串的字符数 );

很多新手第一次看到这五个参数都懵,尤其是最后一个 cchString,它不是“字符串的字节数”,而是“字符数”。在 ANSI 编码下,一个英文字符占一个字节,所以字符数和字节数一致;但如果你编译成了 Unicode 版本(Visual Studio 默认就是 Unicode),一个汉字占 2 个字节却是 1 个字符,这时候要格外小心,传入字节数等于把一个中文字符拦腰截断,显示出来就是乱码或半个矩形。

实践中我一般用_tcslen(lpString)来获取长度,它能根据工程是否定义了_UNICODE自动选择宽字符版本,省心。也可以直接传-1,告诉 TextOut 字符串是 NULL 结尾的,让它自己数长度。不过传-1有一个隐蔽问题:如果你的字符串缓冲区中间某个位置意外出现了 0,TextOut 就会提前截断。所以稳妥做法还是显式传_tcslen的结果。

hdc 这个参数是设备环境句柄,相当于“画板”的凭据。它不会凭空出现,通常通过 BeginPaint 或 GetDC 获得。TextOut 画出来的内容不是持久保存的,窗口被遮挡后再露出来就会丢失,必须依赖 WM_PAINT 消息重绘。你要是把 TextOut 放在 WM_PAINT 之外的地方调用,往往会出现“画了但马上消失”的现象,后面我会详细说。

2.2 逻辑坐标、字体与颜色:决定文本“画”在哪、长什么样

TextOut 的 x、y 坐标是基于当前映射模式的逻辑坐标。默认的 MM_TEXT 模式下,坐标原点在客户区左上角,x 向右增大,y 向下增大,单位是像素。所以你想把文字放在客户区左上角,就写TextOut(hdc, 10, 10, ...),人和代码都能凭直觉理解。

但“文字画出来什么样”不完全由 TextOut 决定,而是由当前选入 HDC 的字体决定。Win32 的 GDI 文本绘制是“画笔选字、画布上墨”的逻辑:你创建一个字体对象,SelectObject 到 HDC 里,后续所有的 TextOut 都会用这个字体渲染。不想自己创建字体的话,系统会给你一个默认的“系统字体”,通常长得很素,字号也偏小。如果你想让显示更清晰或者字号更大,就得自己动手:

HFONT hFont = CreateFont( -24, // 高度,负值表示根据字符高度计算,正负容易搞混 0, 0, 0, // 宽度、角度、角度 FW_NORMAL, // 字重 FALSE, FALSE, FALSE, // 斜体、下划线、删除线 DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, _T("微软雅黑") // 字体名称 ); SelectObject(hdc, hFont); // 画完记得 SelectObject 还原,然后 DeleteObject 释放

颜色方面,文本颜色默认是黑色。想改成其他颜色,用SetTextColor(hdc, RGB(200, 30, 30)),这个设置会一直保留在 HDC 里,直到你再次修改或者 HDC 被释放。背景颜色用SetBkMode(hdc, TRANSPARENT)设成透明模式,否则 TextOut 绘制时会在文字背后画一个矩形背景块,颜色默认为白色,叠在其他图形上会很难看。

有一次我画自绘控件时没把背景模式设成透明,文字底下始终顶着一块白底,怎么调都不对,最后才发现是SetBkMode没调用。这个函数就像一键开关,需要记牢。

2.3 为什么 TextOut 必须放在 WM_PAINT 里

这是新手最容易犯的“逻辑正确但行为诡异”的错误。你把 TextOut 写在窗口过程的某个消息分支里(比如 WM_CREATE),跑起来发现文字确实出现了,但一拖动窗口、一遮挡再恢复,文字就没了。

原因是 GDI 绘图不具备持久性。窗口的客户区内容本质上不是被系统“记住”的,系统只记住了一个概念:这部分区域需要重绘。真正的内容是你的程序在收到 WM_PAINT 时现画出来的。操作系统也没法为你保存 TextOut 画出来的像素 —— 如果它能的话,内存早就爆了。

所以正规做法是:把 TextOut 放在 WM_PAINT 分支里,用 BeginPaint 拿到 HDC,画完用 EndPaint 释放。BeginPaint 会返回“需要重绘的区域”信息,在你画之前它还会自动擦掉背景(如果你设置了窗口类的背景画刷),然后画完之后验证区域,告诉系统“这块我已经画好了”。

case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); // 在这里调用 TextOut TextOut(hdc, 20, 20, _T("Hello, Win32"), 13); EndPaint(hWnd, &ps); return 0; }

大到整个窗口需要重绘,小到一块 50x50 的矩形区域失效,系统都会发 WM_PAINT,你的任务就是把这块区域重新画好。这也是为什么“动态刷新文本”的正确做法不是用定时器一直在 WM_TIMER 里调 TextOut,而是让那块区域失效,然后驱动 WM_PAINT 去重绘。

3. wsprintf:轻量格式化,但边界要心里有数

3.1 wsprintf 到底解决了什么问题

TextOut 只能画“现成的字符串”。但真实需求里,你要画的是“x=120, y=85”这种拼合了变量的文本,总不能手动把数字转成字符串再拼接。这时候格式化函数就是刚需。

wsprintf的签名如下:

int wsprintf( LPTSTR lpOut, // 输出缓冲区 LPCTSTR lpFmt, // 格式字符串 ... // 可变参数 );

它的工作方式和 C 标准库的 sprintf 几乎一样:按格式字符串里的占位符,把后续参数依次替换进去,写入 lpOut 缓冲区,返回写入的字符数(不包括结尾的 NULL 字符)。Win32 程序员之所以熟悉它,是因为在 Windows 的早期生态里,它就是给 GUI 程序用的专用格式化函数,头文件windows.h里直接就能用,不必额外包含<stdio.h>。

我现在写 Win32 代码,依然习惯用它而不是 sprintf,因为窗口程序里格式化字符串的场景本来就轻量,没必要引入完整的 C 运行时库依赖(虽然现在的开发环境里这根本不是问题,但保留这个习惯写出的代码更纯粹、可读性也好)。它支持%d整数、%u无符号、%x十六进制、%s字符串、%c字符,还有一个%ld长整数,基本覆盖了我的日常需求。

3.2 格式占位符使用避坑指南

这块必须细说,因为我在这些占位符上犯过不少低级错误。

第一,%s在 ANSI 版本里对应的是char*,Unicode 版本里必须用%ls(或者大写%S也能用,但写法不统一容易混淆)。如果你在 Unicode 工程里用wsprintf(szBuf, _T("name: %s"), pszAnsiString),编译器可能会报警告,运行时则极有可能乱码甚至崩溃。反过来,%s对应宽字符字符串时,在很多实现里会把内存按字节硬读,出现半截字符。

第二,参数类型和占位符必须严格匹配。%d对应int,你传了个long,在 64 位系统上会出问题,因为long在 Windows 上占 4 字节,倒还好;但如果你传size_t(8 字节),而%d只按 4 字节取参数,那么输出结果就是一个莫名其妙的数。64 位环境下格式化整数,我建议用%ld配long,或者%I64d配__int64,别偷懒。

第三,缓冲区溢出问题。wsprintf不检查输出缓冲区够不够大,它只管往里写,写的比缓冲区容量多就直接踩内存。所以给输出缓冲区分配空间时,宁可多给,不能少给。文本场景下 256 字节或 512 字节通常够了,但如果你要拼接长列表,我的习惯是直接用StringCchPrintf这类安全版本(需要strsafe.h),它显式接收缓冲区大小参数,超长自动截断,不崩。不过在经典教学代码里,wsprintf依然是最顺手的那个,用的时候自己心里装根弦就行。

网上对 wsprintf 还有一个共识值得提:它的内部缓冲区是 1024 字节(这是它在 Windows 内部的固定实现细节),这意味着单个格式化输出的结果不应超过 1024 字节,否则未定义行为。一个简单的基准:如果你的格式化结果可能超过 1KB,换个安全的函数,别硬撑。

3.3 TCHAR 与通用文本映射:一行代码兼容两种编码

讲 wsprintf 和 TextOut 配合时,一定会碰到TCHAR、_T()、LPTSTR这些东西。它们是为了让同一份代码既能编译成 ANSI 版本,也能编译成 Unicode 版本而存在的类型映射。

简单说:

  • TCHAR:当工程定义了_UNICODE时,它是wchar_t;否则是char。
  • LPCTSTR:是const TCHAR*。
  • _T("abc"):根据编码模式自动给字符串字面量加L前缀。

所以案例里我写TCHAR szBuffer[256];而不是char szBuffer[256];,写wsprintf(szBuffer, _T("位置: (%d, %d)"), x, y);而不是wsprintf(szBuffer, "位置: (%d, %d)", x, y);。当你把工程字符集从“多字节字符集”换成“Unicode 字符集”时,代码不用改一行,行为自动切换。

现在 Visual Studio 新建的 Win32 工程默认字符集就是 Unicode,所以从第一课起你就应该写_T包裹字符串字面量,否则写中文文本时会碰到编译出乱码的问题(还不一定报错)。我见过太多新手因为字符串字面量没加_T,在 Unicode 工程下看到满屏乱码,然后怀疑字体问题,其实根子出在编码类型不匹配上。

4. 完整实操:用 wsprintf 构造动态文本 + TextOut 上屏

4.1 目标场景与窗口程序骨架

咱们要做的案例很具体:在窗口客户区里,实时显示鼠标的当前坐标,格式为鼠标位置: (x, y)。每次鼠标移动,文字跟着刷成新的坐标。同时客户区中央显示一个静态信息TextOut + wsprintf 案例,顺便体验一下字体设置。

程序骨架直接套用经典 Win32 窗口程序:

#include <windows.h> #include <tchar.h> // 全局字符串缓冲,保存上一次鼠标坐标文本 TCHAR g_szMousePos[128] = { 0 }; TCHAR g_szScreenInfo[128] = { 0 }; // 窗口过程函数 LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

窗口创建部分就不再重复贴完整代码了,Win32 样板代码大同小异:注册窗口类、创建窗口、消息循环。重点在窗口过程里如何处理消息。

4.2 WM_MOUSEMOVE 里收集坐标并触发重绘

设计思路是这样:鼠标移动时,系统给窗口发WM_MOUSEMOVE,lParam的低 16 位是 x 坐标,高 16 位是 y 坐标。我先提取出来,格式化好文本,然后让这块区域失效,触发 WM_PAINT 重绘。

case WM_MOUSEMOVE: { int x = (short)LOWORD(lParam); int y = (short)HIWORD(lParam); // 关键:只在坐标变化时才重绘,否则鼠标晃动会频繁重绘,CPU 飙升 static int s_lastX = -1, s_lastY = -1; if (x != s_lastX || y != s_lastY) { s_lastX = x; s_lastY = y; wsprintf(g_szMousePos, _T("鼠标位置: (%d, %d)"), x, y); InvalidateRect(hWnd, NULL, TRUE); } return 0; }

这里有个精髓值得说:只重绘必要的内容。一开始我没有加 s_lastX / s_lastY 判断,鼠标在窗口里随便动动,窗口就每秒被无效化几十次,每次都要全窗口擦背景重画,肉眼可见地发虚、耗电。加了一个“坐标变了才更新”的守卫之后,只有在移动到新像素时才触发,效率立刻上一个档次。

InvalidateRect(hWnd, NULL, TRUE)的第三个参数 TRUE 表示在重绘前擦掉背景。传 NULL 表示让整个客户区失效。如果你想优化性能,可以只让文字所在的矩形区域失效,比如:

RECT rc = { 20, 20, 300, 50 }; InvalidateRect(hWnd, &rc, TRUE);

这样只有那个小区域会被重绘,对大规模绘制场景是很大的性能提升。

4.3 WM_PAINT 里把文本画出来

文本已经格式化好了,剩下的工作就是把它画上屏幕。伪代码如下:

case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); // 设置背景透明,避免文字后面白块 SetBkMode(hdc, TRANSPARENT); // 设置文本颜色为深蓝 SetTextColor(hdc, RGB(20, 40, 220)); HFONT hFont = CreateFont( -20, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, _T("微软雅黑")); HFONT hOldFont = (HFONT)SelectObject(hdc, hFont); // 画动态坐标,左上角 (16, 16) TextOut(hdc, 16, 16, g_szMousePos, _tcslen(g_szMousePos)); // 画静态标题,稍微下移 TextOut(hdc, 16, 60, _T("TextOut + wsprintf 案例"), _tcslen(_T("TextOut + wsprintf 案例"))); // 画一条简单的状态说明,顺带展示 %s 格式 wsprintf(g_szScreenInfo, _T("当前字体: %s"), _T("微软雅黑")); TextOut(hdc, 16, 100, g_szScreenInfo, _tcslen(g_szScreenInfo)); SelectObject(hdc, hOldFont); DeleteObject(hFont); EndPaint(hWnd, &ps); return 0; }

这里有一个很典型的模式大家可能注意到了:wsprintf 不只在处理消息时用,在 WM_PAINT 内部也可以直接用。因为它只是个格式化函数,不涉及绘图资源,在哪里调用都行。不过要注意别在 WM_PAINT 里频繁格式化不变的内容,徒增开销。上面代码里那句%s的写法就是演示:运行时把字体名格式化进字符串再输出。

4.4 从客户区中央显示文本:一个更完整的综合版本

上面的例子已经能跑了,但文本全靠在左上角,不够“案例味”。我把代码升级一下,模拟一个“窗口中央显示动态统计信息”的版本,顺便展示怎么计算文本尺寸让它在客户区居中。

居中文本的一个笨办法是:先假设文本画出来差不多多少宽,然后硬编码 x。但这个方案一旦字体变了、窗口大小变了,位置就偏了。我比较推荐的做法:用GetTextExtentPoint32量出文本长度,用GetClientRect拿到窗口客户区尺寸,然后 (客户区宽 - 文本宽) / 2 得到居中的 x。这样不管窗口怎么拉伸,文本都在正中间。

case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); SetBkMode(hdc, TRANSPARENT); RECT rcClient; GetClientRect(hWnd, &rcClient); HFONT hFont = CreateFont(-28, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, _T("微软雅黑")); HFONT hOldFont = (HFONT)SelectObject(hdc, hFont); SetTextColor(hdc, RGB(60, 120, 200)); TCHAR szText[128]; wsprintf(szText, _T("窗口尺寸: %d x %d"), rcClient.right, rcClient.bottom); SIZE size; GetTextExtentPoint32(hdc, szText, _tcslen(szText), &size); int x = (rcClient.right - size.cx) / 2; int y = (rcClient.bottom - size.cy) / 2; TextOut(hdc, x, y, szText, _tcslen(szText)); SelectObject(hdc, hOldFont); DeleteObject(hFont); EndPaint(hWnd, &ps); return 0; }

这个程序配合WM_SIZE消息(窗口大小改变时发InvalidateRect)就能实现“窗口拉伸时文字实时居中”。每次窗口尺寸一变,系统自动重绘,TextOut 用最新的客户区尺寸重新计算居中坐标,视觉上就像文字始终钉在窗口正中。

GetTextExtentPoint32这个函数值得学会:它返回的是选用当前字体时文本的精确像素宽高。不量一下直接画,居中就无从谈起。

4.5 让文本“活”起来的完整机制梳理

综合上面几段,你应该能感受到一个完整的“动态文本显示”闭环了:

  1. 事件发生(鼠标移动、窗口尺寸变化、计时器触发、网络消息到达)
  2. 更新数据/缓冲区(wsprintf 把新值塞进全局字符数组)
  3. 使界面区域失效(InvalidateRect)
  4. 系统合并且发送 WM_PAINT
  5. 窗口过程在 WM_PAINT 里用 TextOut 把缓冲区的文本画出来

只要认清“数据更新”和“界面绘制”是解耦的,所有动态显示的问题都会变得很清晰。你不需要在每次数据变化时立刻上屏重画,交给系统的重绘机制去驱动就行。这也符合 Windows GUI 程序的事件驱动模型:画不画、什么时候画,由系统说了算,你只需要在应该画的时候画对。

5. 常见问题与排查技巧实录

5.1 文本乱码:编码不一致的老问题

现象:运行程序,汉字全部变成诡异的符号或者问号,英文正常。

排查思路分几步:

一,确认工程字符集。VS 里右键工程 -> 配置属性 -> 常规 -> 字符集,看看是“使用 Unicode 字符集”还是“使用多字节字符集”。

二,看字符串字面量是否带_T。如果你工程是 Unicode,但写了"你好"而不是_T("你好"),那这个字符串字面量是char*类型,wsprintf的%s期望wchar_t*,参数类型不匹配。输出乱码几乎是必然的。同理,如果工程是多字节,_T又会自动去掉L前缀,所以一路_T打天下是最省心的。

三,检查传给 TextOut 的长度参数是不是字符数。_tcslen返回的是字符数,不是字节数。如果你写sizeof(szBuf)这种字节数长度传给 TextOut,遇到中文字符时,字符数会被错误地多算(一个汉字 2 字节但 1 字符),TextOut 可能把结尾的 NULL 也算进去画,偶尔还会把缓冲区的残余内容也画出来。

我排查乱码问题的经验是:不要只看显示代码,先从数据本身查 —— 把g_szMousePos在调试器里展开看内容,如果数据在内存里已经是乱码,问题在格式化层面;如果内存数据正确、画出来乱码,问题在字体或 TextOut 参数层。

5.2 文字“闪”或“抖”:重绘太频繁,压低无效化频率

症状:鼠标一扫过快,文字就在原地闪烁,或者窗口拖动时文字像在抖。

原因基本是两个:重绘频率太高 + 全区域无效化。鼠标移动事件极频繁,如果每次 WM_MOUSEMOVE 都在没变化时也 InvalidateRect 整个客户区,系统就会反复擦背景、重画,肉眼看起来就是闪。

修复方案我上面已经给了:只有在坐标变化才触发重绘。更进一步的方案是只让文字所在区域失效,或者用双缓冲(背面上画完再整体 BitBlt 到屏幕)。但双缓冲属于 GDI 进阶内容,文本显示这种轻量场景一般不必要。

还有一个容易被忽略的细节:如果窗口类注册时背景画刷是(HBRUSH)GetStockObject(WHITE_BRUSH),那么每次 WM_PAINT 前系统都要用白色刷子刷一遍整个失效区域,这个动作本身就会造成闪烁感。如果不希望每次都擦背景,可以在注册窗口类时把背景设成NULL,然后在 WM_PAINT 里自己控制何处画背景。不过对新手,直接保持默认,控制好重绘频率就够了。

一个实用口诀:能小范围重绘就别全窗口重绘,能压低频率就别每次都画。

5.3 数据变了但界面不更新:忘记 InvalidateRect

现象:你在 WM_MOUSEMOVE 里更新了全局字符串,但屏幕上的文字纹丝不动。

因为你的 WM_PAINT 代码只在系统认为“有区域失效”的时候才会被调用。你改了字符串,但系统不知道,系统觉得客户区还是干净的,没必要发 WM_PAINT。所以数据更新之后必须主动调用InvalidateRect(hWnd, NULL, TRUE)告诉系统“这块区域其实脏了,请重画”。

这个机制对新手来说有点绕,记住一个铁律:任何影响界面显示的数据更新,后面跟一句 InvalidateRect。如果你想让更新立刻生效,不等消息循环自然处理,可以在 InvalidateRect 之后调用UpdateWindow(hWnd),这会直接给窗口发 WM_PAINT(如果存在无效区域),跳过消息队列排队。但一般场景里没必要这么着急。

有时候你 InvalidateRect 也调了,数据也格式化了,界面还是不更新 —— 那就要怀疑是不是 WM_PAINT 分支里提前 return 了,或者返回值不正确。WM_PAINT 处理完必须走return 0,不能用break跳到 DefWindowProc,否则系统会认为你没处理干净,又给你发一次 WM_PAINT,陷入无限重绘。很多“CPU 占用 100%”的奇怪问题就是这种原因。

5.4 用表格速查:TextOut + wsprintf 常见问题清单

症状可能原因解决方法
汉字乱码编码不一致,字面量没加 _T统一用_T;确认工程字符集
汉字乱码传到 TextOut 的长度是字节数改用_tcslen取字符数
文字画完即消失TextOut 不在 WM_PAINT 里把绘制代码挪进 WM_PAINT 分支
数据变了画面没变忘了 InvalidateRect数据更新后立即 InvalidateRect
画面闪烁重绘太频繁/全区域重绘加坐标变化判断;缩小失效区域
文字后面有白块背景模式不透明SetBkMode(hdc, TRANSPARENT)
程序崩溃缓冲区太小,wsprintf 越界缓冲区给 256/512 字节;换 StringCchPrintf
全窗口刷新卡顿每次移动都全窗口重绘用具体 RECT 局部失效
%s 显示乱码/崩溃ANSI/Unicode 混用%ls配宽字符;%s搭配同一编码
CPU 占用飙升WM_PAINT 无限循环检查是否错误使用 break 而非 return 0

5.5 一套稳得住的编程习惯建议

这篇文章的主题是 TextOut 和 wsprintf 的具体用法,但我更希望你带走的是几个能长期受用的习惯:

  • 字符缓冲区先声明成TCHAR szBuf[...],并且容量宁大勿小,这能省掉 80% 的文本乱码和越界问题。
  • 凡是格式化跟随字符串输出的地方,统一用_tcslen而不是lstrlen、strlen混着用。Windows 编程里越统一越不容易出细碎问题。
  • 绘制代码全部收进 WM_PAINT,数据代码放在事件处理里,二者之间只通过全局缓冲区或窗口附加数据传递,层次清晰,调试方便。
  • 涉及性能的地方,多想想“我真的需要整个窗口都重画吗”。刚开始性能不是问题,但代码习惯养成了,后面做大项目会少掉很多头发。

我自己后来做自绘菜单、自定义列表控件时,文本显示的核心思路还是这一套 —— 无非是 HDC 换成了内存 DC、TextOut 换成了 DrawText、格式化从 wsprintf 换成了更安全的函数,但“事件更新数据 -> 失效区域 -> 重绘时绘制”这个框架一直没变。把 TextOut + wsprintf 这章吃透,等于给你的 Win32 基本功打了一根很扎实的地桩。

6. 实操后分享:这段代码还能怎么扩展

案例本身已经完整跑通了,我最后再分享几个我当时顺手做的扩展,给想继续折腾的人一点方向感。

第一个扩展:把鼠标坐标的显示区域限定在客户区右下角的状态条风格区域里。做法是在 WM_PAINT 里先画一条细线或一个矩形模拟状态栏,再把 TextOut 画在对应的矩形区域顶部。这样你的程序看起来就有一点点“正规工具”的味道了。

第二个扩展:把字体句柄从 WM_PAINT 里每次都 CreateFont/DeleteObject,改成在 WM_CREATE 里创建一次、存成全局变量,在 WM_DESTROY 里 DeleteObject。这样每次重绘少两次系统调用,性能有微提升。但要注意字体对象在 WM_PAINT 之间要持续有效,所以不能创建在栈上自动释放。

第三个扩展:试试把 wsprintf 换成swprintf_s或者StringCchPrintf,观察一下安全性提升。特别是有一次我故意给%d传了一个字符串参数,wsprintf 版本直接摸到了一个非法地址崩了,安全版本立刻被编译器报出警告。这种对比会很直观地让你明白为什么现在新代码推荐安全函数。

TextOut 和 wsprintf 这套组合,说实话有点“古早味”,但现在 Windows 桌面上大量老软件的 UI 依然是这么画出来的。学它不只是为了应付作业或面试题,而是理解 GDI 文本描绘底层逻辑的一块敲门砖。掌握了它,后面学 DirectUI、学双缓冲、学自绘控件,都会顺很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询