01 Windows API窗口程序设计
2026/9/24 17:44:37 网站建设 项目流程

一、题目:

运行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( L"BUTTON", 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[] = L"Sample 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.lib

mmsystem.h提供PlaySound声明,#pragma comment让链接器自动链接winmm.lib,无需手动在项目属性中添加。

2 窗口过程函数 WindowProc

WindowProc是窗口的消息处理核心,所有发往该窗口的消息都在这里分发处理。

(1)WM_CREATE:创建按钮

case WM_CREATE: hBtn = CreateWindow( L"BUTTON", 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识别。

(2)WM_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强制整个客户区重绘,避免切换风格后残留旧图像。

(3)WM_SIZE:尺寸变化时全客户区重绘

case WM_SIZE: InvalidateRect(hwnd, NULL, TRUE); return 0;

最大化、还原、拖边框等操作会触发WM_SIZE。这里让整个客户区作废并重绘,防止"文字残影"。

(4)WM_CLOSE:关闭前停止音乐

case WM_CLOSE: PlaySound(NULL, NULL, 0); DestroyWindow(hwnd); return 0;

先停止音乐,再手动调用DestroyWindow销毁窗口。因为接管了WM_CLOSE,默认销毁逻辑不再执行,必须自己调用DestroyWindow

(5)WM_DESTROY:退出消息循环

case WM_DESTROY: PlaySound(NULL, NULL, 0); PostQuitMessage(0); return 0;

PostQuitMessage(0)向消息队列投递WM_QUIT,使WinMain中的GetMessage返回 0,退出消息循环,进程结束。

(6)WM_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,防止文字被绘制到区域外产生残影。

(7)WM_SETCURSOR:手型光标

case WM_SETCURSOR: { if (LOWORD(lParam) == HTCLIENT) { SetCursor(LoadCursor(NULL, IDC_HAND)); return TRUE; } break; }

lParam低字为命中测试码。若鼠标位于客户区(HTCLIENT),把光标设为手型并返回TRUE,表示消息已处理;否则交给DefWindowProc处理(例如边框上的缩放光标)。

3 入口函数 WinMain

PlaySound(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)→ 确定 → 重新生成。

改完后重新按Ctrl+Shift+B生成,应该就没这两个链接错误了。

顺手确认另外两项(避免改完又出新错)

① 字符集

项目 → 属性 → 配置属性 →高级 → 字符集→ 选"使用 Unicode 字符集"

代码里用了std::wstringL"..."DrawTextW,必须是 Unicode。如果是"多字节字符集",L"..."DrawTextW会有类型不匹配的错。

② 入口点(一般不用改)

如果改成"窗口"子系统后,链接器又报WinMain找不到,那就再设一下入口点:

项目 → 属性 → 链接器 →高级 → 入口点→ 填WinMainCRTStartup

正常情况下,改成"窗口"子系统后,MSVC 会自动匹配WinMain,不需要手动设入口点。

正确状态:成功生成exe文件

问题 2:关闭窗口后音乐仍在播放

现象:点击窗口右上角 × 后,窗口关闭,但开机音效仍在循环播放orz。

原因:一开始代码只在WM_DESTROY里调用PlaySound(NULL,NULL,0),且播放时带了SND_LOOP,停止不够及时。另外当时项目里可能残留了多个 exe 进程。

解决

  1. 播放时去掉SND_LOOP,改为只播放一次。
  2. WM_CLOSE里也调用PlaySound(NULL, NULL, 0),先停声再销毁窗口。
  3. WM_DESTROY里再次停止,形成双保险。
  4. 用任务管理器清理所有残留的Project1.exe进程。

问题 3:点击"切换窗口风格"后窗口卡住、按钮失灵

现象:点击按钮后,窗口变成"只有标题栏",但按钮无法再点击,整个界面像卡死。

原因:切换风格时用style = WS_CAPTION | WS_SYSMENU;整体替换了窗口风格位,把WS_VISIBLE(可见标志)一并抹掉,窗口进入"不可见但未销毁"的状态,子按钮随之异常。

解决:在切换风格时补回WS_VISIBLE

style = WS_CAPTION | WS_SYSMENU | WS_VISIBLE; style = WS_OVERLAPPEDWINDOW | WS_VISIBLE;

或者更稳妥地只修改需要改的风格位,而不整体替换。

问题 4:全屏后或者变动窗口出现两个"大家好"文字残影

现象:最大化窗口或还原后,客户区同时出现两个"大家好……",居中那个是正常的,另一个是旧图像的残留。

原因

  1. DrawTextW使用了DT_NOCLIP,允许文字绘制到矩形区域之外。
  2. WM_PAINTFillRect只填充ps.rcPaint(系统指定的重绘区域),未覆盖的部分保留旧内容。
  3. 窗口尺寸变化时未强制全客户区重绘。

解决

  1. 去掉DT_NOCLIP,让文字被限制在rect内。
  2. WM_SIZE消息中调用InvalidateRect(hwnd, NULL, TRUE),使尺寸变化时整个客户区作废重绘。
  3. 在切换风格后同样调用InvalidateRect + UpdateWindow

五、最终实现效果

起始效果

切换窗口风格

全屏效果

任意尺寸

六、实验总结

本次实验完整地实现了一个具备多项功能的 Win32 窗口程序,主要收获如下:

  1. 理解了 Windows 消息驱动机制。窗口的一切行为(绘制、点击、尺寸变化、关闭)都由消息触发,WindowProc是处理消息的核心,GetMessage/DispatchMessage构成消息循环。
  2. 掌握了 GDI 绘图基本方法。使用BeginPaint/EndPaintFillRectDrawTextW等完成背景填充和文字居中显示。
  3. 学会了动态修改窗口风格。通过GetWindowLongPtr/SetWindowLongPtr配合GWL_STYLESetWindowPos(SWP_FRAMECHANGED)可在运行时切换窗口外观,但必须注意保留WS_VISIBLE等必要风格位。
  4. 掌握了音频播放控制PlaySound配合SND_ASYNC | SND_FILENAME可异步播放一次音频,需要停止时传入NULL
  5. 认识到重绘的重要性InvalidateRectWM_SIZECS_HREDRAW/CS_VREDRAW等机制决定了窗口在尺寸变化、风格切换时能否正确重绘,处理不当会出现残影。
  6. 积累了调试经验。编译错误往往被运行错误掩盖,应先"生成解决方案"确认编译通过;多个 exe 进程残留会造成"程序卡死"的假象;修改代码后必须重新生成,否则运行的仍是旧 exe。

本次实验让我们对 Win32 API 的编程范式和消息机制有了直观而深入的理解,为后续学习更复杂的 Windows 应用开发打下了基础。

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

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

立即咨询