最近更新于 2023-02-02 10:15

环境

Windows 11 专业工作站版 22H2
Visual Studio 2022 专业版

Unicode 字符集,C17 标准编译,64 位编译

代码

#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS // 关闭对不使用 _s 安全函数时的报错
#endif

#include <Windows.h>

HANDLE g_stdo; // 用于存储标准输出句柄
WCHAR g_output_info[64]; // 用于存储输出到控制台的信息

/**
* @brief 窗口过程函数 - 窗口消息处理
* @param hWnd 窗口的句柄
* @param message 消息代码
* @param wParam 其他消息信息,确切含义取决于消息代码
* @param lParam 其他消息信息,确切含义取决于消息代码
* @return 消息处理的结果
*/
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    PAINTSTRUCT ps = { 0 };
    HDC hdc = { 0 };
    WORD h = 0, v = 0; // 存储水平方向和垂直方向的坐标或长度

    memset(g_output_info, 0, sizeof(g_output_info));

    switch (message)
    {
        case WM_CREATE: // 在窗口创建后,显示窗口之前会收到一次该消息代码。一般用于做初始化,创建子窗口等工作。
        {
            CREATESTRUCT* pCreate = (CREATESTRUCT*)lParam; // CREATESTRUCT 结构保存创建窗口函数的 12 个参数
            wsprintf(g_output_info, L"WM_CREATE:%s\n", (PWSTR)pCreate->lpCreateParams);
            // 设置定时器
            // 句柄
            // ID
            // 时间间隔(ms)
            // 处理函数,一般不用
            SetTimer(hWnd, 100, 2000, NULL);
            SetTimer(hWnd, 200, 4000, NULL);
            break;
        }
        case WM_TIMER: // 匹配定时器 ID
            switch (wParam)
            {
                case 100:
                    wcscpy(g_output_info, L"定时器1 消息\n");
                    break;
                case 200:
                    wcscpy(g_output_info, L"定时器2 消息\n");
                    break;
                default:
                    break;
            }
            break;
        case WM_KEYDOWN: // 键盘按键按下
            wsprintf(g_output_info, L"按键按下,键码值为:%I64d\n", wParam);
            break;
        case WM_KEYUP: // 键盘按键放开
            wsprintf(g_output_info, L"按键放开,键码值为:%I64d\n", wParam);
            break;
        // Alt 和 F10 为系统键,以及按 Alt 键和其它键的组合
        case WM_SYSKEYDOWN: // 键盘系统键按下
            wsprintf(g_output_info, L"系统键按下,键码值为:%I64d\n", wParam);
            break;
        case WM_SYSKEYUP: // 键盘系统键放开
            wsprintf(g_output_info, L"系统键放开,键码值为: %I64d\n", wParam);
            break;
        case WM_CHAR: // 该消息由 TranslateMessage 翻译后发出,对大小写字符会区分以及配合上档键输出的按键第二字符
            wsprintf(g_output_info, L"按键字符为:%lc,对应键值码为:%I64d\n", (WCHAR)wParam, wParam);
            break;
        case WM_LBUTTONDOWN: // 鼠标左键按下
            wsprintf(g_output_info, L"鼠标左键按下,其它按键状态:%I64d,鼠标位置:%d %d\n", wParam, LOWORD(lParam), HIWORD(lParam));
            break;
        case WM_LBUTTONUP: // 鼠标左键放开
            wsprintf(g_output_info, L"鼠标左键放开,其它按键状态:%I64d,鼠标位置:%d %d\n", wParam, LOWORD(lParam), HIWORD(lParam));
            break;
        case WM_RBUTTONDOWN: // 鼠标右键按下
            wsprintf(g_output_info, L"鼠标右键按下,其它按键状态:%I64d,鼠标位置:%d %d\n", wParam, LOWORD(lParam), HIWORD(lParam));
            break;
        case WM_RBUTTONUP: // 鼠标右键放开
            wsprintf(g_output_info, L"鼠标右键放开, 其它按键状态:%I64d,鼠标位置:%d %d\n", wParam, LOWORD(lParam), HIWORD(lParam));
            break;
        //case WM_MOUSEMOVE: // 鼠标移动
        //    wsprintf(g_output_info, L"鼠标移动到:%d %d\n", LOWORD(lParam), HIWORD(lParam));
        //    break;
        case WM_MOUSEWHEEL: // 鼠标滚轮滚动
            wsprintf(g_output_info, L"鼠标滚动量(前为正,后为负):%d,其它按键状态:%d,鼠标位置:%d %d\n", (INT16)HIWORD(wParam), LOWORD(wParam), LOWORD(lParam), HIWORD(lParam));
            break;
        case WM_LBUTTONDBLCLK: // 鼠标左键双击
            wsprintf(g_output_info, L"鼠标左键双击,其它按键状态:%I64d,鼠标位置:%d %d\n", wParam, LOWORD(lParam), HIWORD(lParam));
            break;
        case WM_RBUTTONDBLCLK: // 鼠标右键双击
            wsprintf(g_output_info, L"鼠标右键双击,其它按键状态:%I64d,鼠标位置:%d %d\n", wParam, LOWORD(lParam), HIWORD(lParam));
            break;
        case WM_PAINT:
            hdc = BeginPaint(hWnd, &ps);
            TextOut(hdc, 5, 5, L"你好,世界!", wcslen(L"你好,世界!"));
            EndPaint(hWnd, &ps);
            break;
        case WM_SIZE:
            h = LOWORD(lParam);
            v = HIWORD(lParam);
            wsprintf(g_output_info, L"窗口大小修改为:%dx%d\n", h, v);
            break;
        case WM_SYSCOMMAND: // 点击最小化、最大化和关闭等等操作会发出该信号
            if (wParam == SC_CLOSE)
            {
                if (MessageBox(hWnd, L"确认退出?", L"确认信息", MB_OKCANCEL) == IDOK)
                {
                    DestroyWindow(hWnd); // 销毁窗口
                }
            }
            else if (wParam == SC_MAXIMIZE)
            {
                wcscpy(g_output_info, L"SC_MAXIMZE:点击最大化按钮\n");
                break;
            }
            else if (wParam == SC_MINIMIZE)
            {
                wcscpy(g_output_info, L"SC_MINIMZE:点击最小化按钮\n");
                break;
            }
            else
            {
                DefWindowProc(hWnd, message, wParam, lParam);
            }
            h = LOWORD(lParam);
            v = HIWORD(lParam);
            wsprintf(g_output_info, L"鼠标点击位置:%dx%d\n", h, v);  // 只有点击边框和标题栏时
            break;
        case WM_DESTROY: // 执行销毁窗口会收到该消息,可以在这里处理善后
            PostQuitMessage(0); // 在消息队列上放置 WM_QUIT 消息,会让消息循环停止(GetMessage 收到该消息返回 0)
            // 关闭定时器
            KillTimer(hWnd, 100);
            KillTimer(hWnd, 200);
            break;
        default:
            return DefWindowProc(hWnd, message, wParam, lParam); // 默认消息处理
        }
    WriteConsole(g_stdo, g_output_info, wcslen(g_output_info), NULL, NULL);
}

/**
* @brief Windows 程序入口点函数
* @param hInstance 称为“实例句柄”或“模块句柄”。操作系统使用此值在内存中加载可执行文件时标识可执行文件 (EXE) 。 某些Windows函数需要实例句柄,例如加载图标或位图
* @param hPreInstance 没有意义。 它在 16 位Windows中使用,但现在始终为零
* @param lpCmdLine 包含命令行参数作为 Unicode 字符串
* @param nCmdShow 是一个标志,指示主应用程序窗口是最小化、最大化还是正常显示
* @return 返回 int 值。 操作系统不使用返回值,但可以使用返回值将状态代码传达给你编写的其他程序
*/
int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPreInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow)
{
    AllocConsole(); // 申请控制台窗口用于显示调试信息
    g_stdo = GetStdHandle(STD_OUTPUT_HANDLE); // 获取标准输出句柄

    PCWSTR szWindowClass = L"DesktopAPP"; // 设置的窗口类的名字
    WNDCLASSEX wcex = { 0 };
    wcex.cbSize = sizeof(WNDCLASSEX); // 本结构的大小 - 必须设置 1
    wcex.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS; // 类样式 // 添加 CS_DBLCLKS 后,支持接收鼠标双击消息
    wcex.lpfnWndProc = WndProc; // 指向窗口过程的指针 - 必须设置 2 
    wcex.cbClsExtra = 0; // 要按照窗口类结构分配的额外字节数。 系统将字节初始化为零
    wcex.cbWndExtra = 0; // 在窗口实例之后分配的额外字节数。 系统将字节初始化为零
    wcex.hInstance = hInstance; // 包含类的窗口过程的实例的句柄
    wcex.hIcon = LoadIcon(wcex.hInstance, IDI_APPLICATION); // 类图标的句柄。 此成员必须是图标资源的句柄。 如果此成员为 NULL,则系统提供默认图标
    wcex.hCursor = LoadCursor(NULL, IDC_ARROW); // 类游标的句柄。 此成员必须是游标资源的句柄。 如果此成员为 NULL,则每当鼠标移动到应用程序的窗口中时,应用程序都必须显式设置光标形状
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // 类背景画笔的句柄。 此成员可以是用于绘制背景的画笔的句柄,也可以是颜色值(颜色值宏可查阅开发文档)
    wcex.lpszMenuName = NULL; // 指向一个 null 终止的字符串的指针,该字符串指定类菜单的资源名称,因为名称显示在资源文件中
    wcex.lpszClassName = szWindowClass; // 窗口类名字 - 必须设置 3
    wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION); // 与窗口类关联的小图标的句柄

    if (!RegisterClassEx(&wcex)) // 注册窗口类
    {
        MessageBox(NULL, L"注册窗口失败!", L"错误", MB_OK);
        return 1;
    }

    // 创建窗口
    // 第一个参数:允许为窗口指定一些可选行为 (,例如透明窗口) 。 对于默认行为,此参数设置为零
    // 第二个参数:窗口类的名字
    // 第三个参数:窗口文本,通过不同类型的窗口以不同的方式使用。 如果窗口具有标题栏,则文本将显示在标题栏中
    // 第四个参数:窗口样式为一组标志,用于定义窗口的一些外观。 常量 WS_OVERLAPPEDWINDOW 实际上是几个标志与按位 OR 组合。
    // 这些标志一起为窗口提供标题栏、边框、系统菜单,以及 最小化 和 最大化 按钮。 此标志集是顶级应用程序窗口最常见的样式
    // 第五和第六个参数:窗口的初始位置,设置 CW_USEDEFAULT 为默认,在左上角位置
    // 第七和第八个参数:窗口的初始大小,设置 CW_USEDEFAULT 为默认,系统为窗口选择默认宽度和高度
    // 第九个参数:设置新窗口的父窗口或所有者窗口。 如果要创建子窗口,请设置父窗口。 对于顶级窗口,请将此窗口设置为 NULL
    // 第十个参数:定义窗口的菜单,不使用菜单设置 NULL
    // 第十一个参数:实例句柄
    // 第十二个参数:LPVOID 即 void *,用于向窗口过程传递数据
    HWND hWnd = CreateWindowEx(WS_EX_OVERLAPPEDWINDOW, szWindowClass, L"第一个桌面程序", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 400, 100, NULL, NULL, hInstance, L"我是窗口创建函数传入的信息");
    if (!hWnd)
    {
        MessageBox(NULL, L"创建窗口失败!", L"错误", MB_OK);
        return 1;
    }

    ShowWindow(hWnd, nCmdShow); // 设置指定窗口的显示状态,比如最小化和最大化等等
    UpdateWindow(hWnd); // 发送 WM_PAINT 到窗口过程,用于更新指定窗口的工作区(如果有需要更新的内容的话)

    MSG msg = { 0 };
    // 从消息队列拉取消息
    // 第一个参数用于存储抓取的消息
    // 第二个参数可以指定句柄,只抓取某个窗口的消息,设为 NULL,抓取所有窗口的
    // 第三第四参数指定抓取的消息代码范围,都为 0 全部抓取
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg); // 将键盘可见字符消息翻译为对应的字符内容
        DispatchMessage(&msg); // 将消息调度到窗口过程
    }

    // 使用 PostQuitMessage 发送 WM_QUIT 消息
    // WM_QUIT 对 wParam 内容的定义就是 PostQuitMessage 的参数
    // 也就是 PostQuitMessage 参数设置的值是多少,这里的 msg.wParam 就是多少
    return (int)msg.wParam;
}

运行效果

file
file
file
file
file

最简版

前面那个示例程序,我尽可能把最基础的部分都包含进去了,新手看起来可能会比较头疼,这里就搞一个简版的,只是单纯窗口中显示“你好,世界!”

#include <Windows.h>

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
        case WM_PAINT:
        {
            PAINTSTRUCT ps = {0};
            HDC hdc = BeginPaint(hWnd, &ps);
            TextOut(hdc, 10, 10, L"你好,世界!", wcslen(L"你好,世界!"));
            EndPaint(hWnd, &ps);
            break;
        }
        case WM_CLOSE:
        {
            DestroyWindow(hWnd);
            break;
        }
        case WM_DESTROY:
        {
            PostQuitMessage(0);
            break;
        }
        default:
        {
            return DefWindowProc(hWnd, msg, wParam, lParam);
        }
    }
    return 0;
}

int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPreHinstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow)
{
    PCWSTR window_class_name = L"Desktop";
    WNDCLASSEX wc = {0};
    wc.cbSize = sizeof(WNDCLASSEX);
    wc.hInstance = hInstance;
    wc.lpfnWndProc = WndProc;
    wc.lpszClassName = window_class_name;

    if (!RegisterClassEx(&wc))
    {
        MessageBox(NULL, L"注册窗口类失败!", L"错误", MB_OK);
        return 1;
    }

    HWND hWnd = CreateWindowEx(0, window_class_name, L"简单窗口程序", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 260, 80, NULL, NULL, hInstance, NULL);
    if (!hWnd)
    {
        MessageBox(NULL, L"创建窗口失败!", L"错误", MB_OK);
        return 1;
    }

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    MSG msg = {0};
    while (GetMessage(&msg, NULL, 0, 0))
    {
        DispatchMessage(&msg);
    }
    return (int)msg.wParam;
}

file