Windows API窗口程序设计

第一个 Windows API 程序

一、实验要求

  1. 窗口背景色为灰色;
  2. 窗口内居中显示"大家好,这是我的第一个Windows API程序!";
  3. 播放背景音乐 hellowin.wav(打开时播放一次);
  4. 可通过程序把窗口显示风格切换为"只有标题栏";
  5. 鼠标指针形状为手型。

二、整体设计思路

核心步骤:

  1. 注册窗口类 RegisterClass:定义窗口过程、背景刷、光标等;
  2. 创建窗口 CreateWindow:指定类名、标题、样式;
  3. 进入消息循环GetMessage → TranslateMessage → DispatchMessage,把消息交给窗口过程处理;
  4. 在窗口过程中响应消息WM_PAINT 负责绘制灰底和文字,WM_CREATE 里创建菜单并播放音乐,WM_COMMAND 处理菜单点击,WM_SETCURSOR 控制鼠标形状。

全部使用 W 后缀的宽字符版本 API,并结合 /utf-8 编译选项,确保中文字符串显示正确、不乱码。

三、完整源码

/*
 * hellowin.c —— 我的第一个 Windows API 程序
 *
 * 功能:
 *   1. 窗口背景色为灰色;
 *   2. 窗口内居中显示“大家好,这是我的第一个Windows API程序!”;
 *   3. 播放背景音乐(hellowin.wav,打开时播放一次);
 *   4. 可通过菜单把窗口显示风格切换为“只有标题栏”;
 *   5. 可通过菜单切换鼠标指针形状为“手型”。
 *
 * 编译(MSVC,需先执行 vcvars64.bat):
 *   cl /nologo /O2 /utf-8 hellowin.c /Fe:hellowin.exe /link /SUBSYSTEM:WINDOWS
 *   依赖库:user32.lib gdi32.lib winmm.lib
 */

#include <windows.h>
#include <mmsystem.h>
#include <stdio.h>

#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdi32.lib")
#pragma comment(lib, "winmm.lib")

/* 菜单命令 ID */
#define IDM_STYLE_NORMAL   1001   /* 标准窗口          */
#define IDM_STYLE_CAPTION  1002   /* 只有标题栏        */
#define IDM_CURSOR_HAND    1003   /* 手型指针(开关)  */
#define IDM_EXIT           1004   /* 退出              */

static BOOL   g_handCursor = TRUE;              /* 默认使用手型指针  */
static WCHAR  g_wavPath[MAX_PATH];              /* wav 完整路径      */

/* 依据可执行文件所在目录,拼出 hellowin.wav 的完整路径 */
static void BuildWavPath(void)
{
    WCHAR exe[MAX_PATH];
    GetModuleFileNameW(NULL, exe, MAX_PATH);
    WCHAR *slash = wcsrchr(exe, L'\\');
    if (slash) *slash = L'\0';
    wsprintfW(g_wavPath, L"%s\\hellowin.wav", exe);
}

/* 用灰色填充客户区 */
static void PaintGray(HWND hwnd, HDC hdc)
{
    RECT rc;
    GetClientRect(hwnd, &rc);
    HBRUSH gray = CreateSolidBrush(RGB(128, 128, 128));
    FillRect(hdc, &rc, gray);
    DeleteObject(gray);
}

/* 窗口过程 */
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_CREATE:
        {
            /* 构建菜单 */
            HMENU hMenu  = CreateMenu();

            HMENU hStyle = CreatePopupMenu();
            AppendMenuW(hStyle, MF_STRING, IDM_STYLE_NORMAL,  L"标准窗口(&N)");
            AppendMenuW(hStyle, MF_STRING, IDM_STYLE_CAPTION, L"只有标题栏(&T)");
            AppendMenuW(hMenu, MF_POPUP, (UINT_PTR)hStyle, L"显示风格(&S)");

            HMENU hPtr = CreatePopupMenu();
            AppendMenuW(hPtr, MF_STRING | MF_CHECKED, IDM_CURSOR_HAND, L"手型指针(&H)");
            AppendMenuW(hMenu, MF_POPUP, (UINT_PTR)hPtr, L"指针形状(&C)");

            HMENU hFile = CreatePopupMenu();
            AppendMenuW(hFile, MF_STRING, IDM_EXIT, L"退出(&X)");
            AppendMenuW(hMenu, MF_POPUP, (UINT_PTR)hFile, L"文件(&F)");

            SetMenu(hwnd, hMenu);
            DrawMenuBar(hwnd);

            /* 播放背景音乐(打开时播放一次) */
            if (GetFileAttributesW(g_wavPath) != INVALID_FILE_ATTRIBUTES)
                PlaySoundW(g_wavPath, NULL, SND_FILENAME | SND_ASYNC);
        }
        return 0;

    case WM_ERASEBKGND:          /* 灰色背景,避免闪烁 */
        PaintGray(hwnd, (HDC)wParam);
        return 1;

    case WM_PAINT:               /* 绘制灰色背景与居中文字 */
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hwnd, &ps);

            PaintGray(hwnd, hdc);

            RECT rc;
            GetClientRect(hwnd, &rc);
            SetBkMode(hdc, TRANSPARENT);
            SetTextColor(hdc, RGB(255, 255, 255));
            HFONT hFont = CreateFontW(28, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE,
                DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
                CLEARTYPE_QUALITY, DEFAULT_PITCH, L"Microsoft YaHei");
            HFONT old = (HFONT)SelectObject(hdc, hFont);
            DrawTextW(hdc, L"大家好,这是我的第一个Windows API程序!", -1, &rc,
                DT_CENTER | DT_VCENTER | DT_SINGLELINE);
            SelectObject(hdc, old);
            DeleteObject(hFont);

            EndPaint(hwnd, &ps);
        }
        return 0;

    case WM_SETCURSOR:           /* 客户区内鼠标指针形状 */
        if (LOWORD(lParam) == HTCLIENT)
        {
            SetCursor(LoadCursorW(NULL, g_handCursor ? (LPCWSTR)IDC_HAND
                                                     : (LPCWSTR)IDC_ARROW));
            return TRUE;
        }
        break;

    case WM_COMMAND:
        switch (LOWORD(wParam))
        {
        case IDM_STYLE_NORMAL:   /* 恢复标准窗口 */
            SetWindowLongPtrW(hwnd, GWL_STYLE, WS_OVERLAPPEDWINDOW);
            SetWindowPos(hwnd, NULL, 0, 0, 0, 0,
                         SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
            ShowWindow(hwnd, SW_SHOW);
            InvalidateRect(hwnd, NULL, TRUE);
            CheckMenuItem(GetMenu(hwnd), IDM_STYLE_NORMAL, MF_BYCOMMAND | MF_CHECKED);
            CheckMenuItem(GetMenu(hwnd), IDM_STYLE_CAPTION, MF_BYCOMMAND | MF_UNCHECKED);
            return 0;

        case IDM_STYLE_CAPTION:  /* 切换为只有标题栏 */
            SetWindowLongPtrW(hwnd, GWL_STYLE, WS_CAPTION);
            SetWindowPos(hwnd, NULL, 0, 0, 0, 0,
                         SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
            ShowWindow(hwnd, SW_SHOW);
            InvalidateRect(hwnd, NULL, TRUE);
            CheckMenuItem(GetMenu(hwnd), IDM_STYLE_NORMAL, MF_BYCOMMAND | MF_UNCHECKED);
            CheckMenuItem(GetMenu(hwnd), IDM_STYLE_CAPTION, MF_BYCOMMAND | MF_CHECKED);
            return 0;

        case IDM_CURSOR_HAND:    /* 手型 / 箭头指针切换 */
            g_handCursor = !g_handCursor;
            CheckMenuItem(GetMenu(hwnd), IDM_CURSOR_HAND,
                MF_BYCOMMAND | (g_handCursor ? MF_CHECKED : MF_UNCHECKED));
            SetCursor(LoadCursorW(NULL, g_handCursor ? (LPCWSTR)IDC_HAND
                                                     : (LPCWSTR)IDC_ARROW));
            return 0;

        case IDM_EXIT:
            DestroyWindow(hwnd);
            return 0;
        }
        break;

    case WM_DESTROY:
        PlaySoundW(NULL, NULL, 0);   /* 停止音乐 */
        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProcW(hwnd, msg, wParam, lParam);
}

/* 程序入口 */
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR lpCmdLine, int nCmdShow)
{
    (void)hPrev; (void)lpCmdLine;

    BuildWavPath();

    WNDCLASSW wc = { 0 };
    wc.style         = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc   = WndProc;
    wc.hInstance     = hInst;
    wc.hIcon         = LoadIconW(NULL, (LPCWSTR)IDI_APPLICATION);
    wc.hCursor       = LoadCursorW(NULL, (LPCWSTR)IDC_HAND);   /* 手型指针 */
    wc.hbrBackground = (HBRUSH)GetStockObject(GRAY_BRUSH);     /* 灰色背景 */
    wc.lpszClassName = L"HelloWinClass";

    if (!RegisterClassW(&wc))
        return 0;

    HWND hwnd = CreateWindowW(
        L"HelloWinClass",
        L"我的第一个Windows API程序",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 720, 400,
        NULL, NULL, hInst, NULL);
    if (!hwnd)
        return 0;

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    MSG msg;
    while (GetMessageW(&msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }
    return (int)msg.wParam;
}

四、关键知识点讲解

1. 窗口类注册与创建

任何 Win32 窗口都离不开 WNDCLASSRegisterClass。在本例中,我把三处关键配置直接放在了窗口类里:

wc.hCursor       = LoadCursorW(NULL, (LPCWSTR)IDC_HAND);   /* 手型指针 */
wc.hbrBackground = (HBRUSH)GetStockObject(GRAY_BRUSH);     /* 灰色背景 */
  • hCursor:窗口的默认光标,这里直接设为系统手型光标 IDC_HAND
  • hbrBackground:窗口背景刷,用 GRAY_BRUSH 设置灰色。

2. 消息循环

while (GetMessageW(&msg, NULL, 0, 0) > 0)
{
    TranslateMessage(&msg);
    DispatchMessageW(&msg);
}

Windows 是消息驱动的:程序不做主动轮询,而是等待系统投递消息,再分发给对应的窗口过程。GetMessage 取到 WM_QUIT 时返回 0,循环结束,程序退出。

3. 灰色背景与居中文字

绘制工作放在 WM_PAINT 里:

FillRect(hdc, &rc, gray);                       /* 灰色填充 */
DrawTextW(hdc, L"大家好,这是我的第一个Windows API程序!", -1, &rc,
          DT_CENTER | DT_VCENTER | DT_SINGLELINE);  /* 水平垂直居中 */

DrawText 配合 DT_CENTER | DT_VCENTER 就能轻松实现文字在客户区水平垂直居中。文字使用 28 号加粗的微软雅黑,白色,在灰色背景上对比鲜明。

同时处理 WM_ERASEBKGND 并返回 1,可以避免窗口在刷新时的闪烁。

4. 背景音乐

PlaySound 播放 WAV,需要链接 winmm.lib

PlaySoundW(g_wavPath, NULL, SND_FILENAME | SND_ASYNC);
  • SND_FILENAME:指定按文件名播放;
  • SND_ASYNC:异步播放,不阻塞窗口;
  • 由于没加 SND_LOOP,音乐只播放一次

优化:用 GetModuleFileNameW 获取 exe 所在目录,再拼出 hellowin.wav 的完整路径。这样无论程序从哪里启动,都能找到音乐文件,不会因为"当前工作目录不对"而播不出声音。

5. 菜单与样式切换

菜单用 CreateMenu / AppendMenuW 动态构建,不需要额外的 .rc 资源文件,单文件即可编译。

切换窗口样式是最有意思的部分——用 SetWindowLongPtrGWL_STYLE,再调 SetWindowPos 让系统重算边框:

SetWindowLongPtrW(hwnd, GWL_STYLE, WS_CAPTION);   /* 只保留标题栏 */
SetWindowPos(hwnd, NULL, 0, 0, 0, 0,
             SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);

SWP_FRAMECHANGED :通知系统窗口样式已改变,需要重新绘制非客户区。这样"标准窗口"和"只有标题栏"就能在运行时即时切换。

6. 手型指针

系统手型光标就是 IDC_HAND。我在窗口类里设了默认手型,同时用 WM_SETCURSOR 在客户区主动控制,方便通过菜单在手型/箭头之间来回切换:

case WM_SETCURSOR:
    if (LOWORD(lParam) == HTCLIENT)
    {
        SetCursor(LoadCursorW(NULL, g_handCursor ? (LPCWSTR)IDC_HAND
                                                 : (LPCWSTR)IDC_ARROW));
        return TRUE;
    }
    break;

7. 中文不乱码的两个关键

  1. 用宽字符版本 APIW 结尾):RegisterClassWCreateWindowWDrawTextWPlaySoundW……字符串统一用 L"..." 宽字符;
  2. 编译加 /utf-8:告诉编译器源码文件是 UTF-8 编码。

这两步缺一不可,否则中文字符串在 Win32 里极容易出现乱码。

五、编译与运行

我使用 Visual Studio 2022 自带的 MSVC 编译器(cl)。先在命令行加载编译环境,再编译:

call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
cl /nologo /O2 /utf-8 hellowin.c /Fe:hellowin.exe /link /SUBSYSTEM:WINDOWS
  • /O2:优化;
  • /utf-8:源码按 UTF-8 处理(中文关键);
  • /link /SUBSYSTEM:WINDOWS:链接为 GUI 程序(不弹出控制台窗口)。

依赖的库通过源码里的 #pragma comment(lib, ...) 自动链接,无需手动指定。编译成功后,把 hellowin.exehellowin.wav 放在同一目录,双击运行即可。

六、运行效果

窗口正常启动时,灰色背景、居中文字、菜单栏清晰可见,同时会播放一次背景音乐:
标准窗口运行截图

通过菜单"显示风格 → 只有标题栏",窗口会实时变成仅剩标题栏的样子:
image

通过菜单"指针形状 → 手型指针",鼠标在窗口内会变成手型(也可切回普通箭头)。

七、总结

  1. 中文乱码:核心就是"宽字符 API + /utf-8 编译",从源头解决。
  2. 切样式后边框不刷新:忘了 SWP_FRAMECHANGED,窗口样式改了但界面没变,卡了很久才想明白。
  3. PlaySound 找不到文件:直接写相对路径,可能因为启动目录不对而静默失败。用 GetModuleFileNameW 拼绝对路径最稳妥。
  4. 背景闪烁:处理 WM_ERASEBKGND 直接返回 1,并让 WM_PAINT 一次性填充背景,能显著减少重绘闪烁。
posted @ 2026-09-22 23:59  hohohoho---  阅读(6)  评论(0)    收藏  举报