一、题目运行Windows应用程序在桌面显示Windows窗口。窗口内背景色为灰色且窗口中居中显示“大家好这是我的第一个Windows API程序”同时播放背景音乐并可通过程序改变窗口显示风格为只有标题栏以及鼠标指针形状为手型。二、源代码#include windows.h #include string #include mmsystem.h #pragma comment(lib, winmm.lib) #define ID_BTN_STYLE 1001 LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { static HWND hBtn NULL; static bool bTitleOnly false; switch (uMsg) { case WM_CREATE: hBtn CreateWindow( LBUTTON, L切换窗口风格, WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 20, 20, 150, 30, hwnd, (HMENU)ID_BTN_STYLE, ((LPCREATESTRUCT)lParam)-hInstance, NULL); return 0; case WM_COMMAND: if (LOWORD(wParam) ID_BTN_STYLE) { bTitleOnly !bTitleOnly; LONG_PTR style GetWindowLongPtr(hwnd, GWL_STYLE); if (bTitleOnly) style WS_CAPTION | WS_SYSMENU | WS_VISIBLE; // ← 加 WS_VISIBLE else style WS_OVERLAPPEDWINDOW | WS_VISIBLE; // ← 也加上 SetWindowLongPtr(hwnd, GWL_STYLE, style); SetWindowPos(hwnd, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); InvalidateRect(hwnd, NULL, TRUE); // 让整个客户区作废重绘 UpdateWindow(hwnd); // 立即重绘 } return 0; case WM_SIZE: { // 让整个客户区重绘 InvalidateRect(hwnd, NULL, TRUE); // 根据窗口大小重新摆放按钮 if (hBtn) { int cx LOWORD(lParam); // 客户区宽度 int cy HIWORD(lParam); // 客户区高度 // 按钮宽高按客户区比例算 int btnW cx / 4; int btnH cy / 10; if (btnW 80) btnW 80; if (btnH 24) btnH 24; // 按钮放在左上角边距按比例 int margin cx / 40; MoveWindow(hBtn, margin, margin, btnW, btnH, TRUE); } return 0; } case WM_CLOSE: PlaySound(NULL, NULL, 0); // 关窗口前先掐掉声音 DestroyWindow(hwnd); // 手动销毁窗口 return 0; case WM_DESTROY: PlaySound(NULL, NULL, 0); // 再停一次保险 PostQuitMessage(0); return 0; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hwnd, ps); HBRUSH hBrush CreateSolidBrush(RGB(192, 192, 192)); FillRect(hdc, ps.rcPaint, hBrush); DeleteObject(hBrush); SetTextColor(hdc, RGB(0, 0, 0)); SetBkMode(hdc, TRANSPARENT); RECT rect; GetClientRect(hwnd, rect); // 按窗口宽度的 1/20 算字号范围限制在 16~72 int fontSize (rect.right - rect.left) / 20; if (fontSize 16) fontSize 16; if (fontSize 72) fontSize 72; HFONT hFont CreateFont(fontSize, 0, 0, 0, FW_BOLD, 0, 0, 0, DEFAULT_CHARSET, 0, 0, 0, 0, L微软雅黑); HFONT hOld (HFONT)SelectObject(hdc, hFont); const std::wstring text L大家好这是我的第一个Windows API程序; DrawTextW(hdc, text.c_str(), -1, rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER); SelectObject(hdc, hOld); DeleteObject(hFont); EndPaint(hwnd, ps); return 0; } case WM_SETCURSOR: { if (LOWORD(lParam) HTCLIENT) { SetCursor(LoadCursor(NULL, IDC_HAND)); return TRUE; } break; } } return DefWindowProc(hwnd, uMsg, wParam, lParam); } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { PlaySound(TEXT(hellowin.wav), NULL, SND_ASYNC | SND_FILENAME ); const wchar_t CLASS_NAME[] LSample Window Class; WNDCLASS wc {}; wc.lpfnWndProc WindowProc; wc.hInstance hInstance; wc.lpszClassName CLASS_NAME; RegisterClass(wc); int screenWidth GetSystemMetrics(SM_CXSCREEN); int screenHeight GetSystemMetrics(SM_CYSCREEN); const int windowWidth 500; const int windowHeight 400; int startX (screenWidth - windowWidth) / 2; int startY (screenHeight - windowHeight) / 2; HWND hwnd CreateWindowEx( 0, CLASS_NAME, L我的第一个Windows API程序, WS_OVERLAPPEDWINDOW, startX, startY, windowWidth, windowHeight, NULL, NULL, hInstance, NULL ); ShowWindow(hwnd, nShowCmd); UpdateWindow(hwnd); MSG msg; while (GetMessage(msg, nullptr, 0, 0)) { TranslateMessage(msg); DispatchMessage(msg); } return 0; }三、源代码思路解析1 头文件与库#include windows.h // Windows API 基础 #include string // std::wstring #include mmsystem.h // PlaySound 等多媒体 API #pragma comment(lib, winmm.lib) // 链接 winmm.libmmsystem.h提供PlaySound声明#pragma comment让链接器自动链接winmm.lib无需手动在项目属性中添加。2 窗口过程函数 WindowProcWindowProc是窗口的消息处理核心所有发往该窗口的消息都在这里分发处理。1WM_CREATE创建按钮case WM_CREATE: hBtn CreateWindow( LBUTTON, L切换窗口风格, WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 20, 20, 150, 30, hwnd, (HMENU)ID_BTN_STYLE, ((LPCREATESTRUCT)lParam)-hInstance, NULL); return 0;窗口创建时在客户区左上角创建一个子按钮。WS_CHILD | WS_VISIBLE表示它是可见的子窗口BS_PUSHBUTTON是普通按钮样式。按钮 ID 为ID_BTN_STYLE供WM_COMMAND识别。2WM_COMMAND响应按钮切换窗口风格case WM_COMMAND: if (LOWORD(wParam) ID_BTN_STYLE) { bTitleOnly !bTitleOnly; LONG_PTR style GetWindowLongPtr(hwnd, GWL_STYLE); if (bTitleOnly) style WS_CAPTION | WS_SYSMENU | WS_VISIBLE; else style WS_OVERLAPPEDWINDOW | WS_VISIBLE; SetWindowLongPtr(hwnd, GWL_STYLE, style); SetWindowPos(hwnd, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); InvalidateRect(hwnd, NULL, TRUE); UpdateWindow(hwnd); } return 0;GetWindowLongPtr(hwnd, GWL_STYLE)取出当前窗口风格位。WS_CAPTION | WS_SYSMENU表示只有标题栏和关闭按钮无边框、无最大化最小化。WS_OVERLAPPEDWINDOW恢复为普通可缩放窗口。关键点必须加WS_VISIBLE否则整体替换风格位时会把可见标志抹掉导致窗口进入逻辑不可见状态按钮失灵。SetWindowPos(..., SWP_FRAMECHANGED)让新边框立即生效。InvalidateRect(hwnd, NULL, TRUE)UpdateWindow强制整个客户区重绘避免切换风格后残留旧图像。3WM_SIZE尺寸变化时全客户区重绘case WM_SIZE: InvalidateRect(hwnd, NULL, TRUE); return 0;最大化、还原、拖边框等操作会触发WM_SIZE。这里让整个客户区作废并重绘防止文字残影。4WM_CLOSE关闭前停止音乐case WM_CLOSE: PlaySound(NULL, NULL, 0); DestroyWindow(hwnd); return 0;先停止音乐再手动调用DestroyWindow销毁窗口。因为接管了WM_CLOSE默认销毁逻辑不再执行必须自己调用DestroyWindow。5WM_DESTROY退出消息循环case WM_DESTROY: PlaySound(NULL, NULL, 0); PostQuitMessage(0); return 0;PostQuitMessage(0)向消息队列投递WM_QUIT使WinMain中的GetMessage返回 0退出消息循环进程结束。6WM_PAINT绘制背景与文字case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hwnd, ps); HBRUSH hBrush CreateSolidBrush(RGB(192, 192, 192)); FillRect(hdc, ps.rcPaint, hBrush); DeleteObject(hBrush); SetTextColor(hdc, RGB(0, 0, 0)); SetBkMode(hdc, TRANSPARENT); RECT rect; GetClientRect(hwnd, rect); const std::wstring text L大家好这是我的第一个Windows API程序; DrawTextW(hdc, text.c_str(), -1, rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER); EndPaint(hwnd, ps); return 0; }BeginPaint/EndPaint成对使用获取和释放设备上下文。CreateSolidBrush(RGB(192,192,192))创建灰色画刷FillRect填充客户区。SetBkMode(TRANSPARENT)让文字背景透明避免出现文字底色框。GetClientRect获取客户区矩形配合DT_CENTER | DT_VCENTER实现水平垂直居中。不使用DT_NOCLIP防止文字被绘制到区域外产生残影。7WM_SETCURSOR手型光标case WM_SETCURSOR: { if (LOWORD(lParam) HTCLIENT) { SetCursor(LoadCursor(NULL, IDC_HAND)); return TRUE; } break; }lParam低字为命中测试码。若鼠标位于客户区HTCLIENT把光标设为手型并返回TRUE表示消息已处理否则交给DefWindowProc处理例如边框上的缩放光标。3 入口函数 WinMainPlaySound(TEXT(hellowin.wav), NULL, SND_ASYNC | SND_FILENAME);SND_FILENAME表示第一个参数是文件名。SND_ASYNC异步播放不阻塞主线程。不加SND_LOOP因此只播放一次适合开机音效。WNDCLASS wc {}; wc.lpfnWndProc WindowProc; wc.hInstance hInstance; wc.lpszClassName CLASS_NAME; RegisterClass(wc);注册窗口类指定窗口过程、实例句柄和类名。int screenWidth GetSystemMetrics(SM_CXSCREEN); int screenHeight GetSystemMetrics(SM_CYSCREEN); int startX (screenWidth - windowWidth) / 2; int startY (screenHeight - windowHeight) / 2;通过GetSystemMetrics取屏幕宽高计算窗口左上角坐标使窗口在屏幕上居中。HWND hwnd CreateWindowEx( 0, CLASS_NAME, L我的第一个Windows API程序, WS_OVERLAPPEDWINDOW, startX, startY, windowWidth, windowHeight, NULL, NULL, hInstance, NULL);创建窗口初始风格为WS_OVERLAPPEDWINDOW普通可缩放窗口。MSG msg; while (GetMessage(msg, nullptr, 0, 0)) { TranslateMessage(msg); DispatchMessage(msg); }标准消息循环GetMessage取消息TranslateMessage翻译键盘消息DispatchMessage将消息分发到WindowProc。当收到WM_QUIT时循环结束程序退出。四、遇到的问题以及解决方案问题 1无法解析的外部符号 main这两个错误连在一起看原因很明确LNK2019: 无法解析的外部符号 main→链接器在找main函数说明当前是控制台子系统LNK1120: 1 个无法解析的外部命令→ 上面那个错误的连带结果也就是说项目被设成了控制台 (/SUBSYSTEM:CONSOLE)但代码里写的是WinMain链接器找不到main就报错。至于那个C28251警告WinMain注释不一致只是警告不是致命错误可以先不管。解决办法改子系统项目 → 属性或右键项目 → 属性→配置属性 → 链接器 → 系统→ 把子系统从控制台 (/SUBSYSTEM:CONSOLE)改成窗口 (/SUBSYSTEM:WINDOWS)→ 确定 → 重新生成。改完后重新按CtrlShiftB生成应该就没这两个链接错误了。顺手确认另外两项避免改完又出新错① 字符集项目 → 属性 → 配置属性 →高级 → 字符集→ 选使用 Unicode 字符集。代码里用了std::wstring、L...、DrawTextW必须是 Unicode。如果是多字节字符集L...和DrawTextW会有类型不匹配的错。② 入口点一般不用改如果改成窗口子系统后链接器又报WinMain找不到那就再设一下入口点项目 → 属性 → 链接器 →高级 → 入口点→ 填WinMainCRTStartup。正常情况下改成窗口子系统后MSVC 会自动匹配WinMain不需要手动设入口点。正确状态成功生成exe文件问题 2关闭窗口后音乐仍在播放现象点击窗口右上角 × 后窗口关闭但开机音效仍在循环播放orz。原因一开始代码只在WM_DESTROY里调用PlaySound(NULL,NULL,0)且播放时带了SND_LOOP停止不够及时。另外当时项目里可能残留了多个 exe 进程。解决播放时去掉SND_LOOP改为只播放一次。在WM_CLOSE里也调用PlaySound(NULL, NULL, 0)先停声再销毁窗口。WM_DESTROY里再次停止形成双保险。用任务管理器清理所有残留的Project1.exe进程。问题 3点击切换窗口风格后窗口卡住、按钮失灵现象点击按钮后窗口变成只有标题栏但按钮无法再点击整个界面像卡死。原因切换风格时用style WS_CAPTION | WS_SYSMENU;整体替换了窗口风格位把WS_VISIBLE可见标志一并抹掉窗口进入不可见但未销毁的状态子按钮随之异常。解决在切换风格时补回WS_VISIBLEstyle WS_CAPTION | WS_SYSMENU | WS_VISIBLE; style WS_OVERLAPPEDWINDOW | WS_VISIBLE;或者更稳妥地只修改需要改的风格位而不整体替换。问题 4全屏后或者变动窗口出现两个大家好文字残影现象最大化窗口或还原后客户区同时出现两个大家好……居中那个是正常的另一个是旧图像的残留。原因DrawTextW使用了DT_NOCLIP允许文字绘制到矩形区域之外。WM_PAINT中FillRect只填充ps.rcPaint系统指定的重绘区域未覆盖的部分保留旧内容。窗口尺寸变化时未强制全客户区重绘。解决去掉DT_NOCLIP让文字被限制在rect内。在WM_SIZE消息中调用InvalidateRect(hwnd, NULL, TRUE)使尺寸变化时整个客户区作废重绘。在切换风格后同样调用InvalidateRect UpdateWindow。五、最终实现效果起始效果切换窗口风格全屏效果任意尺寸六、实验总结本次实验完整地实现了一个具备多项功能的 Win32 窗口程序主要收获如下理解了 Windows 消息驱动机制。窗口的一切行为绘制、点击、尺寸变化、关闭都由消息触发WindowProc是处理消息的核心GetMessage/DispatchMessage构成消息循环。掌握了 GDI 绘图基本方法。使用BeginPaint/EndPaint、FillRect、DrawTextW等完成背景填充和文字居中显示。学会了动态修改窗口风格。通过GetWindowLongPtr/SetWindowLongPtr配合GWL_STYLE和SetWindowPos(SWP_FRAMECHANGED)可在运行时切换窗口外观但必须注意保留WS_VISIBLE等必要风格位。掌握了音频播放控制。PlaySound配合SND_ASYNC | SND_FILENAME可异步播放一次音频需要停止时传入NULL。认识到重绘的重要性。InvalidateRect、WM_SIZE、CS_HREDRAW/CS_VREDRAW等机制决定了窗口在尺寸变化、风格切换时能否正确重绘处理不当会出现残影。积累了调试经验。编译错误往往被运行错误掩盖应先生成解决方案确认编译通过多个 exe 进程残留会造成程序卡死的假象修改代码后必须重新生成否则运行的仍是旧 exe。本次实验让我们对 Win32 API 的编程范式和消息机制有了直观而深入的理解为后续学习更复杂的 Windows 应用开发打下了基础。