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

环境

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

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

绘制菜单

添加 Windows 桌面向导,创建桌面应用程序空项目,右侧“资源文件”上右键,新建项
file
file

左侧选择“资源”,右边选“资源文件”,文件名字这里我设为“menu.rc”
file

下面切换到“资源视图”
file
file

在创建的资源文件上右键点“添加资源”
file

选 Menu,然后新建
file

然后就可以编辑菜单资源,编辑完成后按 Ctrl+S 保存一下
file
file

注意不管哪种将菜单资源文件添加到代码里的方式,都需要引用一下创建资源文件时生成的头文件 “resource.h”

添加方式一

可以在资源视图查看,这里创建的菜单被命名为了 IDR_MENU1
file

于是可以在注册窗口类的时候将菜单添加进去

#include <Windows.h>
#include "resource.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;
    wc.lpszMenuName = MAKEINTRESOURCE(IDR_MENU1); // 将菜单资源名字添加进去,并且要将数据类型转为宽字符指针(宽字符字符串 PWSTR = WCHAR * = wchar_t *)

    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, 120, 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))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return (int)msg.wParam;
}

file
后面两种添加菜单后界面都一样,我就不贴图了

添加方式二

在创建窗口的时候,通过窗口创建函数参数添加菜单资源

#include <Windows.h>
#include "resource.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;
    }

    HMENU hMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDR_MENU1)); // 加载菜单资源
    HWND hWnd = CreateWindowEx(0, window_class_name, L"简单窗口程序", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 260, 120, NULL, hMenu, 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))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return (int)msg.wParam;
}

添加方式三

在创建窗口后,显示窗口前,发送 WM_CREATE 消息时设置菜单

#include <Windows.h>
#include "resource.h" // 菜单资源文件引入

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
        case WM_CREATE:
        {
            // 通过窗口创建函数传入加载好的菜单资源
            CREATESTRUCT *pCreate = (CREATESTRUCT *)lParam;
            HMENU hMenu = *(HMENU *)(pCreate->lpCreateParams);
            // 设置菜单
            SetMenu(hWnd, hMenu);
        }
        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;
    }

    HMENU hMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDR_MENU1)); // 加载菜单资源
    HWND hWnd = CreateWindowEx(0, window_class_name, L"简单窗口程序", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 260, 120, NULL, NULL, hInstance, &hMenu); // 可以使用最后一个参数传入数据
    if (!hWnd)
    {
        MessageBox(NULL, L"创建窗口失败!", L"错误", MB_OK);
        return 1;
    }

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

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

命令消息处理

前面只是将菜单绘制到窗口里了,但是点击菜单项并没有任何反应,因此还需要为菜单项点击命令设置行为,即菜单命令的消息处理。

#include <Windows.h>
#include "resource.h" // 菜单资源文件引入

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
        case WM_COMMAND: // 点击菜单项会收到该消息
        {
            switch (LOWORD(wParam)) // 匹配对应的菜单项 ID,设计菜单界面的时候在菜单项右键属性也可以修改 ID 值,这里是默认的
            {
                case ID_40001:
                {
                    MessageBox(NULL, L"点击新建", L"消息", MB_OK);
                    break;
                }
                case ID_40002:
                {
                    MessageBox(NULL, L"点击退出", L"消息", MB_OK);
                    break;
                }
                case ID_40003:
                {
                    MessageBox(NULL, L"点击关于", L"消息", MB_OK);
                    break;
                }
                default:
                {
                    break;
                }
            }
            break;
        }
        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;
    wc.lpszMenuName = MAKEINTRESOURCE(IDR_MENU1);

    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, 120, 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))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return (int)msg.wParam;
}

file
file
file

上下文菜单

这个菜单就是一般说的右键菜单,右键菜单使用的菜单项内容还是前面绘制的菜单资源。
此处在前面的基础上再添加一个顶层菜单项,专用于上下文菜单使用。

file

#include <Windows.h>
#include "resource.h" // 菜单资源文件引入

HMENU g_popup = {0}; // 用于存储上下文菜单

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
        case WM_CONTEXTMENU: // 专用于右键菜单的,在右键放开消息后产生
        {
            // 添加上下文菜单
            // 要添加的菜单的句柄
            // 菜单弹出位置,下面的是设置的弹出菜单的左上方贴靠指定坐标
            // 接着两个参数分别传横纵坐标,这里消息附带的是当前鼠标位置的屏幕坐标
            // 保留参数,写 0
            // 窗口句柄
            // 不常用的参数,传 NULL
            TrackPopupMenu(g_popup, TPM_LEFTALIGN | TPM_TOPALIGN, LOWORD(lParam), HIWORD(lParam), 0, hWnd, NULL);
            break;
        }
        case WM_COMMAND: // 点击菜单项会收到该消息
        {
            switch (LOWORD(wParam)) // 匹配对应的菜单项 ID,设计菜单界面的时候在菜单项右键属性也可以修改 ID 值,这里是默认的
            {
                case ID_40001:
                {
                    MessageBox(NULL, L"点击新建", L"消息", MB_OK);
                    break;
                }
                case ID_40002:
                {
                    MessageBox(NULL, L"点击退出", L"消息", MB_OK);
                    break;
                }
                case ID_40003:
                {
                    MessageBox(NULL, L"点击关于", L"消息", MB_OK);
                    break;
                }
                case ID_40004:
                {
                    MessageBox(NULL, L"点击菜单项1", L"消息", MB_OK);
                    break;
                }
                case ID_40005:
                {
                    MessageBox(NULL, L"点击菜单项2", L"消息", MB_OK);
                    break;
                }
                case ID_40006:
                {
                    MessageBox(NULL, L"点击菜单项3", L"消息", MB_OK);
                    break;
                }
                default:
                {
                    break;
                }
            }
            break;
        }
        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;
    }

    HMENU hMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDR_MENU1)); // 加载菜单资源
    g_popup = GetSubMenu(hMenu, 2); // 获取子菜单,设计的菜单在第三个,从 0 开始索引为 2

    HWND hWnd = CreateWindowEx(0, window_class_name, L"简单窗口程序", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 300, 200, NULL, hMenu, 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))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return (int)msg.wParam;
}

file
file
file

其它资源

创建其它资源文件操作和上面类似,创建的时候选好类型或者添加指定类型的资源
file
file

图标资源

注册窗口类的时候,WNDCLASSEX 有一个成员 hIcon 就是设置图标资源句柄的。
需要使用 LoadIcon 加载图标资源,第一个参数就是当前实例句柄,第二个参数传图标资源的名字,和上面的菜单资源加载方法类似。
软件标题框的图标这些都是使用的图标资源。

光标资源

有两种方法使用,一种也是在注册窗口的时候设置,将光标资源句柄设给 hCursor。
加载光标资源使用 LoadCursor,参数也类似上面。
光标资源就是光标的图形,一般是显示的箭头,可以自己画成其它形状的,比如忙碌时鼠标显示圈,有些情况显示十字等等。
上面的设置方法只能在注册窗口的时候使用,但是就无法在程序运行中途进行变化,所以还有一种方法,使用 SetCursor,参数为当前正在使用的光标的句柄(后面的消息会附带传入这个参数)。不过注意这个函数要放到 WM_SETCUSOR 消息的处理中(光标移动就会产生这个消息)。

字符串资源

字符串资源文件可以用于专门存储程序中使用到的字符串,每个字符串有专门的索引 ID,程序中通过 LoadString 去取指定 ID 对应的字符串。
它的意义的话,我想到的是对于多语言的支持。字符串资源文件分别制作不同语言的版本,要用哪种语言就将对应的字符串资源文件放进去,在不修改程序的情况下可以做到程序内语言的修改。

加速键资源

这个加速键就是一般说的快捷键,加速键要和具体的操作绑定起来,所以这里沿用前面“上下文菜单”的源码和菜单资源文件,在上面为部分菜单项添加加速键。

这里要用到加速键资源,就在 menu.rc 里继续添加
file
file

我给上下文菜单的那三个菜单项添加加速键,注意他们各自的 ID
file
设置加速键资源,将 ID 对应要设置加速键的菜单项,后面设置按键。设置完成后记得按下 Ctrl+S 保存。
file

具体实现看下面代码

#include <Windows.h>
#include "resource.h" // 资源文件引入

HMENU g_popup = {0};

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
        case WM_CONTEXTMENU:
        {
            TrackPopupMenu(g_popup, TPM_LEFTALIGN | TPM_TOPALIGN, LOWORD(lParam), HIWORD(lParam), 0, hWnd, NULL);
            break;
        }
        case WM_COMMAND: // 点击菜单项会收到该消息 // 加速键按下也是这个消息代码
        {
            if (HIWORD(wParam)) // 加速键消息 - 这一步其实不是很有必要,现实使用中一般也不会去区分是直接点击的菜单还是按的加速键,这里只是演示这个功能
            {
                switch (LOWORD(wParam))
                {
                    case ID_40004:
                    {
                        MessageBox(NULL, L"加速键触发菜单项1", L"消息", MB_OK);
                        break;
                    }
                    case ID_40005:
                    {
                        MessageBox(NULL, L"加速键触发菜单项2", L"消息", MB_OK);
                        break;
                    }
                    case ID_40006:
                    {
                        MessageBox(NULL, L"加速键触发菜单项3", L"消息", MB_OK);
                        break;
                    }
                    default:
                    {
                        break;
                    }
                }
                break;
            }

            switch (LOWORD(wParam))
            {
                case ID_40001:
                {
                    MessageBox(NULL, L"点击新建", L"消息", MB_OK);
                    break;
                }
                case ID_40002:
                {
                    MessageBox(NULL, L"点击退出", L"消息", MB_OK);
                    break;
                }
                case ID_40003:
                {
                    MessageBox(NULL, L"点击关于", L"消息", MB_OK);
                    break;
                }
                case ID_40004:
                {
                    MessageBox(NULL, L"点击菜单项1", L"消息", MB_OK);
                    break;
                }
                case ID_40005:
                {
                    MessageBox(NULL, L"点击菜单项2", L"消息", MB_OK);
                    break;
                }
                case ID_40006:
                {
                    MessageBox(NULL, L"点击菜单项3", L"消息", MB_OK);
                    break;
                }
                default:
                {
                    break;
                }
            }
            break;
        }
        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;
    }

    HMENU hMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDR_MENU1));
    g_popup = GetSubMenu(hMenu, 2);

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

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

    HACCEL hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR1)); // 加载加速键资源文件
    MSG msg = {0};
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateAccelerator(hWnd, hAccel, &msg); // 翻译加速键
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return (int)msg.wParam;
}

file
file
file
file