news 2026/10/24 3:08:13

Win32 C++ 系统编程示例:窗口消息、GDI与调试排坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Win32 C++ 系统编程示例:窗口消息、GDI与调试排坑

简介:《精通Windows API——函数、接口、编程实例》配套源码包面向已具备C++基础、希望深入Win32桌面开发与系统级编程的读者,围绕窗口管理、消息机制、内存与文件操作、COM接口、动态库加载及多线程等核心主题,提供156个可对照实践的示例工程。压缩包共1789个文件,约24.29MB,包含C/C++源码、头文件、vcproj与sln工程、rc资源脚本、lib/dll库文件,以及编译产生的pdb、obj、ilk、idb等中间与调试文件,另有htm说明和manifest清单,便于还原编译环境、跟踪调试信息。已有376人学习,适合按章节拆解示例、验证API调用流程,并借助现成工程理解Win32项目组织与构建配置,提升调试与问题定位能力。

1. 从一次窗口无响应说起:这套 Win32 C++ 源码能给你什么

接手一个老工具,点下加载按钮后界面转圈三分钟不动,日志一行没有,任务管理器里线程挂在WaitForSingleObject上。这种时候没有 Win32 C++ 的底子,排查基本靠猜。这套源码干的就是去掉猜测:把窗口、消息、GDI、文件、进程线程、动态库这些系统接口,拆成一个个能单独编译运行的小例子,每个例子对着一个函数或一组函数,跑完就知道参数怎么填、返回什么、失败时错在哪。它不适合想直接复制粘贴出成品的人,适合已经会写 C++,但一碰系统层就发怵、想按图索骥把 API 语义啃下来的开发者。下面按工程骨架、核心模块、编译运行、踩坑排查、二次开发的顺序往下拆。

2. 工程骨架:从 WinMain 到消息循环,Win32 C++ 程序的骨架长什么样

刚接触 Win32 的人最容易卡在一个认知落差上:C++ 控制台程序从main开始,一行行往下走;Win32 图形程序从WinMain开始,但进去之后不是“往下走”,而是注册一个回调函数,交给系统反复调用。这个回调就是WndProc,整个程序的行为逻辑都堆在这里面。骨架看不透,后面所有示例都会读成一团乱麻。

2.1 示例怎么组织的:一个函数一个可编译单元

这类源码包常见做法是按功能分目录,而不是按章节排,例如basic/放窗口与消息,gdi/放绘图,file/放文件与注册表,process/放进程线程,dll/放动态库。每个.cpp独立成一个可编译单元,带自己的wWinMain,编译出来就是一个独立的 exe,不需要依赖别的示例。这种组织方式看着原始,但对学习特别友好:想验证CreateFileW的共享标志,就把file/那个例子单独编出来改两行,不用在一个大工程里翻半天入口。

公共部分通常抽一到两个头文件,比如common.h里放错误日志函数、字符集相关宏、简单的字符串转换,避免每个示例都重写一遍。

提示:如果目录里出现同时带_ansi和_unicode后缀的两个文件,多半是同一个示例的两种字符集版本,先编 unicode 版本,能省掉后面一半的乱码问题。

判断一个示例能不能单独跑,看两点:有没有自己的入口函数,有没有包含windows.h之外的私有依赖。两点都满足,就可以直接丢进命令行编译。

2.2 注册窗口类与创建窗口:WNDCLASSEX 里每个字段都在管什么

窗口不是new出来的,是向系统“注册一个类”再“按类创建实例”。这一步是骨架里最容易漏的地方,尤其cbSize这个字段,漏赋值在 Debug 下有时还能跑,Release 下直接创建失败,属于典型的玄学现场。

#include <windows.h> LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int nCmdShow) { WNDCLASSEXW wc = { sizeof(wc) }; // cbSize 必须填,结构体版本靠它区分 wc.style = CS_HREDRAW | CS_VREDRAW; // 窗口横向/纵向变化时整体重绘 wc.lpfnWndProc = WndProc; // 消息回调,系统通过它找你 wc.hInstance = hInst; // 所属模块实例 wc.hCursor = LoadCursorW(nullptr, IDC_ARROW); wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // 系统色,不用自己 DeleteObject wc.lpszClassName = L"DemoWindowClass"; // 类名,创建窗口时要对得上 if (!RegisterClassExW(&wc)) return 1; // 失败通常是类名重复或 cbSize 错了 HWND hWnd = CreateWindowExW( 0, L"DemoWindowClass", L"Demo", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, nullptr, nullptr, hInst, nullptr); if (!hWnd) return 1; // 返回 NULL 时配合 GetLastError 看原因 ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); // 强制立刻发一次 WM_PAINT MSG msg; while (GetMessageW(&msg, nullptr, 0, 0) > 0) { // 0 表示收到 WM_QUIT,-1 表示出错 TranslateMessage(&msg); // 把按键消息翻译成字符消息 DispatchMessageW(&msg); // 投递给 WndProc } return (int)msg.wParam; }

几个参数值得单独拎出来说。wc.hbrBackground用COLOR_WINDOW + 1这种系统色索引时,画刷归系统所有,绝对不能DeleteObject;只有自己CreateSolidBrush出来的才需要删。CreateWindowExW的第一个参数是扩展样式,WS_EX_CLIENTEDGE、WS_EX_TOPMOST这类都填这儿,普通窗口填 0 就行。UpdateWindow不能省,不然窗口第一次显示时可能是一片空白,得等你拖动一下才刷出来。

GetMessageW的返回值判断是个高频翻车点,写成while (GetMessage(...))看着没问题,但函数出错返回 -1 时是“非零”,循环会继续,然后拿着无效的MSG往下跑,表现就是程序莫名奇妙卡死或崩在DispatchMessage里。

2.3 WndProc 的分发逻辑与返回值约定

WndProc的签名固定:四个参数分别是窗口句柄、消息 ID、两个消息相关的附加参数。返回值语义是“我处理结果如何”,返回 0 通常表示已处理,交给DefWindowProcW表示“我不管,系统你自己看着办”。

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_PAINT: OnPaint(hWnd); // 绘制逻辑单独抽函数,别在 switch 里堆 return 0; // 已处理,不再往下传 case WM_SIZE: OnResize(LOWORD(lParam), HIWORD(lParam)); // 宽高从 lParam 拆 return 0; case WM_COMMAND: // LOWORD(wParam) 是控件或菜单 ID,HIWORD(wParam) 是通知码 OnCommand(LOWORD(wParam), HIWORD(wParam), (HWND)lParam); return 0; case WM_DESTROY: PostQuitMessage(0); // 投递 WM_QUIT,让消息循环退出 return 0; } return DefWindowProcW(hWnd, msg, wParam, lParam); }

WM_DESTROY和WM_CLOSE的区别要分清:WM_CLOSE是用户点右上角叉,默认行为是销毁窗口;WM_DESTROY是窗口已经在销毁了,此时该做清理并向消息循环发退出信号。想在关闭前弹个“确定退出吗”的确认框,拦WM_CLOSE,不是拦WM_DESTROY。

WM_COMMAND里的lParam是控件句柄,wParam高低位拆开分别是控件 ID 和通知码,按钮点击时通知码是BN_CLICKED。这套编码方式现在看很不现代,但它是 Win32 所有控件交互的统一入口,读懂它,菜单、按钮、加速键就都能对上号了。

2.4 字符集:UNICODE 宏与 W 后缀到底怎么选

源码里同一个函数常出现CreateFileA和CreateFileW两个版本,CreateFile这个不带后缀的名字是宏,编译期根据有没有定义UNICODE展开成其中一个。工程里同时定义UNICODE和_UNICODE,前者管 Windows API,后者管 C 运行库,只定义一个就会出现一半宽字符一半窄字符的诡异状态。

我的习惯是新代码一律走宽字符,字符串字面量统一加L前缀,参数类型用wchar_t*或std::wstring。跟外部库交互需要char*时,在边界处做一次WideCharToMultiByte转换,别让两种类型在业务代码里混着流。这套源码里的示例如果两种版本都有,先看_unicode那份,把宽字符的路子走顺,窄字符版本当对照看就行。

TCHAR、_T("...")这套兼容宏是老工程的历史包袱,新写代码不太建议继续用,读的时候知道它会展开成什么就够了。

3. 核心模块拆解:句柄、GDI 绘制与文件访问的实例代码

骨架跑通之后,剩下的内容基本是“系统资源怎么拿、怎么用、怎么还”。Win32 的资源和 C++ 的 RAII 思路完全不同,全靠手动管理,一套示例看下来,真正能带走的能力就是养成“拿了就还”的肌肉记忆。

3.1 句柄类型与释放规则:谁创建谁释放

句柄本质是个不透明的整数,你不需要知道它内部长什么样,但必须知道它归谁管、用什么函数还回去。这套源码里出现的句柄类型大致就这几类:

句柄类型典型来源释放方式忘记释放的后果
HWNDCreateWindowExDestroyWindow窗口一直存在,进程退出前不回收
HDCGetDC/BeginPaintReleaseDC/EndPaintGDI 资源耗尽,界面开始画不出来
HBITMAPHPENHBRUSHCreatePen/CreateCompatibleBitmapDeleteObject进程 GDI 句柄上限一万,越用越卡
HANDLECreateFile/CreateThreadCloseHandle文件被占用删不掉,句柄数持续上涨
HMODULELoadLibraryFreeLibraryDLL 引用计数不归零,卸不掉

有个反直觉的点:SelectObject把画笔选进 DC 之后,这支笔就处于“正在使用”状态,此时DeleteObject会直接失败,返回 FALSE,而且不报错。必须先把旧的选回去,再删。这是 GDI 泄漏最常见的成因。

3.2 GDI 绘制:从 BeginPaint 到双缓冲

直接在WM_PAINT里往窗口 DC 上画,窗口一拖动、一遮挡就闪成一片。示例里通常会给一个双缓冲版本,先在内存位图上画完,再一次性贴到屏幕上。

static void OnPaint(HWND hWnd) { PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); // 必须与 EndPaint 配对,别用 GetDC 代替 RECT rc; GetClientRect(hWnd, &rc); int w = rc.right - rc.left, h = rc.bottom - rc.top; HDC memDC = CreateCompatibleDC(hdc); HBITMAP bmp = CreateCompatibleBitmap(hdc, w, h); // 要兼容屏幕 DC,不是 memDC HBITMAP oldBmp = (HBITMAP)SelectObject(memDC, bmp); FillRect(memDC, &rc, (HBRUSH)GetStockObject(WHITE_BRUSH)); HPEN pen = CreatePen(PS_SOLID, 2, RGB(200, 0, 0)); HPEN oldPen = (HPEN)SelectObject(memDC, pen); MoveToEx(memDC, 0, 0, nullptr); LineTo(memDC, w, h); SelectObject(memDC, oldPen); // 先还原,pen 才删得掉 DeleteObject(pen); BitBlt(hdc, 0, 0, w, h, memDC, 0, 0, SRCCOPY); // 一次性贴屏 SelectObject(memDC, oldBmp); DeleteObject(bmp); DeleteDC(memDC); EndPaint(hWnd, &ps); }

CreateCompatibleBitmap(hdc, ...)的第一个参数必须是屏幕 DC,传memDC会得到一张单色位图,画出来全是黑白,这个坑很隐蔽,因为代码不报错,只是颜色不对。BitBlt的最后一个参数SRCCOPY是光栅操作码,直接覆盖;要做半透明叠加就得换成AlphaBlend,但那需要目标窗口支持分层。

窗口尺寸变化时,离屏位图要跟着重建,位置一般放在WM_SIZE里,把旧的DeleteObject掉再建新的。如果只在WM_PAINT里建,每次重绘都创建一张新位图,很快就泄漏。

3.3 文件读写与 GetLastError:错误码必须当场取

Win32 的错误处理套路统一:函数返回失败标识,紧接着调GetLastError()。关键是“紧接着”,中间只要插进任何一次别的 API 调用,错误码就可能被覆盖,这是排查问题时的黑匣子级细节。

bool ReadWholeFile(const wchar_t* path, std::string& out, DWORD& err) { HANDLE h = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (h == INVALID_HANDLE_VALUE) { err = GetLastError(); return false; } LARGE_INTEGER size{}; if (!GetFileSizeEx(h, &size)) { err = GetLastError(); CloseHandle(h); return false; } out.resize((size_t)size.QuadPart); DWORD read = 0; BOOL ok = ReadFile(h, out.data(), (DWORD)out.size(), &read, nullptr); err = ok ? ERROR_SUCCESS : GetLastError(); // 立刻取,别等 CloseHandle 之后 CloseHandle(h); return ok && read == out.size(); }

几个参数值得说清楚。dwShareMode填FILE_SHARE_READ表示别人还能以只读打开这个文件;填 0 表示独占,此时如果别的程序已经打开它,你这儿会拿到ERROR_SHARING_VIOLATION,这个错误码在排查“文件明明存在却打不开”时几乎必现。dwCreationDisposition用OPEN_EXISTING是“有就开、没有就失败”,想“没有就创建”换成OPEN_ALWAYS。

ReadFile的第四個参数是实际读到的字节数,可能小于请求的字节数,尤其在读管道、读网络流的时候。文件读取一般能一次读完,但严谨的写法仍要比较一下,示例里如果直接忽略这个值,说明它假设了同步文件和本地磁盘。

错误码转文字别自己查表,用FormatMessageW直接转,支持多语言的系统上会给出本地化描述。

3.4 动态库导出与运行时加载

dll/目录下的例子一般演示两件事:怎么把函数导出去,怎么在运行时按名字找回来。导出端加extern "C"是为了关掉 C++ 的名字修饰,否则GetProcAddress里要填一大串带@@的符号名,非常难维护。

// 导出端 mathlib.cpp extern "C" __declspec(dllexport) int __stdcall Add(int a, int b) { return a + b; } // 调用端 HMODULE h = LoadLibraryW(L"mathlib.dll"); if (h) { using AddFn = int(__stdcall*)(int, int); // 调用约定必须与导出端一致 auto fn = (AddFn)GetProcAddress(h, "Add"); if (fn) int r = fn(1, 2); FreeLibrary(h); }

调用约定不一致在 x86 上会让栈失衡,表现为函数返回后程序直接崩,而且崩的位置离出错点很远;x64 上只有一种调用约定,这个问题自动消失。所以这类问题常常“32 位编出来必崩,64 位跑得好好的”,排查时先看位数。

4. 编译与运行:把示例逐个跑起来的三条路径

源码包给的多半是散的.cpp文件,没有现成工程文件,也不一定带构建脚本。这时候别急着一个一个手动点,先确定工具链,再决定用命令行还是构建系统批量编。

4.1 工具链选择:cl.exe 与 MinGW-w64 的差异

两条路都能走通。MSVC 的cl.exe对 Windows SDK 的适配最顺,windows.h里的新结构体、新宏基本不缺;MinGW-w64 的g++免费、轻量,但在一些较新的 API 声明上可能滞后,遇到某个结构体找不到定义,多半是 SDK 版本问题而不是代码问题。

选择建议很直接:主要做 Windows 桌面开发,用cl.exe;只是想把示例跑起来验证概念,MinGW-w64 装起来更快。两边都可以在同一台机器上共存,用不同的命令行环境切换。

4.2 命令行逐个编译与链接库对照

用cl.exe编一个窗口程序,命令长这样:

cl /nologo /EHsc /W4 /DUNICODE /D_UNICODE demo.cpp ^ /link /SUBSYSTEM:WINDOWS user32.lib gdi32.lib kernel32.lib comctl32.lib

/EHsc开标准 C++ 异常处理,/W4把警告开到较高级别,/DUNICODE /D_UNICODE定义字符集宏。/SUBSYSTEM:WINDOWS告诉链接器这是个 GUI 程序,入口是wWinMain而不是main;不加这个参数,链接器会去找main,然后报一个“找不到入口点”的错。

需要链接哪些库,看代码里用了哪些 API:

用到的 API需要链接的库
窗口、消息、菜单user32.lib
绘图、字体、位图gdi32.lib
文件、进程、线程、注册表kernel32.lib(多数情况自动链)
通用控件、任务对话框comctl32.lib
外壳函数、文件对话框shell32.libcomdlg32.lib
COM 初始化与接口ole32.lib

链接错误LNK2019: unresolved external symbol,先查这个表,九成是少了个.lib。

MinGW-w64 的等价命令:

g++ -municode -mwindows demo.cpp -o demo.exe -luser32 -lgdi32 -lcomctl32

-municode对应宽字符入口wWinMain,-mwindows对应 GUI 子系统。漏掉-municode会报undefined reference to WinMain,这个报错信息很误导人,实际原因是入口函数签名对不上。

4.3 CMake 统一构建:一次配置,批量编译

示例一多,一条条敲命令就不现实了。写一个顶层CMakeLists.txt,把每个.cpp扫成一个 target:

cmake_minimum_required(VERSION 3.15) project(win32_demos CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) file(GLOB DEMO_SOURCES CONFIGURE_DEPENDS "basic/*.cpp" "gdi/*.cpp" "file/*.cpp") foreach(src ${DEMO_SOURCES}) get_filename_component(name ${src} NAME_WE) add_executable(${name} WIN32 ${src}) target_compile_definitions(${name} PRIVATE UNICODE _UNICODE) target_link_libraries(${name} PRIVATE user32 gdi32 comctl32 shell32 ole32) endforeach()

add_executable里的WIN32关键字等价于设置WIN32_EXECUTABLE属性,CMake 会自动加上/SUBSYSTEM:WINDOWS。CONFIGURE_DEPENDS让 CMake 每次构建时重新扫目录,新增.cpp不用手动重跑配置。

注意:file(GLOB ...)在正式项目里一般不建议用,因为依赖关系不明确、增量构建容易漏文件;但用来批量编译一堆互不相关的教学示例,它是最省事的方式。

4.4 调试:OutputDebugString 与断点配合

GUI 程序没有控制台,printf看不到输出。最轻量的办法是OutputDebugStringW,输出会进调试器的“输出”窗口,不挂调试器时可以用轻量的日志查看工具接。

void LogLastError(const wchar_t* where) { DWORD e = GetLastError(); wchar_t* buf = nullptr; FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, e, 0, (LPWSTR)&buf, 0, nullptr); OutputDebugStringW(where); OutputDebugStringW(L": "); OutputDebugStringW(buf ? buf : L"(unknown)"); OutputDebugStringW(L"\n"); if (buf) LocalFree(buf); // FORMAT_MESSAGE_ALLOCATE_BUFFER 分配的要用 LocalFree }

FORMAT_MESSAGE_ALLOCATE_BUFFER让函数自己分配缓冲区,所以传的是指针的地址,用完必须LocalFree,不能delete。FORMAT_MESSAGE_IGNORE_INSERTS是防止错误文本里的%1占位符被当成格式化参数处理。

断点方面,WndProc里断在switch上,可以看每条消息的到达顺序。窗口创建过程中的WM_CREATE、WM_SIZE、WM_PAINT顺序是固定的,看到不符合预期的顺序,基本就能定位到是哪一步的返回值写错了。

5. 踩坑排查:Win32 示例跑不起来时先看这几处

这类代码的血泪经验是:编译过了不代表能跑,跑起来了不代表行为对。下面几条是我在照着示例改代码时反复撞上的,每条都按“现象—原因—解决”记下来,遇到了直接对号入座。

5.1 链接报 LNK2019 找不到入口点或符号

现象:编译阶段干净,链接阶段报unresolved external symbol WinMain或者某个 API 的名字。

原因:入口点和子系统不匹配,或者缺库。写了wWinMain但没加/SUBSYSTEM:WINDOWS,链接器按控制台程序去找main;反过来写了WinMain(窄字符)却定义了UNICODE,预处理后入口函数名变了,同样找不到。

解决:先看用的是WinMain还是wWinMain,再决定要不要/SUBSYSTEM:WINDOWS和/DUNICODE。符号找不到就查第 4 章那张库对照表,GetProcAddress、RegOpenKeyEx这类都对应着固定库。

5.2 CreateWindowEx 返回 NULL,什么都显示不出来

现象:程序启动后没有任何窗口,进程却活着,或者直接退出。

原因:九成是窗口类没注册成功,或者类名对不上。WNDCLASSEX的cbSize漏赋值会让RegisterClassEx静默失败;wc.lpszClassName和CreateWindowEx里传的字符串差一个字符,也会创建失败。

解决:每个失败分支都加上LogLastError,RegisterClassExW失败通常是ERROR_CLASS_ALREADY_EXISTS(类名重复注册)或者ERROR_INVALID_PARAMETER(结构体字段有问题)。把所有字符串字面量统一加L前缀,能排除掉字符集不一致导致的类名匹配失败。

5.3 界面上中文全是问号或方块

现象:按钮、标题栏上的中文显示成???,或者一串方块。

原因:工程用窄字符 API(CreateWindowExA)传了 UTF-8 或 GBK 的字节串,系统按当前代码页解释,对不上就变成问号。另一种情况是用了宽字符 API,但字体选的是不支持中文的字体。

解决:所有字符串统一走宽字符,字面量加L,API 用W后缀版本;从配置文件、网络读进来的 UTF-8 文本,进 UI 前用MultiByteToWideChar(CP_UTF8, ...)转一次。字体用CreateFontW时字符集参数填DEFAULT_CHARSET,让系统自己挑。

5.4 界面用久了越来越卡,最后画不出来

现象:程序跑几十分钟后界面刷新变慢,任务管理器里 GDI 对象数一直涨。

原因:GDI 对象泄漏。典型是CreatePen之后SelectObject进 DC,用完直接DeleteObject,因为还在 DC 里选着,删除失败但不报错;或者GetDC拿了 DC 却没ReleaseDC。

解决:养成“先 SelectObject 还原旧对象,再 DeleteObject”的固定顺序,选中的旧对象句柄必须保存下来。GetDC/ReleaseDC成对出现,能在WM_PAINT里解决的绘制不要用GetDC。任务管理器的“GDI 对象”列可以直接观察,正常程序这个数字应该稳定在一个区间内。

5.5 Debug 下正常,Release 下崩溃

现象:同一份代码,Debug 配置跑得好好的,切到 Release 直接崩,或者行为完全不对。

原因:未初始化的局部变量。Debug 下编译器会把栈填成特定值(常见是0xCC),某些“碰巧能用”的代码就蒙对了;Release 下栈上是上次留下的垃圾数据,指针、长度、结构体字段全都是随机值。另一个常见原因是结构体的cbSize字段只在某些构造方式下被赋值。

解决:所有 Win32 结构体一律用= { sizeof(x) }或ZeroMemory初始化,别指望栈上的默认值。变量声明时就给初值,尤其是DWORD、LONG这类看不出“未初始化”的整型。Release 下开/W4甚至/Wall,编译器对“可能未初始化”的警告命中率相当高。

6. 二次开发:从跑通示例到做出自己的小工具

示例跑通之后,真正有价值的一步是挑一个最接近你需求的例子,把它改成能用的东西。我的做法是先别急着加功能,把原示例的WndProc拆成“消息分发 + 业务处理”两层:WndProc只负责把消息转给对应的成员函数,业务逻辑全放到一个自己写的类里。改完之后窗口行为一模一样,但后续加控件、加线程、加配置都只是给这个类加方法。

具体的改造路径一般是这样:先把WinMain和WndProc收进一个MainWindow类,hWnd存成成员变量;再把绘制、命令处理、尺寸变化各自拆成方法;最后把资源创建放在WM_CREATE、释放放在WM_DESTROY,形成对称。这套结构一旦立起来,后面不管加多线程读文件还是加托盘图标,都不会把代码搅成一锅粥。

改动位置和验证方式可以对着这张表走:

想改的地方动哪个函数怎么验证改对了
窗口初始大小、样式CreateWindowExW的样式与尺寸参数启动后看窗口边框、能否缩放
自定义绘制WM_PAINT里的绘制函数拖动窗口、最小化再还原,看是否闪烁
加一个按钮WM_CREATE里CreateWindowExW(L"BUTTON", ...)点击后WM_COMMAND是否命中对应 ID
后台读文件新建工作线程 +PostMessage回主线程读大文件时界面不卡,进度能刷新
退出前清理WM_DESTROY任务管理器里句柄数、GDI 对象数是否归零

后台线程那块要特别注意:工作线程绝对不能直接碰窗口控件,所有 UI 更新都得通过PostMessage把结果投回主线程,让WndProc去改。直接跨线程调SetWindowText在多数情况下“看起来能用”,但那是个定时炸弹,压力一上来就随机崩,崩的位置还跟真正的错误点没关系。

验证改动是否干净,我一般看两个指标:程序连续开关二十次,任务管理器里的句柄数和 GDI 对象数回到启动时的水平;把窗口反复拉伸到极限尺寸,不闪、不黑、不错位。这两条过不了,说明还有资源没还回去或者绘制路径有问题,功能加得再多也是空中楼阁。

踩过最贵的一次坑是省了SelectObject的还原,测试阶段完全正常,上线跑了两天开始有用户反馈界面变白要重启——那之后我每次写完一段 GDI 代码,都强制走一遍“创建、选入、还原、删除”的顺序检查,缺一步就不提交。希望这些能帮到你,少走一遍我走过的弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 3:07:49

SKP转GLTF:三维格式转换的语义对齐与工程化实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 3:07:33

PCA9422与R7FA6E2BB3CFM电源管理方案设计与调试实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 3:06:58

socat:字节流协议桥接与网络调试的终极工具

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 3:05:45

跨境电商采购砍单的技术根因与风控技术栈

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华