WindowsAPI窗口程序设计-菜单与资源

菜单-菜单资源法

开始

在资源文件区新建.rc资源文件,在Menu字样下新建一个菜单IDR_MENU,然后就可以可视的方式设计菜单。双击每个菜单项,在属性窗口中设置ID,例如ID_FILE_NEW、ID_FILE_OPEN等。

菜单添加

法一:设置WNDCLASSEX结构的lpszMenuName字段,需要把IDR_MENU转为LPCTSTR类型。

1
wcex.lpszMenuName = MAKEINTRESOURCE(IDR_MENU);

法二:创建窗口时指定hMenu参数:

1
2
3
HMENU hMenu;
hMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDR_MENU));
HWND hWnd = CreateWindowEx(0, szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 300, 180, NULL, hMenu, hInstance, NULL);

其中LoadMenu用来加载菜单资源:

1
2
3
4
HMENU WINAPI LoadMenu(
_In_opt_ HINSTANCE hInstance, //要加载的菜单资源所属模块句柄
_In_ LPCTSTR lpMenuName //菜单资源名称
)

法三:用SetMenu为指定窗口设置菜单:

1
2
3
4
5
case WM_CREATE: {
hMenu = LoadMenu(g_hInstance, MAKEINTRESOURCE(IDR_MENU));
SetMenu(hwnd, hMenu);
return 0;
};

其中SetMenu用法:

1
2
3
4
BOOL WINAPI SetMenu(
_In_ HWND hWnd, //窗口句柄
_In_opt_ HMENU hMenu //菜单句柄 NULL删除菜单
)

加速键

在.rc文件的Accelerator项中。在键栏填大写字母时,类型填VIRTKEY表示字母键;填“^大写字母”时,类型填ASCII,表示Ctrl键加字母键;填大小写字母或ASCII值时,类型填ASCII,表示字母键。

在消息循环调用中TranslateAccelerator函数,它监视消息队列中WM_KEYDOWN或WM_KEYUP,并转化为WM_COMMAND或WM_SYSCOMMAND。

先用LoadAccelerators加载加速键表:

1
2
3
4
HACCEL WINAPI LoadAccelerators(
_In_opt_ HINSTANCE hInstance, //所属模块句柄
_In_ LPCTSTR lpTableName //加速键表名
)//成功返回加速键表句柄类型

然后用TranslateAccelerator函数:

1
2
3
4
5
INT WINAPI TranslateAccelerator(
_In_ HWND hWnd, //处理哪个窗口消息
_In_ HACCEL hAccTable, //加速键表句柄
_In_ LPMSG lpMsg //消息结构
)//成功非0 失败0

一般消息处理改为这样写:

1
2
3
4
5
6
HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_WINDOWSPROJECT1));
while (GetMessage(&msg, NULL, 0, 0))
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
};

对于WM_COMMAND消息,一般参数:

消息来源 HIWORD(wParam) LOWORD(wParam) lParam
菜单命令项 0 菜单项ID 0
加速键 1 菜单项ID 0
子窗口控件 通知码 控件ID 控件句柄

对于WM_SYSCOMMAND消息,一般直接给DefWindowProc默认处理,一些常用系统命令:

常量 含义
SC_MINIMIZE 0xF020 最小化窗口
SC_MAXIMIZE 0xF030 最大化窗口
SC_RESTORE 0xF120 恢复窗口位置大小
SC_CLOSE 0xF060 关闭窗口
SC_SIZE 0xF000 调整窗口大小
SC_MOVE 0xF010 移动窗口
SC_HSCROLL 0xF080 水平滚动
SC_VSCROLL 0xF070 垂直滚动
SC_HOTKEY 0xF150 全局热键消息

当用鼠标选择系统菜单命令项时,分别用GET_X_LPARAM(lParam)GET_Y_LPARAM(lParam)获取鼠标光标X和Y坐标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#include <Windows.h>
#include <tchar.h>
#include "resource.h"
// 全局变量
HINSTANCE g_hInstance;
// 函数声明,窗口过程
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, INT nCmdShow) {
WNDCLASSEX wndclass;
TCHAR szClassName[] = TEXT("MyWindow");
TCHAR szAppName[] = TEXT("HelloWindows");
HWND hwnd;
MSG msg;
g_hInstance = hInstance;
wndclass.cbSize = sizeof(WNDCLASSEX);
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WindowProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szClassName;
wndclass.hIconSm = NULL;
RegisterClassEx(&wndclass);
hwnd = CreateWindowEx(0, szClassName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 400, 300, NULL, NULL, hInstance, NULL);
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
HACCEL hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACC));
while (GetMessage(&msg, NULL, 0, 0) != 0)
if (!TranslateAccelerator(hwnd, hAccel, &msg)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
};
return msg.wParam;
};
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
HMENU hMenu;
TCHAR szBuf[64] = { 0 };
switch (uMsg) {
case WM_CREATE: {
hMenu = LoadMenu(g_hInstance, MAKEINTRESOURCE(IDR_MENU));
SetMenu(hwnd, hMenu);
return 0;
};
case WM_COMMAND: {
switch (LOWORD(wParam)) {
case ID_FILE_NEW: { // 新建
wsprintf(szBuf, TEXT("您点击了 新建 菜单项,命令ID:%d\n"), ID_FILE_NEW);
MessageBox(hwnd, szBuf, TEXT("提示"), MB_OK);
break;
};
case ID_EDIT_CUT: { // 剪切
wsprintf(szBuf, TEXT("您点击了 剪切 菜单项,命令ID:%d\n"), ID_EDIT_CUT);
MessageBox(hwnd, szBuf, TEXT("提示"), MB_OK);
break;
};
case ID_HELP_ABOUT: { // 关于HelloWindows
wsprintf(szBuf, TEXT("您点击了 关于HelloWindows 菜单项,命令ID:%d\n"), ID_HELP_ABOUT);
MessageBox(hwnd, szBuf, TEXT("提示"), MB_OK);
break;
};
case ID_FILE_EXIT: { // 退出
wsprintf(szBuf, TEXT("您点击了 退出 菜单项,命令ID:%d\n"), ID_FILE_EXIT);
MessageBox(hwnd, szBuf, TEXT("提示"), MB_OK);
SendMessage(hwnd, WM_CLOSE, 0, 0);
break;
};
};
return 0;
};
case WM_SYSCOMMAND: {
switch (wParam & 0xFFF0) {
case SC_CLOSE: {
MessageBox(hwnd, TEXT("您点击了 系统菜单 关闭 菜单项"), TEXT("提示"), MB_OK);
SendMessage(hwnd, WM_CLOSE, 0, 0);
break;
};
default: {
return DefWindowProc(hwnd, uMsg, wParam, lParam);
};
};
return 0;
};
case WM_DESTROY: {
PostQuitMessage(0);
return 0;
};
};
return DefWindowProc(hwnd, uMsg, wParam, lParam);
};

菜单-动态创建法

太麻烦了,略。

菜单-菜单项加载法

CreateMenu创建一个空菜单:

1
HMENU CreateMenu(VOID); //返回菜单句柄

AppendMenu向菜单句柄添加菜单项:

1
2
3
4
5
6
BOOL WINAPI AppendMenu(
_In_ HMENU hMenu, //句柄
_In_ UINT uFlags, //选项
_In_ UINT_PTR uIDNewItem, //菜单项ID/弹出菜单句柄
_In_opt_ LPCTSTR lpNewItem //名称/位图句柄
)

uFlags的常用取值有:

枚举值 含义
MF_POPUP 弹出菜单
MF_STRING 文本字符串菜单项
MF_CHECKED 复选标志
MF_UNCHECKED 没有复选标志
MF_DISABLED 禁用
MF_ENABLED 启用
MF_GRAYED 灰化
MF_SEPARATOR 分隔符

最后用SetMenu显示到菜单栏。

实例,添加“文件”和“编辑”两个主菜单项:

1
2
3
4
5
6
7
8
9
10
11
12
hMenu = CreateMenu();
hMenuPopup = CreateMenu();
AppendMenu(hMenuPopup, MF_STRING, ID_FILE_NEW, TEXT("新建"));
AppendMenu(hMenuPopup, MF_STRING, ID_FILE_OPEN, TEXT("打开"));
AppendMenu(hMenuPopup, MF_STRING, ID_FILE_SAVE, TEXT("保存"));
AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT_PTR)hMenuPopup, TEXT("文件"));
hMenuPopup = CreateMenu();
AppendMenu(hMenuPopup, MF_STRING, ID_EDIT_CUT, TEXT("剪切"));
AppendMenu(hMenuPopup, MF_STRING, ID_EDIT_COPY, TEXT("复制"));
AppendMenu(hMenuPopup, MF_STRING, ID_EDIT_PASTE, TEXT("黏贴"));
AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT_PTR)hMenuPopup, TEXT("编辑")); //“编辑”是第2个主菜单项 编号为1
SetMenu(hwnd, hMenu);

在已有菜单基础上添加“视图”主菜单项,用GetMenu获取窗口菜单句柄,用DrawMenuBar强制刷新菜单栏:

1
2
3
4
5
6
hMenu = GetMenu(hwnd);
hMenuPopup = CreateMenu();
AppendMenu(hMenuPopup, MF_STRING, ID_VIEW_BIG, TEXT("大图标"));
//...
AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT_PTR)hMenuPopup, TEXT("视图"));
DrawMenuBar(hwnd);

GetSubMenu获取弹出菜单或子菜单项句柄:

1
2
3
4
HMENU WINAPI GetSubMenu(
_In_ HMENU hMenu, //菜单句柄
_In_ INT nPos //从0开始相对位置
)

向“编辑”菜单中添加子菜单项:

1
2
3
4
5
6
7
8
9
hMenu = GetMenu(hwnd);
hMenuPopup = GetSubMenu(hMenu, 1);
AppendMenu(hMenuPopup, MF_SEPARATOR, NULL, NULL);
AppendMenu(hMenuPopup, MF_STRING, ID_EDIT_RED, TEXT("红色(&R)"));
//...
hMenuPopupSub = CreateMenu();
AppendMenu(hMenuPopupSub, MF_STRING, ID_EDIT_UPPER, TEXT("大写字母(&U)"));
//...
AppendMenu(hMenuPopup, MF_STRING | MF_POPUP, (UINT_PTR)hMenuPopupSub, TEXT("更改大小写(&N)"));

InsertMenu添加菜单项并指定插入位置:

1
2
3
4
5
6
7
BOOL WINAPI InsertMenu(
_In_ HMENU hMenu, //菜单句柄
_In_ UINT uPosition, //新菜单项插入位置
_In_ UINT uFlags, //选项
_In_ UINT_PTR uIDNewItem, //菜单项ID/弹出菜单句柄
_In_opt_ LPCTSTR lpNewItem //名称/位图句柄
)

其中当uFlags为MF_YCOMMAND时,uPosition指定菜单项ID;当为MF_BYPOSITION时,uPosition指定新菜单项从0开始的相对位置,-1则插入菜单末尾。

将“视图”调整为第3个:

1
2
3
4
5
6
hMenu = GetMenu(hwnd);
hMenuPopup = CreateMenu();
AppendMenu(hMenuPopup, MF_STRING, ID_VIEW_BIG, TEXT("大图标"));
//...
InsertMenu(hMenu, 2, MF_STRING | MF_POPUP | MF_BYPOSITION, (UINT_PTR)hMenuPopup, TEXT("视图"));
DrawMenuBar(hwnd);

系统默认菜单

就是点左上角图标蹦出来的那个。

获取系统菜单句柄:

1
2
3
4
HMENU WINAPI GetSystemMenu(
_In_ HWND hWnd, //窗口句柄
_In_ BOOL bRevert
)

其中当bRevert为TRUE时,表示获取系统菜单句柄;为FALSE时,表示系统菜单恢复为默认状态,返回NULL。

系统菜单项ID都为0xF000以上,向系统菜单添加的菜单项ID都必须小于0xF000。

修改菜单项:

1
2
3
4
5
6
7
BOOL WINAPI ModifyMenu(
_In_ HMENU hMenu,
_In_ UINT uPosition,
_In_ UINT uFlags,
_In_ UINT_PTR uIDNewItem,
_In_opt_ LPCTSTR lpNewItem
)

例如灰化系统菜单的关闭菜单项:

1
ModifyMenu(GetSystemMenu(hwnd, FALSE), SC_CLOSE, MF_GRAYED, SC_CLOSE, TEXT("关闭(&C)\tAlt+F4"));

删除菜单项:

1
RemoveMenu(GetSystemMenu(hwnd, FALSE), SC_CLOSE, MF_BYCOMMAND);

系统菜单后添加一个“打开”菜单项:

1
AppendMenu(GetSystemMenu(hwnd, FALSE), MF_STRING, ID_FILE_NEW, TEXT("打开(&O)"));

恢复系统菜单:

1
GetSystemMenu(hwnd, TRUE);

EnableMenuItem启用、禁用或灰化一个菜单项,比ModifyMenu简单:

1
2
3
4
5
BOOL WINAPI EnableMenuItem(
_In_ HMENU hMenu, //菜单句柄
_In_ UINT uIDEnableItem, //要控制的菜单项/菜单项ID/相对位置
_In_ UINT uEnable //MF_BYCOMMAND MF_BYPOSITION MF_ENABLED MF_DISABLED MF_GRAYED
)

CheckMenuItem设置或取消复选标志:

1
2
3
4
5
DWORD WINAPI CheckMenuItem(
_In_ HMENU hMenu, //菜单句柄
_In_ UINT uIDCheckItem, //菜单项ID/相对位置
_In_ UINT uCheck //MF_BYCOMMAND MF_BYPOSITION MF_CHECKED MF_UNCHECKED
) //返回先前MF_CHECKED或MF_UNCHECKED 不存在-1

几个菜单项之间模仿一组单选按钮:

1
2
3
4
5
6
7
BOOL WINAPI CheckMenuRadioItem(
_In_ HMENU hMenu, //包含菜单项组的菜单句柄
_In_ UINT idFirst, //组中第一个菜单项的ID/相对位置
_In_ UINT idLast, //组中最后一个菜单项的ID/相对位置
_In_ UINT idCheck, //要设置选定菜单项的ID/相对位置
_In_ UINT uFlags
)

当uFlags为MF_BYCOMMAND时,组中ID在数值上是连续的;当uFlags为MF_BYPOSITION时,在菜单栏中位置是连续的。

右键弹出菜单

CreatePopupMenu创建一个空的弹出菜单:

1
HMENU WINAPI CreatePopupMenu(VOID);

GetMenuItemCount获取指定菜单句柄下菜单项个数:

1
2
3
INT GetMenuItemCount(
_In_opt_ HMENU hMenu
)

例如:

1
2
hMenu = GetMenu(hwnd);
n = GetMenuItemCount(GetSubMenu(hMenu, 1)); //主菜单项的第2个菜单“编辑”的菜单项个数

GetMenuString获取一个菜单项文本字符串:

1
2
3
4
5
6
7
INT WINAPI GetMenuString(
_In_ HMENU hMenu, //菜单句柄
_In_ UINT uIDItem, //ID/相对位置
_Out_opt_ LPTSTR lpString, //返回菜单项名称
_In_ INT nMaxCount, //缓冲区大小 单位字节
_In_ UINT uFlag //MF_BYCOMMAND MF_BYPOSITION
)//lpString缓冲区内字节数 不包括终止空字符 失败返回0

右键弹出菜单:

1
2
3
4
5
6
7
8
9
BOOL WINAPI TrackPopupMenu(
_In_ HMENU hMenu, //弹出菜单句柄
_In_ UINT uFlags, //标志
_In_ INT x, //X坐标
_In_ INT y, //Y坐标
_In_ INT nReserved,
_In_ HWND hWnd, //接收菜单命令消息的快捷菜单所属窗口句柄
_In_opt_ CONST PRECT prcRect
)

参数uFlags有些常用标志:

枚举值 含义
TPM_LEFTALIGN 快捷菜单左侧与X参数指定坐标对齐
TPM_TOPALIGN 快捷菜单顶部与Y参数指定坐标对齐

例如弹出“编辑”菜单项:

1
2
3
4
5
6
7
case WM_RBUTTONDOWN: {
pt.x = GET_X_LPARAM(lParam);
pt.y = GET_Y_LPARAM(lParam);
ClientToScreen(hwnd, &pt);
TrackPopupMenu(GetSubMenu(GetMenu(hwnd), 1), TPM_LEFTALIGN | TPM_TOPALIGN, pt.x, pt.y, 0, hwnd, NULL);
return 0;
};

没法直接用TrackPopupMenu显示整个主菜单,有别的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
HMENU hMenu, hMenuPopup;
POINT pt;
TCHAR szBuf[32] = { 0 };
case WM_RBUTTONDOWN: {
hmenu = GetMenu(hwnd);
hMenuPopup = CreatePopupMenu();
for (SIZE_T i = 0; i < GetMenuItemCount(hMenu); i++) {
GetMenuString(hMenu, i, szBuf, _countof(szBuf), MF_BYPOSITION);
AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT_PTR)GetSubMenu(hMenu, i), szBuf);
};
pt.x = GET_X_LPARAM(lParam);
pt.y = GET_Y_LPARAM(lParam);
ClientToScreen(hwnd, &pt);
TrackPopupMenu(hMenuPopup, TPM_LEFTALIGN | TPM_TOPALIGN, pt.x, py.y, 0, hwnd, NULL);
return 0;
};

扩展菜单项&菜单项自绘

挖坑不打算填。

图标

在资源文件.rc中导入图标.ico文件,然后这里设置:

1
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WINDOWSPROJECT1));

动态更换窗口标题栏左侧图标和任务栏图标:

1
SetClassLongPtr(hwnd, GCLP_HICON, (LONG)LoadIcon(g_hInstance, MAKEINTRESOURCE(IDI_EAGLE)));

动态更换可执行文件图标:

1
SendMessage(hwnd, WM_SETICON, ICON_BIG, (LPARAM)LoadIcon(g_hInstance, MAKEINTRESOURCE(IDI_EAGLE)));

光标

在资源文件.rc中导入静态光标.cur文件,然后这里设置:

1
wcex.hCursor = LoadCursor(hInstance, MAKEINTRESOURCE(IDC_POINT));

动态光标类型只能从外部文件加载,当然静态光标也可以这么加载:

1
wcex.hCursor = LoadCursorFromFile(TEXT("ninjanormal.ani"));

修改指定系统光标:

1
2
3
4
BOOL WINAPI SetSystemCursor(
_In_ HCURSOR hcur, //用来替换的光标
_In_ DWORD id //系统光标ID 自己查
)

系统光标ID以OCR_开头,但有一一对应的IDC_进行MAKEINTRESOURCE封装,使用OCR_资源前,需要在Windows.h包含前:

1
#define OEMRESOURCE 1

例如这样两种方法,但重启计算机后又恢复:

1
2
SetSytemCursor(CopyCursor(LoadCursor(g_hInstance, MAKEINTRESOURCE(IDC_POINT))), OCR_NORMAL);
SetSytemCursor(CopyCursor(LoadCursorFromFile(TEXT("ninjanormal.ani"))), OCR_NORMAL);

动态更换光标同样两种方法:

1
2
SetClassLongPtr(hwnd, GCLP_HCURSOR, (LONG)LoadCursor(g_hInstance, MAKEINTRESOURCE(IDC_POINT)));
SetClassLongPtr(hwnd, GCLP_HCURSOR, (LONG)LoadCursorFromFile(TEXT("ninjanormal.ani")));

自定义资源

获取模块中指定类型和名称的资源:

1
2
3
4
5
HRSRC WINAPI FindResource(
_In_opt_ HMODULE hModule, //模块句柄
_In_ LPCTSTR lpName, //资源名称
_In_ LPCTSTR lpType //资源类型名称
) //成功返回资源信息块句柄 失败NULL

失败时用GetLastError获取错误信息:

1
DWORD GetLastError(VOID); //0成功完成 1功能错误 2找不到指定文件

通过资源信息块句柄得到资源句柄:

1
2
3
4
HGLOBAL WINAPI LoadResource(
_In_opt_ HMODULE hModule, //模块句柄
_In_ HRSRC hResInfo //资源信息块句柄
)

获取内存中资源数据指针:

1
2
3
LPVOID WINAPI LockResource(
_In_ HANDLE hResData
)

获取资源大小:

1
SizeofResource(g_hInstance, hResBlock); //返回DWORD类型资源大小

例如在.rc文件中新建WAVE文件类型的文件夹,其中有IDR_WAVE1,则歌曲播放方法:

1
2
3
4
5
#pragma comment(lib, "Winmm.lib")
HRSRC hResBlock = FindResource(g_hInstance, MAKEINTRESOURCE(IDR_WAVE), TEXT("WAVE"));
HANDLE hRes = LoadResource(g_hInstance, hResBlock);
LPVOID lpMusic = LockResource(hRes);
PlaySound((LPCTSTR)lpMusic, NULL, SND_MEMORY | SND_ASYNC | SND_LOOP);

资源不需要手动释放,不需要时系统自动释放。