ReactOS 窗口系统分析(11):菜单(上)--- 对象与构造 --- menu.c
系列:窗口系统三大主线之窗口管理 · 第 11 册
对象文件:
win32ss/user/ntuser/menu.c(6699 行,拆两册,本册为对象与构造 )配套头文件:
win32ss/user/ntuser/menu.h、win32ss/include/ntuser.h(MENU/POPUPMENU/ITEM)用户态侧:
win32ss/user/user32/windows/menu.c主题:菜单对象模型、创建/插入/移除/查询/销毁、系统菜单、菜单栏绘制、菜单项信息通道
目录
- [1. 概述](#1. 概述)
- [2. 设计动机](#2. 设计动机)
- [3. 核心数据结构](#3. 核心数据结构)
- [4. 菜单对象生命周期](#4. 菜单对象生命周期)
- [5. 核心函数逐一展开](#5. 核心函数逐一展开)
- [6. 菜单项插入算法](#6. 菜单项插入算法)
- [7. 系统菜单构建](#7. 系统菜单构建)
- [8. 菜单栏绘制(MENU_DrawMenuBar 调用链)](#8. 菜单栏绘制(MENU_DrawMenuBar 调用链))
- [9. 与周边模块的关系](#9. 与周边模块的关系)
- [10. 调用链图](#10. 调用链图)
- [11. 源码索引](#11. 源码索引)
1. 概述
菜单子系统是 ReactOS 窗口管理中最复杂的一块。它既要支持顶层菜单栏 (挂在窗口客户区上方的一条横栏),也要支持弹出式菜单 (右键菜单、下拉子菜单、系统菜单),还要承担模态菜单循环 (TrackPopupMenuEx 捕获鼠标键盘、高亮导航、子菜单展开)与自绘菜单(owner-draw)等重活。
总览文档 5.4 节把 menu.c 的职责归纳为五条主线:
- 菜单对象 :
PMENU/PITEM,MENUINFO包含菜单样式、项数组;菜单内存分配自桌面堆(Desktop Heap)。 - 菜单构造 :
CreateMenu/CreatePopupMenu/AppendMenu/InsertMenuItem/LoadMenu(用户态解析资源后逐项插入)。 - 菜单栏 :
IntSetMenu把菜单挂到窗口,MENU_DrawMenuBar绘制顶层菜单栏。 - 菜单循环 :
TrackPopupMenuEx(NtUserTrackPopupMenuEx L6634)进入模态菜单循环(本册仅挂起,下册详述)。 - 消息交互:WM_MENUSELECT/WM_INITMENU/WM_INITMENUPOPUP/WM_COMMAND/WM_MEASUREITEM/WM_DRAWITEM 等。
本册聚焦前三条主线------对象与构造:菜单从何而来(CreateMenu → InsertMenuItem → SetMenu 链),对象内部长什么样,如何被查询、修改与销毁,系统菜单如何构建,菜单栏如何绘制。
1.1 核心调用链总览(ASCII 架构图)
用户态 user32.dll 内核态 win32k.sys(ntuser/menu.c)
────────────────────────────── ────────────────────────────────
CreateMenu() NtUserCallNoParam(CREATEMENU)
└─ NtUserxCreateMenu() └─ UserCreateMenu(rpdesk, FALSE)
└─ NtUserCallNoParam() └─ IntCreateMenu(&h, FALSE, desk, ppi)
├─ UserCreateObject(TYPE_MENU) ← 桌面堆分配 MENU
└─ 初始化 fFlags/cItems/rgItems...
AppendMenuW() / InsertMenuW() / InsertMenuItemW() NtUserThunkedMenuItemInfo(hMenu,item,bypos,INSERT,...)
└─ MENU_mnu2mnuii() 组装 MENUITEMINFOW ├─ ProbeAndCaptureUnicodeString(标题)
└─ NtUserThunkedMenuItemInfo(...) └─ UserInsertMenuItem(Menu, uItem, fByPosition, lpmii, lpstr)
└─ IntInsertMenuItem()
├─ MENU_InsertItem() ← 数组扩容/移位
└─ IntSetMenuItemInfo() ← 填充 ITEM 各字段
SetMenu(hWnd, hMenu) NtUserSetMenu(hWnd, hMenu, Repaint=TRUE)
└─ NtUserSetMenu(...) └─ IntSetMenu(Wnd, Menu, &Changed)
├─ 校验子窗口/句柄/占用
├─ Wnd->IDMenu = (UINT_PTR)Menu ← 挂到窗口
└─ co_WinPosSetWindowPos(SWP_FRAMECHANGED) 触发重绘
WM_NCPAINT(nonclient.c) ┌─ UserGetMenuObject(Wnd->IDMenu)
└─ MENU_DrawMenuBar(hDC, &rect, pWnd, FALSE) ──────────┘
├─ MENU_MenuBarCalcSize() 计算各项 xItem/cxItem/cyMenu
└─ MENU_DrawMenuItem() 逐项绘制(文本/位图/自绘/分隔符)
GetMenuState()/GetMenuStringW()/GetSubMenu()/GetMenuItemID() ← 直接在 user32 读桌面堆共享映射!
(不经过系统调用:MENU_FindItem 走 DesktopPtrToUser)
1.2 一个关键事实:很多"菜单 API"不在内核
翻阅 menu.c 的 NtUser* 导出表会发现,Windows 上很多菜单 API 在 ReactOS 里并没有对应的内核系统调用,而是由 user32.dll 直接读取桌面堆的共享映射完成:
| API | 实现位置 | 机制 |
|---|---|---|
CreateMenu/CreatePopupMenu |
user32 → NtUserCallNoParam | NOPARAM_ROUTINE_CREATEMENU(simplecall.c L68) |
DrawMenuBar |
user32 → NtUserCallHwndLock | HWNDLOCK_ROUTINE_DRAWMENUBAR(simplecall.c L611)→ 实际只做 SWP_FRAMECHANGED,绘制发生在 WM_NCPAINT |
GetMenu |
user32 直接读 Wnd->IDMenu |
不调用内核(共享结构) |
GetMenuState/GetMenuString/GetSubMenu/GetMenuItemID/GetMenuItemCount |
user32 直接遍历共享 ITEM 数组 | 不调用内核 |
Get/SetMenuItemInfo、InsertMenuItem、ModifyMenu、AppendMenu |
user32 组装后 → NtUserThunkedMenuItemInfo |
单一通用入口(bInsert 区分插入/修改) |
Get/SetMenuInfo |
user32 → NtUserThunkedMenuInfo |
单一通用入口 |
DestroyMenu/RemoveMenu/DeleteMenu/EnableMenuItem/CheckMenuItem/GetSystemMenu/SetMenu 等 |
user32 → 对应 NtUser* 系统调用 | 真系统调用 |
这解释了为什么 menu.c 里见不到 NtUserGetMenuState、NtUserGetMenuString、NtUserModifyMenu、NtUserCreateMenu 这些名字------它们不存在 。内核只保留 IntGetMenuState、IntGetSubMenu、IntFindSubMenu、IntGetMenuItemRect 等内部函数供 win32k 自身(系统菜单、菜单栏信息、自绘回调)使用,用户态应用一律走 user32 的直读路径或通用 thunk 入口。这个"共享桌面堆 + 少量通用入口"的设计是 ReactOS 菜单子系统与 Windows 内核菜单实现的重要差异点。
2. 设计动机
2.1 菜单资源 vs 运行时菜单
Windows 的菜单有两种来源:
- 编译期资源 :应用在 .rc 文件里用 MENU/MENUEX 模板声明,
LoadMenu/LoadMenuIndirect在运行时解析模板,逐项插入生成一个普通 HMENU。 - 运行时构造 :应用调用
CreateMenu/CreatePopupMenu拿空菜单,再用AppendMenu/InsertMenuItem等逐项填充。
ReactOS 对两种来源走同一条内核路径:资源解析完全发生在 user32 (MENU_ParseResource/MENUEX_ParseResource),内核只提供"创建空对象 + 插入项"的原子操作。这样内核菜单对象保持纯净,不需要理解资源格式。
2.2 菜单对象为何必须是内核对象
菜单句柄 HMENU 是 USER 对象(TYPE_MENU),理由是:
- 进程无关性 :一个菜单可以被挂到多个窗口,甚至被
SetMenu到别的进程创建的窗口(经 handle 复制语义),需要内核对象管理器统一引用计数。 - 安全:菜单项文本、子菜单指针都在桌面堆中,用户态无法伪造指针(只能通过系统调用或共享只读映射访问)。
- 窗口联动 :菜单与窗口(
Wnd->IDMenu、Wnd->SystemMenu)相互引用,销毁窗口时要能反向清理菜单;销毁菜单时要能通知窗口重绘(IntEnableMenuItem中 SC_* 项变化 →UserPaintCaption)。 - 自绘回调:菜单需要向拥有窗口发送 WM_MEASUREITEM/WM_DRAWITEM/WM_INITMENU 等消息,这只有内核态能安全完成(经 KeUserModeCallback)。
2.3 系统菜单
每个顶层窗口(带 WS_SYSMENU)都有一个系统菜单:标题栏左侧图标点击弹出的那份 SC_* 菜单。系统菜单必须可被应用修改 (如加"关于"项),但每个窗口又要有一份独立的副本(改动不互相影响),因此设计成"模板加载 + 逐窗口克隆":MENU_GetSystemMenu 从 user32 回调加载 SYSMENU 资源模板,再 IntCloneMenu 克隆一份挂到窗口的 SystemMenu 字段。
2.4 自绘(owner-draw)支持
菜单项可以是 MF_OWNERDRAW:尺寸由拥有窗口的 WM_MEASUREITEM 决定(MENU_CalcItemSize L1842-1876),绘制由 WM_DRAWITEM 回调完成(MENU_DrawMenuItem)。这要求菜单尺寸计算与绘制都必须能回到拥有窗口的线程上下文------这也是为什么 MENU->spwndNotify(接收自绘消息的窗口)贯穿整个 menu.c。
2.5 全局唯一的"当前活动菜单"
menu.c 顶部有一组全局变量:
c
static HWND top_popup = NULL; // 当前顶层弹出菜单窗口(全局只有一个)
static HMENU top_popup_hmenu = NULL; // 对应菜单句柄
BOOL fInsideMenuLoop = FALSE; // 是否处于菜单循环
BOOL fInEndMenu = FALSE; // 是否正在结束菜单(防重入)
注释写明:"Use global popup window because there's no way 2 menus can be tracked at the same time."(L20-21)------即 ReactOS 同一时刻只允许一个菜单被跟踪,这是模态菜单循环的单实例约束。MENU_IsMenuActive()、MENU_EndMenu()、NtUserGetMenuBarInfo(fBarFocused 判定)都依赖这两个全局量。
3. 核心数据结构
3.1 MENU 对象(ntuser.h L415-439)
c
typedef struct tagMENU
{
PROCDESKHEAD head; /* 对象头:桌面、进程、引用计数(USER 对象标准头) */
ULONG fFlags; /* [样式标志 | 菜单标志]:MNF_POPUP/MNF_SYSMENU/MNF_SYSSUBMENU... + MNS_* */
INT iItem; /* 选中(焦点)项位置;-1 = NO_SELECTED_ITEM 未选中 */
UINT cAlloced; /* 已分配项数(按 8 递增,历史遗留,现多用 cItems) */
UINT cItems; /* 菜单中的项数 */
ULONG cxMenu; /* 整个菜单的宽度 */
ULONG cyMenu; /* 整个菜单的高度(0 表示需要重新计算) */
ULONG cxTextAlign; /* 项同时含位图和文本时文本的偏移 */
struct _WND *spwndNotify; /* 接收自绘/通知消息的窗口 */
PITEM rgItems; /* 菜单项数组(桌面堆分配) */
struct tagMENULIST *pParentMenus; /* 若本菜单是子菜单,父菜单链表 */
DWORD dwContextHelpId; /* 上下文帮助 ID */
ULONG cyMax; /* 菜单最大高度;0 = 屏幕高度 */
DWORD_PTR dwMenuData; /* 应用自定义数据(MIM_MENUDATA) */
HBRUSH hbrBack; /* 菜单背景画刷(MIM_BACKGROUND) */
INT iTop; /* 当前滚动位置(顶部) */
INT iMaxTop; /* 当前滚动最大位置 */
DWORD dwArrowsOn:2; /* 滚动箭头:0 关,1 开,2 指上,3 指下 */
/* ReactOS 扩展 */
LIST_ENTRY ListEntry;
HWND hWnd; /* 包含本菜单的窗口(顶层菜单挂 Wnd->IDMenu 时) */
BOOL TimeToHide; /* 再次点击顶层菜单项时请求隐藏 */
} MENU, *PMENU;
要点:
head(PROCDESKHEAD)是 USER 对象头,含rpdesk(桌面指针)、hTaskWow(进程信息)等,IntCreateMenu用它拿到桌面堆句柄。fFlags是菜单标志 (MNF_)与菜单样式(MNS_ ,如 MNS_CHECKORBMP/MNS_NOCHECK)的合成位域。menu.c 用IS_SYSTEM_MENU(MenuInfo)宏检测MNF_SYSMENU(L60-61)。rgItems是动态数组 而非链表,插入删除靠RtlCopyMemory移动元素------这是后面插入算法的前提。cyMenu == 0是"尺寸脏"标记:MENU_InsertItem插入后menu->cyMenu = 0,绘制前MENU_MenuBarCalcSize/MENU_PopupMenuCalcSize会重新计算。
菜单标志常量(ntuser.h L401-410):
| 标志 | 值 | 含义 |
|---|---|---|
MNF_POPUP |
0x0001 | 弹出式菜单(CreatePopupMenu 创建时置位) |
MNF_UNDERLINE |
0x0004 | 显示下划线(Alt 键加速键) |
MNF_INACTIVE |
0x0010 | 非活动菜单 |
MNF_RTOL |
0x0020 | 从右到左布局 |
MNF_DESKTOPMN |
0x0040 | 桌面菜单 |
MNF_SYSDESKMN |
0x0080 | 系统桌面菜单 |
MNF_SYSSUBMENU |
0x0100 | 系统菜单的弹出子菜单(SC_* 那层) |
MNF_SYSMENU |
0x0200 | 系统菜单(Hack 标志) |
3.2 POPUPMENU 结构(ntuser.h L441-480)
每个弹出菜单窗口 (WC_MENU 类,PMENUWND)内嵌一个 POPUPMENU 记录跟踪状态:
c
typedef struct tagPOPUPMENU
{
ULONG fIsMenuBar:1; /* 本窗口是菜单栏 */
ULONG fHasMenuBar:1;
ULONG fIsSysMenu:1; /* 系统菜单 */
ULONG fIsTrackPopup:1; /* TrackPopupMenu 启动的 */
ULONG fDroppedLeft:1; /* 向左展开(RTL/对齐) */
ULONG fHierarchyDropped:1; /* 层级已展开 */
ULONG fRightButton:1; /* 右键触发 */
ULONG fToggle:1;
ULONG fSynchronous:1; /* TPM_RETURNCMD 同步返回 */
ULONG fFirstClick:1; /* 首次点击 */
ULONG fDropNextPopup:1;
ULONG fNoNotify:1; /* TPM_NONOTIFY */
ULONG fAboutToHide:1;
ULONG fShowTimer:1; /* 展开延迟定时器 */
ULONG fHideTimer:1; /* 收起延迟定时器 */
ULONG fDestroyed:1;
ULONG fDelayedFree:1; /* 延迟释放 */
ULONG fFlushDelayedFree:1;
ULONG fFreed:1;
ULONG fInCancel:1;
ULONG fTrackMouseEvent:1;
ULONG fSendUninit:1; /* 已发 WM_UNINITMENUPOPUP */
ULONG fRtoL:1;
ULONG iDropDir:5; /* 展开方向 */
ULONG fUseMonitorRect:1;
struct _WND *spwndNotify; /* 通知窗口(拥有者) */
struct _WND *spwndPopupMenu; /* 本弹出菜单窗口 */
struct _WND *spwndNextPopup; /* 层级链:下一个弹出窗口 */
struct _WND *spwndPrevPopup; /* 层级链:上一个弹出窗口 */
PMENU spmenu; /* 对应的 PMENU 对象 */
PMENU spmenuAlternate; /* 备用菜单(RTL 镜像) */
struct _WND *spwndActivePopup;
struct tagPOPUPMENU *ppopupmenuRoot; /* 根弹出菜单 */
struct tagPOPUPMENU *ppmDelayedFree;
UINT posSelectedItem; /* 选中项位置 */
UINT posDropped; /* 已展开的项位置 */
} POPUPMENU, *PPOPUPMENU;
这个结构是下册(菜单循环)的主战场;本册只在 MENU_InitPopup(L2781)中看到它的初始化:弹出菜单窗口创建时由 WM_CREATE(PopupMenuWndProc L4708)把 cs->lpCreateParams(即菜单句柄)转成 pPopupMenu->spmenu。
3.3 ITEM 菜单项(ntuser.h L370-393)
c
typedef struct tagITEM
{
UINT fType; /* 项类型:MF_STRING/MF_BITMAP/MF_OWNERDRAW/MF_SEPARATOR + MF_POPUP/MF_SYSMENU... */
UINT fState; /* 项状态:MF_CHECKED/MF_GRAYED/MF_DISABLED/MF_HILITE/MFS_DEFAULT/MFS_HBMMENUBMP... */
UINT wID; /* 项命令 ID(MF_BYCOMMAND 查找键) */
struct tagMENU *spSubMenu; /* 子菜单指针(本项是弹出项时) */
HANDLE hbmpChecked; /* 选中时位图 */
HANDLE hbmpUnchecked; /* 未选中时位图 */
USHORT *Xlpstr; /* 项文本指针(兼容旧代码的宽字符指针) */
ULONG cch; /* 文本字符数 */
DWORD_PTR dwItemData; /* 应用自定义数据 */
ULONG xItem; /* 项位置:左 */
ULONG yItem; /* 项位置:上 */
ULONG cxItem; /* 项宽度 */
ULONG cyItem; /* 项高度 */
ULONG dxTab; /* Tab 后文本的 X 位置(快捷键列) */
ULONG ulX; /* 下划线起始位置 */
ULONG ulWidth; /* 下划线宽度 */
HBITMAP hbmp; /* 项位图(MIIM_BITMAP) */
INT cxBmp; /* 位图最大宽度 */
INT cyBmp; /* 位图最大高度 */
/* ReactOS */
UNICODE_STRING lpstr; /* 项文本(真正的存储,Xlpstr 指向其 Buffer) */
} ITEM, *PITEM;
要点:
fType的类型位由宏MENU_ITEM_TYPE(flags)提取:(flags) & (MF_STRING | MF_BITMAP | MF_OWNERDRAW | MF_SEPARATOR)(menu.h L6-8)。IS_STRING_ITEM(flags)判断是否文本项(menu.c L64)。fState与fType在同一个 32 位里通过不同掩码区分:TYPE_MASK(menu.c L52)含 MF_POPUP/MF_SYSMENU 等类型位,STATE_MASK = ~TYPE_MASK是状态位。UpdateMenuItemState宏(L95-127)按位设置/清除 MF_GRAYED/MF_DISABLED/MFS_CHECKED/MFS_HILITE/MFS_DEFAULT/MF_MOUSESELECT。- 文本有两份表示:
Xlpstr(老式 USHORT*)与lpstr(UNICODE_STRING)。IntSetMenuItemInfo分配新文本后两者同步:MenuItem->Xlpstr = (USHORT*)MenuItem->lpstr.Buffer(L1188)。user32 侧绘制/取文本用Xlpstr,内核传输用lpstr。 spSubMenu指向子菜单 PMENU,插入子菜单时会UserReferenceObject增加引用 (L1144),销毁时IntReleaseMenuObject减引用(L1142/L1154)------防止父菜单还引用着已被销毁的子菜单。MFS_HBMMENUBMP(0x20000000,ntuser.h L368)标记"神奇位图"(HBMMENU_* 系统内置位图,如 HBMMENU_POPUP_CLOSE),这类位图不属于进程,销毁时不能GreSetObjectOwner还给进程(IntDestroyMenuL287 有!(item->fState & MFS_HBMMENUBMP)判断)。
3.4 MENUINFO / MENUITEMINFOW(Win32 公共结构,winuser.h)
c
typedef struct tagMENUINFO { /* winuser.h L3842-3850 */
DWORD cbSize;
DWORD fMask; /* MIM_* */
DWORD dwStyle; /* MNS_* */
UINT cyMax;
HBRUSH hbrBack;
DWORD dwContextHelpID;
ULONG_PTR dwMenuData;
} MENUINFO;
typedef struct tagMENUITEMINFOW { /* winuser.h L3367-3382 */
UINT cbSize;
UINT fMask; /* MIIM_* */
UINT fType; /* MFT_* */
UINT fState; /* MFS_* */
UINT wID;
HMENU hSubMenu;
HBITMAP hbmpChecked;
HBITMAP hbmpUnchecked;
ULONG_PTR dwItemData;
LPWSTR dwTypeData; /* 文本缓冲区指针 */
UINT cch; /* 文本长度 */
HBITMAP hbmpItem; /* Win2000+ 才有 */
} MENUITEMINFOW;
掩码常量:
| MIIM_* | 值 | 含义 | MIM_* | 值 | 含义 | |
|---|---|---|---|---|---|---|
| MIIM_STATE | 0x1 | 状态 | MIM_MAXHEIGHT | 0x1 | 最大高度 | |
| MIIM_ID | 0x2 | 命令 ID | MIM_BACKGROUND | 0x2 | 背景画刷 | |
| MIIM_SUBMENU | 0x4 | 子菜单 | MIM_HELPID | 0x4 | 帮助 ID | |
| MIIM_CHECKMARKS | 0x8 | 选中/未选中位图 | MIM_MENUDATA | 0x8 | 菜单数据 | |
| MIIM_TYPE | 0x10 | 类型(含文本) | MIM_STYLE | 0x10 | 菜单样式 | |
| MIIM_DATA | 0x20 | 项数据 | MIM_APPLYTOSUBMENUS | 0x80000000 | 递归应用到子菜单 | |
| MIIM_STRING | 0x40 | 文本 | ||||
| MIIM_BITMAP | 0x80 | 项位图 | ||||
| MIIM_FTYPE | 0x100 | 仅类型位 |
3.5 MF_* 标志速查表(winuser.h L128-153)
| 标志 | 值 | 类别 | 说明 |
|---|---|---|---|
| MF_STRING | 0 | 类型 | 文本项(默认) |
| MF_BITMAP | 4 | 类型 | 位图项 |
| MF_POPUP | 16 | 类型 | 弹出子菜单项 |
| MF_OWNERDRAW | 256 | 类型 | 自绘项 |
| MF_SEPARATOR | 0x800 | 类型 | 分隔符 |
| MF_MENUBARBREAK | 32 | 类型 | 菜单栏断行(竖线) |
| MF_MENUBREAK | 64 | 类型 | 断行 |
| MF_RIGHTJUSTIFY | 0x4000 | 类型 | 右对齐(同 MF_HELP) |
| MF_CHECKED | 8 | 状态 | 勾选 |
| MF_GRAYED | 1 | 状态 | 置灰 |
| MF_DISABLED | 2 | 状态 | 禁用 |
| MF_HILITE | 128 | 状态 | 高亮 |
| MF_DEFAULT | 4096 | 状态 | 默认项(MFS_DEFAULT) |
| MF_SYSMENU | 0x2000 | 特殊 | 系统菜单项 |
| MF_END | 128 | 资源 | 资源模板结束标记 |
| MF_BYCOMMAND | 0 | 查找 | 按命令 ID 查找 |
| MF_BYPOSITION | 1024 | 查找 | 按位置查找 |
| MF_MOUSESELECT | 0x8000 | 状态 | 鼠标选中 |
类型掩码(menu.c L47-52):
c
#define MENUITEMINFO_TYPE_MASK \
(MFT_STRING | MFT_BITMAP | MFT_OWNERDRAW | MFT_SEPARATOR | \
MFT_MENUBARBREAK | MFT_MENUBREAK | MFT_RADIOCHECK | \
MFT_RIGHTORDER | MFT_RIGHTJUSTIFY)
#define TYPE_MASK (MENUITEMINFO_TYPE_MASK | MF_POPUP | MF_SYSMENU)
#define STATE_MASK (~TYPE_MASK)
#define MENUITEMINFO_STATE_MASK (STATE_MASK & ~(MF_BYPOSITION | MF_MOUSESELECT))
注意 MENUITEMINFO_STATE_MASK:状态位是除类型位之外的所有位,再刨掉查找用标志。
3.6 ROSMENUINFO / ROSMENUITEMINFO(ReactOS 内核传输结构,menu.h)
用户态的 MENUITEMINFOW 通过 NtUserThunkedMenuItemInfo 传给内核时,内核先把结构拷到栈上,但为了传输菜单内部私有数据(项矩形、文本指针等),ReactOS 定义了扩展版 结构,cbSize == sizeof(ROSMENUITEMINFO) 时多出的字段生效:
c
typedef struct tagROSMENUITEMINFO /* menu.h L100-120 */
{
/* ----------- MENUITEMINFOW ----------- */
UINT cbSize; UINT fMask; UINT fType; UINT fState; UINT wID;
HMENU hSubMenu; HBITMAP hbmpChecked; HBITMAP hbmpUnchecked;
DWORD dwItemData; LPWSTR dwTypeData; UINT cch; HBITMAP hbmpItem;
/* ----------- Extra(仅内核可见) ----------- */
RECT Rect; /* 项区域(相对菜单窗口) */
UINT dxTab; /* Tab 后文本 X */
LPWSTR lpstr; /* 文本指针副本 */
SIZE maxBmpSize; /* MIIM_BITMAP 状态下位图最大尺寸 */
} ROSMENUITEMINFO;
typedef struct tagROSMENUINFO /* menu.h L73-98 */
{
/* ----------- MENUINFO ----------- */
DWORD cbSize; DWORD fMask; DWORD dwStyle; UINT cyMax;
HBRUSH hbrBack; DWORD dwContextHelpID; ULONG_PTR dwMenuData;
/* ----------- Extra ----------- */
ULONG fFlags; /* 菜单标志(MF_POPUP/MF_SYSMENU...) */
UINT iItem; /* 当前焦点项 */
UINT cItems; /* 项数 */
WORD cxMenu, cyMenu; /* 菜单尺寸 */
ULONG cxTextAlign;
PWND spwndNotify;
INT iTop, iMaxTop;
DWORD dwArrowsOn:2;
HMENU Self; HWND Wnd; BOOL TimeToHide;
} ROSMENUINFO;
内核侧 IntMenuItemInfo(L5102)对 lpmii->cbSize 的判定:sizeof(ROSMENUITEMINFO) 才拷贝/回填扩展字段。UserMenuItemInfo(L5130)对 cbSize 的合法值校验:
c
if ( Size != sizeof(MENUITEMINFOW) &&
Size != FIELD_OFFSET(MENUITEMINFOW, hbmpItem) && /* 老版本无 hbmpItem */
Size != sizeof(ROSMENUITEMINFO) )
{ EngSetLastError(ERROR_INVALID_PARAMETER); return FALSE; }
且当 Size == FIELD_OFFSET(MENUITEMINFOW, hbmpItem)(即 < Win2000 的旧结构)时不允许 MIIM_BITMAP(L5165-5170),保证向后兼容。
3.7 MENUSTATE 状态机(menu.h L13-60)
MENUSTATE 描述"正在跟踪的菜单"的完整运行时状态,属于线程 (pti->pMenuState),由下册菜单循环使用。本册只列出其字段清单:
c
typedef struct tagMENUSTATE
{
PPOPUPMENU pGlobalPopupMenu; /* 全局活动弹出菜单 */
struct {
ULONG fMenuStarted:1; /* 菜单循环已开始 */
ULONG fIsSysMenu:1; /* 是系统菜单 */
ULONG fInsideMenuLoop:1; /* 在菜单循环内 */
ULONG fButtonDown:1; /* 按钮按下 */
ULONG fInEndMenu:1; /* 正在结束菜单 */
ULONG fUnderline:1; /* 显示下划线 */
ULONG fButtonAlwaysDown:1;
ULONG fDragging:1; /* 拖拽中 */
ULONG fModelessMenu:1; /* MNS_MODELESS */
ULONG fInCallHandleMenuMessages:1;
ULONG fDragAndDrop:1; /* MNS_DRAGDROP */
ULONG fAutoDismiss:1; /* MNS_AUTODISMISS */
ULONG fAboutToAutoDismiss:1;
ULONG fIgnoreButtonUp:1;
ULONG fMouseOffMenu:1;
ULONG fInDoDragDrop:1;
ULONG fActiveNoForeground:1;
ULONG fNotifyByPos:1; /* MNS_NOTIFYBYPOS */
ULONG fSetCapture:1;
ULONG iAniDropDir:5; /* 动画方向 */
};
POINT ptMouseLast; /* 最后鼠标位置 */
INT mnFocus; /* 焦点索引 */
INT cmdLast; /* 最后命令 ID */
PTHREADINFO ptiMenuStateOwner; /* 拥有者线程 */
DWORD dwLockCount; /* 锁计数 */
struct tagMENUSTATE* pmnsPrev; /* 前一个状态(嵌套) */
POINT ptButtonDown; /* 按下位置 */
ULONG_PTR uButtonDownHitArea;
UINT uButtonDownIndex;
INT vkButtonDown;
ULONG_PTR uDraggingHitArea;
UINT uDraggingIndex; UINT uDraggingFlags;
HDC hdcWndAni; DWORD dwAniStartTime;
INT ixAni, iyAni, cxAni, cyAni; /* 展开动画 */
HBITMAP hbmAni; HDC hdcAni;
} MENUSTATE, *PMENUSTATE;
fNotifyByPos 对应 MNS_NOTIFYBYPOS 菜单样式------WM_MENUSELECT/WM_COMMAND 的 wParam 传位置而不是命令 ID。
3.8 其他相关结构
MTRACKER(menu.c L78-85):菜单跟踪器,TrackFlags/CurrentMenu/TopMenu/OwnerWnd/Pt。SETMENUITEMRECT(menu.h L62-67):uItem/fByPosition/rcRect,设置菜单项矩形。- 内部跟踪标志(menu.c L35-37):
TF_ENDMENU 0x10000、TF_SUSPENDPOPUP 0x20000、TF_SKIPREMOVE 0x40000。 - 内部 TPM 标志(L88-90):
TPM_INTERNAL 0xF0000000、TPM_BUTTONDOWN 0x40000000、TPM_POPUPMENU 0x20000000。 - 弹出菜单窗口私有消息(L30-31):
MM_SETMENUHANDLE (WM_USER+0)、MM_GETMENUHANDLE (WM_USER+1)。 - 常量:
MAXMENUDEPTH 30(L43,Windows 是 25,故意放宽防环)、MAX_MENU_ITEMS 0x4000(L67)、NO_SELECTED_ITEM 0xffff(ntuser.h L413)。
4. 菜单对象生命周期
创建:NtUserCallNoParam(CREATEMENU) → UserCreateMenu → IntCreateMenu
│ UserCreateObject(TYPE_MENU) 在桌面堆分配 sizeof(MENU)
▼
填充:AppendMenuW/InsertMenuItemW → NtUserThunkedMenuItemInfo → UserInsertMenuItem
│ → IntInsertMenuItem → MENU_InsertItem(分配 ITEM 数组并移位)
│ → IntSetMenuItemInfo(文本/位图/子菜单/状态)
▼
挂窗:SetMenu(hWnd,hMenu) → NtUserSetMenu → IntSetMenu
│ Wnd->IDMenu = hMenu;NewMenu->hWnd = hWnd(互斥占用)
│ 旧菜单 hWnd 清空 → SWP_FRAMECHANGED 重绘
▼
使用:WM_NCPAINT → MENU_DrawMenuBar → MENU_MenuBarCalcSize + MENU_DrawMenuItem
│ WM_COMMAND:点击项 → 窗口过程处理
▼
卸载:SetMenu(hWnd, NULL) 或窗口销毁(winpos/defwnd 清理 IDMenu)
▼
销毁:DestroyMenu → NtUserDestroyMenu → IntDestroyMenuObject(bRecurse=TRUE)
│ 或进程/桌面清理 IntCleanupMenus(清理失败对象)
▼
回收:UserMarkObjectDestroy → IntDestroyMenu(递归释放子菜单、文本、位图)
→ UserDeleteObject(TYPE_MENU) → 桌面堆回收
4.1 创建:IntCreateMenu(L728-767)
c
PMENU FASTCALL IntCreateMenu(_Out_ PHANDLE Handle, _In_ BOOL IsMenuBar,
_In_ PDESKTOP Desktop, _In_ PPROCESSINFO ppi)
{
Menu = (PMENU)UserCreateObject( gHandleTable, Desktop, ppi->ptiList,
Handle, TYPE_MENU, sizeof(MENU));
...
Menu->cyMax = 0; /* 默认:不限高 */
Menu->hbrBack = NULL; /* 无背景画刷 */
Menu->dwContextHelpId = 0;
Menu->dwMenuData = 0;
Menu->iItem = NO_SELECTED_ITEM; /* 无焦点项 */
Menu->fFlags = (IsMenuBar ? 0 : MNF_POPUP); /* 菜单栏 vs 弹出菜单 */
Menu->spwndNotify = NULL;
Menu->cyMenu = 0; Menu->cxMenu = 0; /* 尺寸待计算 */
Menu->cItems = 0; Menu->rgItems = NULL; /* 空项数组 */
Menu->iTop = 0; Menu->iMaxTop = 0; Menu->cxTextAlign = 0;
Menu->hWnd = NULL; Menu->TimeToHide = FALSE;
return Menu;
}
UserCreateObject是 USER 对象工厂:分配对象、插入句柄表、返回 HANDLE。TYPE_MENU注册的析构回调是UserDestroyMenuObject(menu.h L134、menu.c L311)。IsMenuBar参数决定fFlags是否带MNF_POPUP:CreateMenu创建的菜单栏对象不带;CreatePopupMenu创建的弹出菜单带。这个位决定MENU_FindItem之外的诸多行为,比如IntGetMenuItemRect中坐标基准是rcWindow(菜单栏)还是rcClient(弹出菜单窗口)。- 注意
UserCreateObject返回的对象带一个引用,UserCreateMenu(L5058)在返回句柄前UserDereferenceObject(Menu)(L5097)把初始引用去掉,使对象处于"引用计数 0 但句柄有效"状态------之后每次IntGetMenuObject/UserGetMenuObject取用时临时 +1。
4.2 用户态入口:UserCreateMenu(L5058-5099)
c
HMENU FASTCALL UserCreateMenu(PDESKTOP Desktop, BOOL PopupMenu)
{
...
if (gpepCSRSS != CurrentProcess) /* 非 CSRSS 进程 */
{
/* 校验进程的 WindowStation 句柄 */
Status = IntValidateWindowStationHandle(CurrentProcess->Win32WindowStation,
UserMode, 0, &WinStaObject, 0);
...
Menu = IntCreateMenu(&Handle, !PopupMenu, Desktop, GetW32ProcessInfo());
if (Menu && Menu->head.rpdesk->rpwinstaParent != WinStaObject)
ERR("Desktop Window Station does not match Process one!\n");
ObDereferenceObject(WinStaObject);
}
else /* CSRSS 走捷径 */
{
Menu = IntCreateMenu(&Handle, !PopupMenu, GetW32ThreadInfo()->rpdesk, GetW32ProcessInfo());
}
if (Menu) UserDereferenceObject(Menu);
return (HMENU)Handle;
}
- 常规进程创建菜单前要校验其 WindowStation(防止跨窗口站操作),CSRSS 免检。
- 入口是
NtUserCallNoParam(simplecall.c L68-74):
c
case NOPARAM_ROUTINE_CREATEMENU:
Result = (DWORD_PTR)UserCreateMenu(GetW32ThreadInfo()->rpdesk, FALSE);
break;
case NOPARAM_ROUTINE_CREATEMENUPOPUP:
Result = (DWORD_PTR)UserCreateMenu(GetW32ThreadInfo()->rpdesk, TRUE);
break;
- user32 的
CreateMenu/CreatePopupMenu(user32/windows/menu.c L829-841)只是NtUserxCreateMenu()/NtUserxCreatePopupMenu()(ntwrapper.h L551-559)的薄包装。
4.3 克隆:IntCloneMenu / IntCloneMenuItems(L770-860)
系统菜单构建需要"模板 → 每窗口一份副本",IntCloneMenu 负责深拷贝:
c
PMENU FASTCALL IntCloneMenu(PMENU Source)
{
Menu = (PMENU)UserCreateObject( gHandleTable, Source->head.rpdesk,
((PPROCESSINFO)Source->head.hTaskWow)->ptiList,
&hMenu, TYPE_MENU, sizeof(MENU));
Menu->fFlags = Source->fFlags;
Menu->cyMax = Source->cyMax; Menu->hbrBack = Source->hbrBack;
Menu->dwContextHelpId = Source->dwContextHelpId;
Menu->dwMenuData = Source->dwMenuData;
Menu->iItem = NO_SELECTED_ITEM; /* 克隆不保留选中态 */
Menu->spwndNotify = NULL; Menu->hWnd = NULL;
...(尺寸清 0、项数清 0)
IntCloneMenuItems(Menu, Source); /* 拷贝项数组 */
return Menu;
}
IntCloneMenuItems(L770-818)逐项拷贝 fType/fState/wID/spSubMenu/hbmpChecked/hbmpUnchecked/dwItemData,文本深拷贝 (DesktopHeapAlloc + RtlCopyUnicodeString,L799-807)并同步 Xlpstr。注意 spSubMenu 是浅拷贝(L791)------克隆的菜单项仍指向同一批子菜单对象,这符合"系统菜单模板共享 SC_* 子菜单"的语义。
4.4 销毁:IntDestroyMenuObject(L317-356)
c
BOOL FASTCALL IntDestroyMenuObject(PMENU Menu, BOOL bRecurse)
{
if (PsGetCurrentProcessSessionId() == Menu->head.rpdesk->rpwinstaParent->dwSessionId)
{
if (Menu->hWnd) /* 还挂在窗口上 */
{
Window = ValidateHwndNoErr(Menu->hWnd);
if (Window)
{
/* DestroyMenu 不应销毁系统菜单弹出窗口的拥有者 */
if ((Menu->fFlags & (MNF_POPUP | MNF_SYSSUBMENU)) == MNF_POPUP)
{
ERR("FIXME Pop up menu window thing'ie\n");
//co_UserDestroyWindow( Window ); ← 注释掉的危险路径
}
}
}
if (!UserMarkObjectDestroy(Menu)) return TRUE; /* 防重入 */
IntDestroyMenu( Menu, bRecurse); /* 递归清理项 */
ret = UserDeleteObject(UserHMGetHandle(Menu), TYPE_MENU);
return ret;
}
return FALSE; /* 会话不匹配:拒绝跨会话销毁 */
}
- 会话校验:只有菜单所在桌面属于当前会话才允许销毁,防止跨会话句柄滥用。
UserMarkObjectDestroy把对象标记为"正在销毁",防重入(VerifyMenu也会检查UserObjectInDestroy,L229)。UserDestroyMenuObject(L311-314)是对象管理器回调,转发到IntDestroyMenuObject(Object, TRUE)。
IntDestroyMenu(L269-307)递归清理项数组:
c
for (i = pMenu->cItems; i > 0; i--, item++)
{
SubMenu = item->spSubMenu; item->spSubMenu = NULL;
FreeMenuText(pMenu,item); /* 释放文本(桌面堆) */
if (item->hbmp && !(item->fState & MFS_HBMMENUBMP))
{
GreSetObjectOwner(item->hbmp, GDI_OBJ_HMGR_POWNED); /* 位图还给进程 */
item->hbmp = NULL;
}
if (bRecurse && SubMenu)
{
IntReleaseMenuObject(SubMenu); /* 释放插入时的引用 */
IntDestroyMenuObject(SubMenu, bRecurse);/* 递归销毁子菜单 */
}
}
DesktopHeapFree(pMenu->head.rpdesk, pMenu->rgItems); /* 释放项数组 */
FreeMenuText 宏(L183-189):仅当项是 MF_STRING 且 lpstr.Length 非零时 DesktopHeapFree 文本缓冲区。
4.5 销毁入口
NtUserDestroyMenu(L5842-5866):校验句柄 + 桌面匹配(Menu->head.rpdesk != gptiCurrent->rpdesk则 ERROR_ACCESS_DENIED),调IntDestroyMenuObject(Menu, TRUE)。UserDestroyMenu(L5820-5836):内部快捷版,同样校验桌面,但bRecurse=FALSE(由调用者决定是否递归------MENU_GetSystemMenu出错清理时用)。- 进程退出时的清理:
IntCleanupMenus(menu.h 声明 L147-149)由对象管理器在进程句柄表清理时调用,遍历该进程的菜单句柄强制销毁。
5. 核心函数逐一展开
5.1 对象取用与判定
IntGetMenuObject(L191-199)
c
PMENU FASTCALL IntGetMenuObject(HMENU hMenu)
{
PMENU Menu = UserGetMenuObject(hMenu);
if (Menu) Menu->head.cLockObj++;
return Menu;
}
UserGetMenuObject 是句柄表查询(object.c,ValidateHandle 系),IntGetMenuObject 在其上额外加锁 (cLockObj++)。加锁的语义:调用方持有临时引用,使用完毕必须 IntReleaseMenuObject(menu.h L130-131 定义为 UserDereferenceObject)。不加锁直接用 UserGetMenuObject 的场合(如 IntIsMenu)只是"查一下存不存在"。
VerifyMenu(L201-236)
对 PMENU 指针做健壮性验证:SEH 包裹读 rgItems/wID 防访问违例("Run away LOOP!"),并检查 UserObjectInDestroy(hMenu)------正在销毁的菜单视作无效 。返回前恢复 EngGetLastError(不让验证过程污染错误码)。
IntIsMenu(L238-244)
c
BOOL FASTCALL IntIsMenu(HMENU Menu)
{
if (UserGetMenuObject(Menu)) return TRUE;
return FALSE;
}
判断句柄是不是有效菜单对象(等价于 IsMenu API)。menu.h L125 声明为 FASTCALL,被内核多处(如 IntGetSubMenu 路径)使用。
IntGetMenu(L247-256)与 get_win_sys_menu(L258-267)
c
PMENU WINAPI IntGetMenu(HWND hWnd)
{
PWND Wnd = ValidateHwndNoErr(hWnd);
if (!Wnd) return NULL;
return UserGetMenuObject(UlongToHandle(Wnd->IDMenu)); /* 窗口的菜单栏 */
}
PMENU get_win_sys_menu( HWND hwnd )
{
PWND win = ValidateHwndNoErr( hwnd );
if (win) ret = UserGetMenuObject(win->SystemMenu); /* 窗口的系统菜单 */
return ret;
}
IntGetMenu 被 MENU_TrackMouseMenuBar(L4488)、MENU_DoNextMenu(L3794/L3832)等菜单循环代码使用;get_win_sys_menu 取 Wnd->SystemMenu 字段。用户态 GetMenu(user32 menu.c L867-875)则是直接读 Wnd->IDMenu 返回句柄------不经过系统调用。
5.2 查找:MENU_FindItem(L507-554)
查找是菜单子系统的"心脏",几乎所有查询/修改 API 的第一步:
c
PITEM FASTCALL MENU_FindItem( PMENU *pmenu, UINT *nPos, UINT wFlags )
{
MENU *menu = *pmenu;
ITEM *fallback = NULL;
UINT fallback_pos = 0, i;
if (wFlags & MF_BYPOSITION)
{
if (!menu->cItems) return NULL;
if (*nPos >= menu->cItems) return NULL;
return &menu->rgItems[*nPos]; /* 位置查找:O(1) */
}
else /* MF_BYCOMMAND */
{
PITEM item = menu->rgItems;
for (i = 0; i < menu->cItems; i++, item++)
{
if (item->spSubMenu)
{
/* 先递归进子菜单找 */
PMENU psubmenu = item->spSubMenu;
PITEM subitem = MENU_FindItem( &psubmenu, nPos, wFlags );
if (subitem) { *pmenu = psubmenu; return subitem; }
else if (item->wID == *nPos)
{ /* 子菜单里没找到,本项是"后备" */
fallback_pos = i; fallback = item;
}
}
else if (item->wID == *nPos) /* 普通项直接命中 */
{ *nPos = i; return item; }
}
}
if (fallback) *nPos = fallback_pos;
return fallback; /* 可能返回"子菜单项本身"作为后备 */
}
三个要点:
- MF_BYPOSITION 直接索引
rgItems[*nPos],越界返回 NULL。 - MF_BYCOMMAND 是深度优先递归 :先深入子菜单树找 wID;若子菜单树没有而父级项本身 wID 匹配,把该弹出项 作为后备返回。
*pmenu可能被改写为子菜单指针------调用方据此知道"找到了子菜单里的项"。 - 这正是
GetMenuItemID(hMenu, nPos)与MENU_FindItem在 user32 侧的同名实现(user32 menu.c L83)共用的语义,保证命令查找能穿透子菜单层级。
MENU_FindItem 的调用方几乎覆盖所有"按 ID/位置操作项"的函数:IntGetMenuState、IntGetSubMenu、IntRemoveMenuItem、IntMenuItemInfo、IntEnableMenuItem、IntCheckMenuItem、IntGetMenuItemRect、NtUserMenuItemFromPoint 等。
相关:MENU_FindSubMenu(L563-592)
按子菜单对象指针 (而非 wID)查找子菜单所在位置,同样深度优先;找不到返回 NO_SELECTED_ITEM。被 IntFindSubMenu(L5041-5055)包装成"按句柄找位置"的 API:
c
UINT FASTCALL IntFindSubMenu(HMENU *hMenu, HMENU hSubTarget )
{
if (((*hMenu)==(HMENU)0xffff) || (!(menu = UserGetMenuObject(*hMenu))))
return NO_SELECTED_ITEM;
pSubTarget = UserGetMenuObject(hSubTarget);
Pos = MENU_FindSubMenu(&menu, pSubTarget);
*hMenu = (menu ? UserHMGetHandle(menu) : NULL); /* 回传实际所在菜单 */
return Pos;
}
IntFindSubMenu 在 menu.h L156 声明,被 mdi.c 等外部模块用来定位"给定子菜单在哪个父菜单的哪个位置"。
遗留声明:IntGetMenuItemByFlag
menu.h L142-145 仍声明 IntGetMenuItemByFlag(PMENU, UINT, UINT, PMENU*, PITEM*, PITEM*),但当前 menu.c 已不再实现它 ------其职责完全被 MENU_FindItem 取代(旧签名还带 PrevMenuItem 输出,是 Wine 早期 API)。这是历史遗留声明,勿误以为内核存在该函数。
5.3 插入:IntInsertMenuItem(L690-725)
c
BOOL FASTCALL IntInsertMenuItem(PMENU MenuObject, UINT uItem, BOOL fByPosition,
PROSMENUITEMINFO ItemInfo, PUNICODE_STRING lpstr)
{
if (MAX_MENU_ITEMS <= MenuObject->cItems) /* 0x4000 上限 */
{ EngSetLastError(ERROR_NOT_ENOUGH_MEMORY); return FALSE; }
SubMenu = MenuObject;
if(!(MenuItem = MENU_InsertItem( SubMenu, uItem,
fByPosition ? MF_BYPOSITION : MF_BYCOMMAND, &SubMenu, &uItem )))
return FALSE;
if(!IntSetMenuItemInfo(SubMenu, MenuItem, ItemInfo, lpstr))
{
/* 填充失败:回滚刚插入的项 */
IntRemoveMenuItem(SubMenu, uItem, fByPosition ? MF_BYPOSITION : MF_BYCOMMAND, FALSE);
return FALSE;
}
SubMenu->cyMenu = 0; /* 强制尺寸重算 */
MenuItem->hbmpChecked = MenuItem->hbmpUnchecked = 0;
return TRUE;
}
流程:容量检查 → MENU_InsertItem 在数组里腾位置 → IntSetMenuItemInfo 填充字段 → 失败则 IntRemoveMenuItem 回滚 → 清 cyMenu 触发尺寸重算。
MENU_InsertItem(L638-687)实现数组插入:
c
ITEM *MENU_InsertItem( PMENU menu, UINT pos, UINT flags, PMENU *submenu, UINT *npos )
{
if (flags & MF_BYPOSITION) {
if (pos > menu->cItems) pos = menu->cItems; /* 越界钳制到末尾 */
} else {
if (!MENU_FindItem( &menu, &pos, flags )) /* 命令查找失败则追加 */
{ ... pos = menu->cItems; }
}
/* MDI 系统按钮保持右侧:插入点前若有"神奇位图"(HBMMENU_SYSTEM..HBMMENU_MBAR_CLOSE_D)
则把插入点继续前移,让这些按钮始终在右端 */
while ( pos > 0 &&
(INT_PTR)menu->rgItems[pos - 1].hbmp >= (INT_PTR)HBMMENU_SYSTEM &&
(INT_PTR)menu->rgItems[pos - 1].hbmp <= (INT_PTR)HBMMENU_MBAR_CLOSE_D)
pos--;
newItems = DesktopHeapAlloc(menu->head.rpdesk, sizeof(ITEM) * (menu->cItems+1) );
if (menu->cItems > 0)
{
if (pos > 0) RtlCopyMemory( newItems, menu->rgItems, pos * sizeof(ITEM) );
if (pos < menu->cItems) RtlCopyMemory( &newItems[pos+1], &menu->rgItems[pos],
(menu->cItems-pos)*sizeof(ITEM) );
DesktopHeapFree(menu->head.rpdesk, menu->rgItems );
}
menu->rgItems = newItems;
menu->cItems++;
RtlZeroMemory( &newItems[pos], sizeof(*newItems) );
menu->cyMenu = 0; /* 强制尺寸重算 */
return &newItems[pos];
}
要点:
- 数组重分配策略 :每次插入分配
cItems+1个 ITEM 的新数组、前后两段 memcpy、释放旧数组------插入是 O(n) 的,换来查找 O(1) 与缓存友好。 - MDI 魔法位图钳制:若插入点之前紧邻的是 MDI 系统按钮位图(HBMMENU_SYSTEM=1 到 HBMMENU_MBAR_CLOSE_D 之间的"神奇位图"),插入点持续前移,保证 MDI 最小化/还原/关闭按钮永远在菜单栏右端。注释说明 XP 只把位图句柄 1-6 视为神奇位图(L656-659)。
- 新项
RtlZeroMemory清零,cyMenu=0标记尺寸脏。
5.4 填充:IntSetMenuItemInfo(L1032-1213)
这是"把 MENUITEMINFOW 的字段写进 ITEM"的核心,按 fMask 位逐个处理:
c
if (lpmii->fMask & MIIM_FTYPE)
{
MenuItem->fType &= ~MENUITEMINFO_TYPE_MASK; /* 清类型位 */
MenuItem->fType |= lpmii->fType & MENUITEMINFO_TYPE_MASK;
}
if (lpmii->fMask & MIIM_TYPE)
{
if (MenuItem->fType != lpmii->fType &&
MENU_ITEM_TYPE(MenuItem->fType) == MFT_STRING) /* 类型从字符串变走 */
{
FreeMenuText(MenuObject,MenuItem); /* 释放旧文本 */
RtlInitUnicodeString(&MenuItem->lpstr, NULL);
MenuItem->Xlpstr = NULL;
}
if (lpmii->fType & MFT_BITMAP)
{
if (lpmii->hbmpItem) MenuItem->hbmp = lpmii->hbmpItem;
else MenuItem->hbmp = (HBITMAP)((ULONG_PTR)(LOWORD(lpmii->dwTypeData))); /* Win9x 老用法 */
lpmii->dwTypeData = 0;
}
}
if (lpmii->fMask & MIIM_BITMAP)
{
MenuItem->hbmp = lpmii->hbmpItem;
if (MenuItem->hbmp <= HBMMENU_POPUP_MINIMIZE && MenuItem->hbmp >= HBMMENU_CALLBACK)
MenuItem->fState |= MFS_HBMMENUBMP; /* 神奇位图标记 */
else
MenuItem->fState &= ~MFS_HBMMENUBMP;
}
if (lpmii->fMask & MIIM_CHECKMARKS)
{ MenuItem->hbmpChecked = lpmii->hbmpChecked; MenuItem->hbmpUnchecked = lpmii->hbmpUnchecked; }
if (lpmii->fMask & MIIM_DATA) MenuItem->dwItemData = lpmii->dwItemData;
if (lpmii->fMask & MIIM_ID) MenuItem->wID = lpmii->wID;
if (lpmii->fMask & MIIM_STATE)
{
if (lpmii->fState & MFS_DEFAULT)
UserSetMenuDefaultItem(MenuObject, -1, 0); /* 先清掉其他项的默认位 */
UpdateMenuItemState(MenuItem->fState, lpmii->fState);
}
子菜单链接(L1112-1157):
c
if (lpmii->fMask & MIIM_SUBMENU)
{
if (lpmii->hSubMenu)
{
SubMenuObject = UserGetMenuObject(lpmii->hSubMenu);
if ( SubMenuObject && !(UserObjectInDestroy(lpmii->hSubMenu)) )
{
if (MenuObject == SubMenuObject) /* 自环:CORE-7967 防死循环 */
{
SubMenuObject = IntCreateMenu(&hMenu, FALSE, MenuObject->head.rpdesk,
(PPROCESSINFO)MenuObject->head.hTaskWow);
if (!SubMenuObject) return FALSE;
IntReleaseMenuObject(SubMenuObject);
circref = TRUE;
}
if ( MENU_depth( SubMenuObject, 0) > MAXMENUDEPTH ) /* 深度/环路检测 */
{
ERR("Loop detected in menu hierarchy or maximum menu depth exceeded!\n");
if (circref) IntDestroyMenuObject(SubMenuObject, FALSE);
return FALSE;
}
SubMenuObject->fFlags |= MNF_POPUP; /* 子菜单必须标记为弹出菜单 */
if (MenuItem->spSubMenu) IntReleaseMenuObject(MenuItem->spSubMenu);
MenuItem->spSubMenu = SubMenuObject;
UserReferenceObject(SubMenuObject); /* 插入即持有引用 */
}
else { EngSetLastError(ERROR_INVALID_PARAMETER); return FALSE; }
}
else /* 清空子菜单 */
{
if (MenuItem->spSubMenu) IntReleaseMenuObject(MenuItem->spSubMenu);
MenuItem->spSubMenu = NULL;
}
}
文本设置(L1159-1191):
c
if ((lpmii->fMask & MIIM_STRING) ||
((lpmii->fMask & MIIM_TYPE) && (MENU_ITEM_TYPE(lpmii->fType) == MF_STRING)))
{
FreeMenuText(MenuObject,MenuItem);
RtlInitUnicodeString(&MenuItem->lpstr, NULL);
MenuItem->Xlpstr = NULL;
if (lpmii->dwTypeData && lpmii->cch && lpstr && lpstr->Buffer)
{
if (!NT_VERIFY(lpmii->cch <= UNICODE_STRING_MAX_CHARS)) return FALSE;
Source.Length = Source.MaximumLength = (USHORT)(lpmii->cch * sizeof(WCHAR));
Source.Buffer = lpmii->dwTypeData;
MenuItem->lpstr.Buffer = DesktopHeapAlloc( MenuObject->head.rpdesk,
Source.Length + sizeof(WCHAR));
if (MenuItem->lpstr.Buffer != NULL)
{
RtlCopyUnicodeString(&MenuItem->lpstr, &Source);
MenuItem->lpstr.Buffer[MenuItem->lpstr.Length / sizeof(WCHAR)] = 0; /* 补 NUL */
MenuItem->cch = MenuItem->lpstr.Length / sizeof(WCHAR);
MenuItem->Xlpstr = (USHORT*)MenuItem->lpstr.Buffer;
}
}
}
自动分隔符判定(L1193-1198):
c
if ( !(MenuObject->fFlags & MNF_SYSMENU) && /* 系统菜单例外 */
!MenuItem->Xlpstr && !lpmii->dwTypeData &&
!(MenuItem->fType & MFT_OWNERDRAW) && !MenuItem->hbmp)
MenuItem->fType |= MFT_SEPARATOR; /* 无文本无位图非自绘 → 变分隔符 */
即:普通菜单里一个既没文本也没位图、非自绘的项自动成为分隔符;系统菜单不适用此规则(系统菜单项用"魔法位图"表达,见第 7 章)。
扩展字段回填(L1200-1210):sizeof(ROSMENUITEMINFO) == cbSize 时写入 xItem/yItem/cxItem/cyItem/dxTab/cxBmp/cyBmp,并把 lpmii->lpstr 回传新分配的文本指针(供 user32 同步)。
5.5 移除:IntRemoveMenuItem(L594-631)
c
BOOL FASTCALL IntRemoveMenuItem( PMENU pMenu, UINT nPos, UINT wFlags, BOOL bRecurse )
{
if (!(item = MENU_FindItem( &pMenu, &nPos, wFlags ))) return FALSE;
FreeMenuText(pMenu,item); /* 释放文本 */
if (bRecurse && item->spSubMenu) /* bRecurse 时销毁子菜单 */
IntDestroyMenuObject(item->spSubMenu, bRecurse);
if (--pMenu->cItems == 0)
{
DesktopHeapFree(pMenu->head.rpdesk, pMenu->rgItems ); /* 空菜单释放数组 */
pMenu->rgItems = NULL;
}
else
{
while (nPos < pMenu->cItems) /* 左移覆盖被删项 */
{ *item = *(item+1); item++; nPos++; }
newItems = DesktopHeapReAlloc(pMenu->head.rpdesk, pMenu->rgItems,
pMenu->cItems * sizeof(ITEM));
if (newItems) pMenu->rgItems = newItems; /* 收缩数组(失败则保留原块) */
}
return TRUE;
}
- 支持
MF_BYPOSITION/MF_BYCOMMAND(经 MENU_FindItem)。 - bRecurse 语义 :
NtUserDeleteMenu(L5635-5655)传 TRUE(连带销毁子菜单对象),NtUserRemoveMenu(L6397-6417)传 FALSE(只摘除项,子菜单句柄仍有效,可再挂到别处)------这正是 Windows 里 DeleteMenu 与 RemoveMenu 的经典区别。 - 数组收缩用
DesktopHeapReAlloc,失败时保留旧块(项已被左移覆盖,语义正确,只是内存不回收)。
5.6 信息查询/设置:IntMenuItemInfo 与 User 侧包装
IntMenuItemInfo(L5102-5127)
c
BOOL FASTCALL IntMenuItemInfo(PMENU Menu, UINT Item, BOOL ByPosition,
PROSMENUITEMINFO ItemInfo, BOOL SetOrGet,
PUNICODE_STRING lpstr)
{
if (!(MenuItem = MENU_FindItem( &Menu, &Item,
(ByPosition ? MF_BYPOSITION : MF_BYCOMMAND) )))
{ EngSetLastError(ERROR_MENU_ITEM_NOT_FOUND); return FALSE; }
if (SetOrGet) Ret = IntSetMenuItemInfo(Menu, MenuItem, ItemInfo, lpstr);
else Ret = IntGetMenuItemInfo(Menu, MenuItem, ItemInfo);
return Ret;
}
一个入口,SetOrGet 分流到 IntSet/IntGetMenuItemInfo。MENU_GetSystemMenu 用它给 SC_CLOSE 等项设置 MIIM_BITMAP 魔法位图(L5358-5367)。
IntGetMenuItemInfo(L960-1029)
按 fMask 回填:MIIM_FTYPE|MIIM_TYPE→fType;MIIM_BITMAP→hbmpItem;MIIM_CHECKMARKS→两个位图;MIIM_DATA→dwItemData;MIIM_ID→wID;MIIM_STATE→fState;MIIM_SUBMENU→hSubMenu(经 UserHMGetHandle)。
文本回填(L996-1014)有三态:
dwTypeData == NULL:只回填cch(调用方先用 NULL 问长度);dwTypeData != NULL:MmCopyToCaller拷min(cch*2, lpstr.MaximumLength)字节到用户缓冲区;- 扩展结构:
Rect/dxTab/lpstr/maxBmpSize一并回填(L1016-1026)。
UserMenuItemInfo(L5129-5201):安全边界
内核面向用户的通用入口,负责结构合法性 与跨模式拷贝:
c
Status = MmCopyFromCaller(&Size, &UnsafeItemInfo->cbSize, sizeof(UINT));
if ( Size != sizeof(MENUITEMINFOW) &&
Size != FIELD_OFFSET(MENUITEMINFOW, hbmpItem) &&
Size != sizeof(ROSMENUITEMINFO) )
{ EngSetLastError(ERROR_INVALID_PARAMETER); return FALSE; }
Status = MmCopyFromCaller(&ItemInfo, UnsafeItemInfo, Size);
if (FIELD_OFFSET(MENUITEMINFOW, hbmpItem) == Size && 0 != (ItemInfo.fMask & MIIM_BITMAP))
{ EngSetLastError(ERROR_INVALID_PARAMETER); return FALSE; } /* 老结构禁位图 */
...
if (!(MenuItem = MENU_FindItem( &Menu, &Item, ... )))
{
/* workaround for Word 95: pretend that SC_TASKLIST item exists. */
if ( SetOrGet && Item == SC_TASKLIST && !ByPosition ) return TRUE;
EngSetLastError(ERROR_MENU_ITEM_NOT_FOUND);
return FALSE;
}
if (SetOrGet) Ret = IntSetMenuItemInfo(Menu, MenuItem, &ItemInfo, lpstr);
else { Ret = IntGetMenuItemInfo(...); if (Ret) MmCopyToCaller(UnsafeItemInfo, &ItemInfo, Size); }
MmCopyFromCaller/MmCopyToCaller做带 SEH 的用户↔内核拷贝。- Word 95 兼容:请求 SC_TASKLIST 项时假装存在(该应用在 Windows 上同样依赖此容错)。
- Get 路径最后把(可能被扩展字段填充的)结构整体拷回用户。
UserInsertMenuItem(L4960-4999)
Insert 专用包装:先尝试拷完整 sizeof(MENUITEMINFOW),再尝试拷 FIELD_OFFSET(MENUITEMINFOW, hbmpItem)(老结构,hbmpItem 置 0 补齐),两种尺寸都要与 cbSize 一致,然后调 IntInsertMenuItem。
通用 thunk:NtUserThunkedMenuItemInfo(L6567-6623)
所有"改菜单项"类调用的内核总入口:
c
BOOL APIENTRY
NtUserThunkedMenuItemInfo(HMENU hMenu, UINT uItem, BOOL fByPosition,
BOOL bInsert, LPMENUITEMINFOW lpmii,
PUNICODE_STRING lpszCaption)
{
if (!(Menu = UserGetMenuObject(hMenu))) goto Cleanup;
if (lpszCaption && lpszCaption->Buffer)
{
Status = ProbeAndCaptureUnicodeString(&lstrCaption, UserMode, lpszCaption);
if (!NT_SUCCESS(Status)) { SetLastNtError(Status); goto Cleanup; }
}
if (bInsert)
Ret = UserInsertMenuItem(Menu, uItem, fByPosition, lpmii, &lstrCaption);
else
Ret = UserMenuItemInfo(Menu, uItem, fByPosition, (PROSMENUITEMINFO)lpmii, TRUE, &lstrCaption);
Cleanup:
UserLeave();
if (lstrCaption.Buffer) ReleaseCapturedUnicodeString(&lstrCaption, UserMode);
return Ret;
}
bInsert=TRUE走插入(AppendMenu/InsertMenu/InsertMenuItem),bInsert=FALSE走修改(SetMenuItemInfo/ModifyMenu)。- 菜单项文本(lpszCaption)经
ProbeAndCaptureUnicodeString拷入内核,用完ReleaseCapturedUnicodeString释放------内核与用户态各持一份,IntSetMenuItemInfo再深拷贝到桌面堆。注意这个参数是 ReactOS 扩展(标准 Windows 的 NtUserThunkedMenuItemInfo 无此参数),是为了省去内核二次访问用户态文本缓冲区。 - 菜单样式通用入口
NtUserThunkedMenuInfo(L6542-6561)同理:UserMenuInfo(Menu, (PROSMENUINFO)lpcmi, TRUE)。
UserMenuInfo(L5203-5253):菜单级信息的 thunk 包装
与 UserMenuItemInfo 对称:MmCopyFromCaller 拷入、校验 sizeof(MENUINFO) <= Size <= sizeof(ROSMENUINFO)、Set/Get 分流到 IntSetMenuInfo/IntGetMenuInfo、结果 MmCopyToCaller 拷回。Get/Set 期间不持有菜单锁(由外层 UserEnterExclusive 保护)。
IntGetMenuInfo / IntSetMenuInfo(L877-958)
按 fMask 逐位搬运 MENUINFO 字段:
c
BOOL FASTCALL IntGetMenuInfo(PMENU Menu, PROSMENUINFO lpmi)
{
if (lpmi->fMask & MIM_BACKGROUND) lpmi->hbrBack = Menu->hbrBack;
if (lpmi->fMask & MIM_HELPID) lpmi->dwContextHelpID = Menu->dwContextHelpId;
if (lpmi->fMask & MIM_MAXHEIGHT) lpmi->cyMax = Menu->cyMax;
if (lpmi->fMask & MIM_MENUDATA) lpmi->dwMenuData = Menu->dwMenuData;
if (lpmi->fMask & MIM_STYLE) lpmi->dwStyle = Menu->fFlags & MNS_STYLE_MASK;
if (sizeof(MENUINFO) < lpmi->cbSize) /* ROSMENUINFO 扩展字段 */
{
lpmi->cItems = Menu->cItems; lpmi->iItem = Menu->iItem;
lpmi->cxMenu = Menu->cxMenu; lpmi->cyMenu = Menu->cyMenu;
lpmi->spwndNotify = Menu->spwndNotify; lpmi->cxTextAlign = Menu->cxTextAlign;
lpmi->iTop = Menu->iTop; lpmi->iMaxTop = Menu->iMaxTop;
lpmi->dwArrowsOn = Menu->dwArrowsOn;
lpmi->fFlags = Menu->fFlags; lpmi->Self = UserHMGetHandle(Menu);
lpmi->TimeToHide = Menu->TimeToHide; lpmi->Wnd = Menu->hWnd;
}
return TRUE;
}
IntSetMenuInfo(L913-958)除反向赋值外还有两点:
- MIM_APPLYTOSUBMENUS(L925-936):递归对每个子菜单应用同样的设置。
- MIM_STYLE 的合成方式
Menu->fFlags ^= (Menu->fFlags ^ lpmi->dwStyle) & MNS_STYLE_MASK(L924):只替换 MNS_* 位,不动 MNF_* 位。设置后对未实现的样式打 FIXME 日志:MNS_AUTODISMISS/MNS_DRAGDROP/MNS_MODELESS(L951-956)------ReactOS 暂未实现这三项,遇到时仅告警不处理。
5.7 挂窗口:IntSetMenu(L5484-5553)与 NtUserSetMenu(L6422-6460)
c
BOOL FASTCALL IntSetMenu(PWND Wnd, HMENU Menu, BOOL *Changed)
{
if ((Wnd->style & (WS_CHILD | WS_POPUP)) == WS_CHILD)
{ /* 子窗口不能有菜单栏 */
EngSetLastError(ERROR_INVALID_WINDOW_HANDLE);
return FALSE;
}
*Changed = (UlongToHandle(Wnd->IDMenu) != Menu);
if (!*Changed) return TRUE; /* 相同菜单:无操作 */
if (Wnd->IDMenu)
{
OldMenu = IntGetMenuObject(UlongToHandle(Wnd->IDMenu));
ASSERT(NULL == OldMenu || OldMenu->hWnd == UserHMGetHandle(Wnd));
}
else OldMenu = NULL;
if (NULL != Menu)
{
NewMenu = IntGetMenuObject(Menu);
if (NULL == NewMenu) { ...; EngSetLastError(ERROR_INVALID_MENU_HANDLE); return FALSE; }
if (NULL != NewMenu->hWnd)
{ /* 一个菜单不能同时挂两个窗口 */
...; EngSetLastError(ERROR_INVALID_MENU_HANDLE); return FALSE;
}
}
Wnd->IDMenu = (UINT_PTR) Menu; /* 挂接 */
if (NULL != NewMenu) { NewMenu->hWnd = UserHMGetHandle(Wnd); IntReleaseMenuObject(NewMenu); }
if (NULL != OldMenu) { OldMenu->hWnd = NULL; /* 旧菜单解除占用 */ IntReleaseMenuObject(OldMenu); }
return TRUE;
}
四条规则:
- 子窗口禁挂菜单栏(WS_CHILD 且非 WS_POPUP)。
- 同句柄幂等 :
Changed为 FALSE 直接成功返回。 - 互斥占用 :
NewMenu->hWnd非空表示已被别的窗口占用,SetMenu 失败(ERROR_INVALID_MENU_HANDLE)。hWnd在这里充当"占用标记 + 反向引用"。 - 所有权转移 :旧菜单
hWnd清空(解除占用),调用方稍后销毁旧菜单不会被"仍挂窗口"状态干扰。
NtUserSetMenu(L6422-6460)在其上补了两件事:
c
if (!IntSetMenu(Window, Menu, &Changed)) goto Exit;
/* 未最小化且需要重绘(Repaint 或 Changed)时刷新窗口框架 */
if (!(Window->style & WS_MINIMIZE) && (Repaint || Changed))
{
co_WinPosSetWindowPos(Window, 0,0,0,0,0,
SWP_NOSIZE|SWP_NOMOVE|SWP_NOACTIVATE|SWP_NOZORDER|SWP_FRAMECHANGED);
}
SWP_FRAMECHANGED 使 winpos 重算非客户区并触发 WM_NCPAINT------菜单栏随之重绘。用户态 SetMenu(user32 menu.c L1489-1493)传 Repaint=TRUE。菜单栏高度变化还通过 NtUserxSetMenuBarHeight(ntwrapper.h L816-819,TWOPARAM_ROUTINE_SETMENUBARHEIGHT)同步给客户区布局。
5.8 菜单栏信息:NtUserGetMenuBarInfo(L5927-6082)
GetMenuBarInfo 的三种对象类型:
c
switch (idObject)
{
case OBJID_CLIENT: /* 弹出菜单窗口本身(FNID_MENU) */
if (pWnd->pcls->fnid != FNID_MENU) { ...; goto Cleanup; }
hMenu = (HMENU)co_IntSendMessage(hwnd, MN_GETHMENU, 0, 0); /* 向菜单窗口要句柄 */
pPopupMenu = ((PMENUWND)pWnd)->ppopupmenu;
if (pPopupMenu && pPopupMenu->spmenu && UserHMGetHandle(pPopupMenu->spmenu) != hMenu)
ERR("Window Pop Up hMenu %p not the same as Get hMenu %p!\n", ...);
break;
case OBJID_MENU: /* 菜单栏 */
if (pWnd->style & WS_CHILD) goto Cleanup;
hMenu = UlongToHandle(pWnd->IDMenu);
break;
case OBJID_SYSMENU: /* 系统菜单 */
if (!(pWnd->style & WS_SYSMENU)) goto Cleanup;
Menu = IntGetSystemMenu(pWnd, FALSE);
hMenu = UserHMGetHandle(Menu);
break;
}
然后校验 pmbi->cbSize == sizeof(MENUBARINFO)、idItem 范围;idItem==0 取整个菜单栏矩形(IntGetMenuItemRect(..., 0) 后按 cxMenu/cyMenu 扩边),否则取 idItem-1 对应项的矩形。回填:
c
kmbi.hMenu = hMenu;
kmbi.fBarFocused = top_popup_hmenu == hMenu; /* 是否正在跟踪 */
if (idItem)
{
kmbi.fFocused = Menu->iItem == idItem-1; /* 该项是否焦点 */
if (kmbi.fFocused && Menu->rgItems[idItem-1].spSubMenu)
kmbi.hwndMenu = Menu->rgItems[idItem-1].spSubMenu->hWnd; /* 已展开子菜单的窗口 */
}
else kmbi.fFocused = kmbi.fBarFocused;
5.9 查询类 API:GetMenuState / GetMenuString / GetSubMenu / GetMenuItemCount
这些 API 在 ReactOS 中不需要进内核------user32 对共享桌面堆有只读映射,直接遍历 ITEM 数组:
user32 GetMenuState(menu.c L1036-1060)
c
UINT WINAPI GetMenuState(HMENU hMenu, UINT uId, UINT uFlags)
{
if (!(pItem = MENU_FindItem( &hMenu, &uId, uFlags ))) return -1;
if (!pItem->Xlpstr && pItem->hbmp) Type = MFT_BITMAP;
if (pItem->spSubMenu)
{
PMENU pSubMenu = DesktopPtrToUser(pItem->spSubMenu);
HMENU hsubmenu = UserHMGetHandle(pSubMenu);
Type |= MF_POPUP; /* Fix CORE-9269 */
if (!IsMenu(hsubmenu)) return (UINT)-1;
else return (pSubMenu->cItems << 8) | ((pItem->fState|pItem->fType|Type) & 0xff);
}
else return (pItem->fType | pItem->fState | Type);
}
返回值编码与 Windows 一致:弹出项 的高字节是子菜单项数(cItems << 8),低字节是 fState|fType|MF_POPUP;普通项直接返回 fType|fState。DesktopPtrToUser 把桌面堆指针换算成用户态可读的共享映射地址。内核侧对应的 IntGetMenuState(menu.c L5001-5019)逻辑完全相同,供 win32k 内部(如菜单循环里查状态)使用。
user32 GetMenuStringW(L1100-1133)
c
if (!(item = MENU_FindItem( &hMenu, &uIDItem, uFlag ))) { SetLastError(ERROR_MENU_ITEM_NOT_FOUND); return 0; }
text = item->Xlpstr ? DesktopPtrToUser(item->Xlpstr) : NULL;
if (!lpString || !nMaxCount) return text ? strlenW(text) : 0; /* 只问长度 */
lstrcpynW( lpString, text, nMaxCount );
return strlenW(lpString);
GetMenuStringA(L1065-1095)多做一层 WideCharToMultiByte(CP_ACP) 转换。文本源统一是 item->Xlpstr(与内核 IntSetMenuItemInfo 写入的 Buffer 同一份内存)。
user32 GetSubMenu(L1138-1154)
c
if (!(pItem = MENU_FindItem( &hMenu, (UINT*)&nPos, MF_BYPOSITION ))) return NULL;
if (pItem->spSubMenu)
{
PMENU pSubMenu = DesktopPtrToUser(pItem->spSubMenu);
HMENU hsubmenu = UserHMGetHandle(pSubMenu);
if (IsMenu(hsubmenu)) return hsubmenu;
}
return NULL;
内核侧 IntGetSubMenu(menu.c L5021-5039)结构相同(不查 IsMenu,因为内核句柄必有效)。user32 侧多一层 IsMenu 防御,是因为共享内存可能被用户态进程破坏。
user32 GetMenuItemCount / GetMenuItemID(L955-975)
c
int WINAPI GetMenuItemCount(HMENU hmenu)
{ if ((pMenu = ValidateHandle(hmenu, TYPE_MENU))) return pMenu->cItems; return -1; }
UINT WINAPI GetMenuItemID(HMENU hMenu, int nPos)
{ if (!(lpmi = MENU_FindItem(&hMenu,(UINT*)&nPos,MF_BYPOSITION))) return -1;
if (lpmi->spSubMenu) return -1; /* 弹出项没有 ID(Windows 语义) */
return lpmi->wID; }
ValidateHandle(user32 的句柄校验)对用户态是只读校验 ,不做引用计数。NtUserGetMenuIndex(menu.c L6087-6118)是内核侧按子菜单对象查 wID 的辅助(MDI 用)。
5.10 使能/勾选:NtUserEnableMenuItem(L5871-5892)与 IntEnableMenuItem(L1216-1256)
c
UINT FASTCALL IntEnableMenuItem(PMENU MenuObject, UINT uIDEnableItem, UINT uEnable)
{
if (!(MenuItem = MENU_FindItem( &MenuObject, &uIDEnableItem, uEnable ))) return (UINT)-1;
res = MenuItem->fState & (MF_GRAYED | MF_DISABLED); /* 旧状态 */
MenuItem->fState ^= (res ^ uEnable) & (MF_GRAYED | MF_DISABLED); /* 异或式切换 */
if (res != uEnable) /* 状态确实变化 */
{
switch (MenuItem->wID) /* 系统菜单里的窗口控制项 */
{
case SC_CLOSE: case SC_MAXIMIZE: case SC_MINIMIZE:
case SC_MOVE: case SC_RESTORE: case SC_SIZE:
if (MenuObject->fFlags & MNF_SYSSUBMENU && MenuObject->spwndNotify != 0)
UserPaintCaption(MenuObject->spwndNotify, DC_BUTTONS); /* 刷新标题栏按钮 */
break;
default: break;
}
}
return res; /* 返回旧状态 */
}
- 用异或技巧
fState ^= (res ^ uEnable) & (MF_GRAYED|MF_DISABLED)只翻转发生变化的位。 - SC_ 联动 *:系统菜单里关闭/最大化/最小化/还原/移动/大小项的状态变化会通过
UserPaintCaption重绘标题栏按钮(因为按钮外观由对应菜单项的 enabled 状态决定)。前提是菜单带MNF_SYSSUBMENU(系统菜单的 SC_* 子菜单)且有spwndNotify。 NtUserEnableMenuItem(L5872)只是句柄校验 + 转发。IntCheckMenuItem(L1258-1271)同构,操作 MF_CHECKED 位,供NtUserCheckMenuItem(L5608-5629)调用。注意 user32 的CheckMenuItem(L741-759)会先自己读一次状态,相同则不调用内核(减少系统调用)。
5.11 菜单栏绘制链
绘制有两条链路:尺寸计算 (CalcSize)与实际绘制(Draw)。
5.11.1 入口:DrawMenuBar → SWP_FRAMECHANGED
user32 DrawMenuBar(menu.c L846-850)→ NtUserxDrawMenuBar(ntwrapper.h L778-781)→ NtUserCallHwndLock(hWnd, HWNDLOCK_ROUTINE_DRAWMENUBAR)。内核处理(simplecall.c L611-625):
c
case HWNDLOCK_ROUTINE_DRAWMENUBAR:
Ret = TRUE;
if ((Window->style & (WS_CHILD | WS_POPUP)) != WS_CHILD)
co_WinPosSetWindowPos(Window, HWND_DESKTOP, 0,0,0,0,
SWP_NOSIZE|SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE|SWP_FRAMECHANGED);
break;
也就是说 DrawMenuBar 本身不画,只是请求 FRAMECHANGED,让 WM_NCPAINT 阶段真正绘制(与 SetMenu 的 Repaint 同路径)。
5.11.2 内核绘制主函数:MENU_DrawMenuBar(L2742-2774)
c
UINT MENU_DrawMenuBar( HDC hDC, LPRECT lprect, PWND pWnd, BOOL suppress_draw )
{
PMENU lppop = UserGetMenuObject(UlongToHandle(pWnd->IDMenu));
if (lppop == NULL) return 0; /* 无菜单:不占空间 */
if (lprect == NULL) return UserGetSystemMetrics(SM_CYMENU);
if (suppress_draw) /* 只算不画(NtUserCalcMenuBar) */
{
hfontOld = NtGdiSelectFont(hDC, ghMenuFont);
MENU_MenuBarCalcSize(hDC, lprect, lppop, pWnd);
lprect->bottom = lprect->top + lppop->cyMenu;
if (hfontOld) NtGdiSelectFont(hDC, hfontOld);
return lppop->cyMenu;
}
else /* 真正绘制 */
return IntDrawMenuBarTemp(pWnd, hDC, lprect, lppop, NULL);
}
调用方(nonclient.c L1073/L1252/L1358 的 NC_Paint)拿到返回的菜单栏高度,累加进非客户区布局;suppress_draw=TRUE 模式被 NtUserCalcMenuBar(L5563-5603)用于纯测量(不产生绘制)。
5.11.3 IntDrawMenuBarTemp(L2684-2740)
c
DWORD WINAPI IntDrawMenuBarTemp(PWND pWnd, HDC hDC, LPRECT Rect, PMENU pMenu, HFONT Font)
{
UserSystemParametersInfo(SPI_GETFLATMENU, 0, &flat_menu, 0); /* 扁平菜单样式 */
if (!pMenu) pMenu = UserGetMenuObject(UlongToHandle(pWnd->IDMenu));
if (!Font) Font = ghMenuFont;
if (Rect == NULL || !pMenu) return UserGetSystemMetrics(SM_CYMENU);
FontOld = NtGdiSelectFont(hDC, Font);
if (pMenu->cyMenu == 0) MENU_MenuBarCalcSize(hDC, Rect, pMenu, pWnd); /* 尺寸脏则重算 */
Rect->bottom = Rect->top + pMenu->cyMenu;
FillRect(hDC, Rect, IntGetSysColorBrush(flat_menu ? COLOR_MENUBAR : COLOR_MENU)); /* 背景 */
NtGdiSelectPen(hDC, NtGdiGetStockObject(DC_PEN));
IntSetDCPenColor(hDC, IntGetSysColor(COLOR_3DFACE));
GreMoveTo(hDC, Rect->left, Rect->bottom - 1, NULL);
NtGdiLineTo(hDC, Rect->right, Rect->bottom - 1); /* 底部 3D 边框线 */
if (pMenu->cItems == 0) { NtGdiSelectFont(hDC, FontOld); return UserGetSystemMetrics(SM_CYMENU); }
for (i = 0; i < pMenu->cItems; i++)
MENU_DrawMenuItem(pWnd, pMenu, pWnd, hDC, &pMenu->rgItems[i],
pMenu->cyMenu, TRUE, ODA_DRAWENTIRE); /* 逐项绘制 */
NtGdiSelectFont(hDC, FontOld);
return pMenu->cyMenu;
}
5.11.4 尺寸计算:MENU_MenuBarCalcSize(L2101-2169)
c
static void MENU_MenuBarCalcSize( HDC hdc, LPRECT lprect, PMENU lppop, PWND pwndOwner )
{
lppop->cxMenu = lprect->right - lprect->left; /* 菜单栏宽度 = 客户区宽度 */
lppop->cyMenu = 0; maxY = lprect->top; start = 0; helpPos = ~0U;
while (start < lppop->cItems)
{
/* 从 start 开始,逐项计算尺寸;遇到 MF_MENUBREAK/MF_MENUBARBREAK 断行 */
for (i = start; i < lppop->cItems; i++, lpitem++)
{
if ((helpPos == ~0U) && (lpitem->fType & MF_RIGHTJUSTIFY)) helpPos = i;
if ((i != start) && (lpitem->fType & (MF_MENUBREAK | MF_MENUBARBREAK))) break;
MENU_CalcItemSize(hdc, lpitem, lppop, pwndOwner, orgX, orgY, TRUE, FALSE);
if (lpitem->cxItem > lprect->right)
{ if (i != start) break; else lpitem->cxItem = lprect->right; } /* 超宽钳制 */
maxY = max(maxY, lpitem->cyItem);
orgX = lpitem->cxItem; /* 下一项紧随其后 */
}
start = i; /* 下一行 */
}
lprect->bottom = maxY + 1;
lppop->cyMenu = lprect->bottom - lprect->top;
/* MF_RIGHTJUSTIFY 项(帮助菜单等)右对齐到 lprect->right */
if (helpPos == ~0U) return;
for (i = lppop->cItems - 1; i >= helpPos; i--, lpitem--)
{ ... lpitem->xItem += orgX - lpitem->cxItem; ... }
}
菜单栏项沿 X 轴横排,MENU_CalcItemSize(L1825-1990)负责单项测量:
- 自绘项 :发
WM_MEASUREITEM(ODT_MENU)给拥有窗口,宽度加2 * MenuCharSize.cx;菜单栏项高度忽略 itemHeight,取SM_CYMENUSIZE(L1856-1861)。 - 分隔符 :菜单栏分隔符高度
SM_CYMENUSIZE/2。 - 位图项 :
MENU_GetBitmapItemSize取尺寸;弹出菜单加勾选列宽度2*SM_CXMENUCHECK、文本间隙与箭头宽度(L1893-1930)。 - 文本项 :
DrawTextW(DT_SINGLELINE|DT_CALCRECT)测宽;MFS_DEFAULT项用ghMenuFontBold粗体测(L1943-1945);含\t的文本拆分测快捷键列,dxTab记录 Tab 后起点(L1954-1976)。
5.11.5 单项绘制:MENU_DrawMenuItem(L2204 起,签名见 L2204-2210)
c
static void FASTCALL MENU_DrawMenuItem(PWND Wnd, PMENU Menu, PWND WndOwner, HDC hdc,
PITEM lpitem, UINT itemheight, BOOL menuBar, UINT state)
按 fType 分流:MFT_OWNERDRAW → 发 WM_DRAWITEM(ODT_MENU);MFT_BITMAP → MENU_DrawBitmapItem(L1653,处理普通位图与魔法位图,魔法位图用 MENU_DrawPopupGlyph L1398 以 Marlett 字体画符号:'2'=还原、'0'=最小化、'1'=最大化、'r'=关闭);MF_STRING → 画文本 + 勾选标记 + 高亮背景 + 下划线。它同时服务于菜单栏(menuBar=TRUE)与弹出菜单(MENU_DrawPopupMenu L2599 调用)。本册不详述其像素级细节(属绘制专题)。
5.12 修改:ModifyMenuW 与 IntModifyMenu
menu.c 没有 名为 IntModifyMenu 的函数------"修改菜单项"就是 NtUserThunkedMenuItemInfo(..., bInsert=FALSE)。user32 ModifyMenuW(L1455-1483):
c
MENU_mnu2mnuii( uFlags, uIDNewItem, lpNewItem, &mii, TRUE); /* 老式 flag 转 MENUITEMINFOW */
if (lpNewItem && mii.fMask & MIIM_STRING && !mii.hbmpItem && mii.dwTypeData)
{ RtlInitUnicodeString(&MenuText, (PWSTR)mii.dwTypeData); ... }
res = NtUserThunkedMenuItemInfo(hMenu, uPosition, (BOOL)(uFlags & MF_BYPOSITION), FALSE, &mii, &MenuText);
MENU_mnu2mnuii(user32 L521)把 InsertMenu/ModifyMenu 的旧式 (flags, id, text) 三元组转成 MENUITEMINFOW:
c
pmii->fMask = MIIM_STATE | MIIM_ID | MIIM_FTYPE;
if (IS_STRING_ITEM(flags)) { pmii->fMask |= MIIM_STRING | MIIM_BITMAP; ... }
if (flags & MF_BITMAP) { pmii->fType = MFT_BITMAP; pmii->hbmpItem = (HBITMAP)id; }
else if (flags & MF_OWNERDRAW) { pmii->fType = MFT_OWNERDRAW; pmii->dwItemData = id; }
else if (flags & MF_SEPARATOR) { pmii->fType = MFT_SEPARATOR; }
else { pmii->fType = MFT_STRING; pmii->wID = (UINT)id; } /* 文本项:id 是命令 ID */
if (flags & MF_POPUP) { pmii->fMask |= MIIM_SUBMENU; pmii->hSubMenu = (HMENU)id; }
pmii->fState = flags & MII_STATE_MASK;
5.13 项矩形:IntGetMenuItemRect(L5255-5302)与 NtUserGetMenuItemRect(L6123-6204)
c
BOOL FASTCALL IntGetMenuItemRect(PWND pWnd, PMENU Menu, UINT uItem, PRECTL Rect)
{
if ((MenuItem = MENU_FindItem (&Menu, &I, MF_BYPOSITION)))
{
Rect->left = MenuItem->xItem; Rect->top = MenuItem->yItem;
Rect->right = MenuItem->cxItem; Rect->bottom = MenuItem->cyItem; /* 相对菜单自身的坐标 */
}
else { ERR("Failed Item Lookup! %u\n", uItem); return FALSE; }
if (!pWnd) { HWND hWnd = Menu->hWnd; if (!(pWnd = UserGetWindowObject(hWnd))) return FALSE; }
if (Menu->fFlags & MNF_POPUP) /* 弹出菜单:基准是菜单窗口客户区 */
{ XMove = pWnd->rcClient.left; YMove = pWnd->rcClient.top; }
else /* 菜单栏:基准是窗口本身 */
{ XMove = pWnd->rcWindow.left; YMove = pWnd->rcWindow.top; }
Rect->left += XMove; Rect->top += YMove; Rect->right += XMove; Rect->bottom += YMove;
return TRUE;
}
- 项坐标由
MENU_CalcItemSize在绘制前写入 ITEM 的 xItem/yItem/cxItem/cyItem,本函数只是换算成屏幕坐标。 - 菜单栏项是窗口坐标(rcWindow 基准),弹出菜单项是菜单窗口客户区坐标(rcClient 基准)------因为弹出菜单窗口本身就是贴屏弹出的顶层窗口。
NtUserGetMenuItemRect(L6124)做句柄校验(hWnd 为空时用Menu->hWnd)与_SEH2_TRY包裹的RtlCopyMemory回拷,供 user32GetMenuItemRect(ntwrapper.h L481-485)调用。
5.14 初始化:MenuInit(L358-395)
c
BOOL MenuInit(VOID)
{
if (!ghMenuFont || !ghMenuFontBold) /* 只初始化一次 */
{
ncm.cbSize = sizeof(ncm);
if(!UserSystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(ncm), &ncm, 0)) return FALSE;
ghMenuFont = GreCreateFontIndirectW(&ncm.lfMenuFont); /* 系统菜单字体 */
...
ncm.lfMenuFont.lfWeight = min(ncm.lfMenuFont.lfWeight + (FW_BOLD - FW_NORMAL), FW_HEAVY);
ghMenuFontBold = GreCreateFontIndirectW(&ncm.lfMenuFont); /* 粗体(MFS_DEFAULT 项用) */
...
GreSetObjectOwner(ghMenuFont, GDI_OBJ_HMGR_PUBLIC); /* 公开字体:所有进程可用 */
GreSetObjectOwner(ghMenuFontBold, GDI_OBJ_HMGR_PUBLIC);
co_IntSetupOBM(); /* 初始化内置位图(OBM_*) */
}
return TRUE;
}
在 win32k 初始化(InitSystemMetrics/IntUserInitialize 阶段)调用,为菜单子系统准备:
ghMenuFont:系统菜单字体(来自 NONCLIENTMETRICS.lfMenuFont);ghMenuFontBold:粗体变体,绘制MFS_DEFAULT(默认项)时选中(MENU_CalcItemSizeL1943);- 两个字体都设为
GDI_OBJ_HMGR_PUBLIC(公开对象,跨进程共享句柄); co_IntSetupOBM():初始化系统内置位图(OBI_UPARROW/OBI_DNARROW/OBI_MNARROW 等),供滚动箭头与弹出箭头绘制(MENU_DrawScrollArrowsL2176、MENU_CalcItemSizeL1836 读取gpsi->oembmi[])。
5.15 其他本册相关函数
5.15.1 默认项:UserSetMenuDefaultItem(L1273-1312)与 IntGetMenuDefaultItem(L1314-1344)
c
BOOL FASTCALL UserSetMenuDefaultItem(PMENU MenuObject, UINT uItem, UINT fByPos)
{
for (i = 0; i < MenuObject->cItems; i++, MenuItem++)
MenuItem->fState &= ~MFS_DEFAULT; /* 先清全部默认位 */
if (uItem == (UINT)-1) return TRUE; /* 无默认项 */
if (fByPos) { if (uItem >= cItems) return FALSE; MenuItem[uItem].fState |= MFS_DEFAULT; }
else { 按 wID 匹配置位; }
}
IntSetMenuItemInfo 在设置 MFS_DEFAULT 时(L1106-1107)会先调 UserSetMenuDefaultItem(MenuObject, -1, 0) 清掉其他项------保证同一菜单只有一个默认项 。IntGetMenuDefaultItem 支持 GMDI_USEDISABLED(允许返回禁用项)与 GMDI_GOINTOPOPUPS(递归进子菜单)两个标志,深度计数器 *gismc 防环。入口是 NtUserSetMenuDefaultItem(L6491-6512)。
5.15.2 高亮:IntHiliteMenuItem(L4803-4827)与 NtUserHiliteMenuItem(L6209-6241)
c
if (!(MenuItem = MENU_FindItem( &MenuObject, &uItem, uHilite ))) return TRUE;
if (uHilite & MF_HILITE) MenuItem->fState |= MF_HILITE;
else MenuItem->fState &= ~MF_HILITE;
if (MenuObject->iItem == uItemHilite) return TRUE;
MENU_HideSubPopups( WindowObject, MenuObject, FALSE, 0 ); /* 收起子菜单 */
MENU_SelectItem( WindowObject, MenuObject, uItemHilite, TRUE, 0 ); /* 移到新项 */
return TRUE; /* 注释:Always returns true!!!! */
MENU_SelectItem(L3144)负责切换高亮并发送 WM_MENUSELECT------它是菜单循环的组成部分,这里只展示其外部调用面。NtUserHiliteMenuItem(L6210)校验窗口与菜单句柄后转发,供 MDI 框架(WM_MDIREFRESHMENU 相关)使用。
5.15.3 命中测试:NtUserMenuItemFromPoint(L6303-6356)
c
if (!(Window = UserGetWindowObject(Menu->hWnd))) goto Exit; /* 用菜单所在窗口 */
X -= Window->rcWindow.left; Y -= Window->rcWindow.top;
for (i = 0; i < Menu->cItems; i++, mi++)
{
Rect = {mi->xItem, mi->yItem, mi->cxItem, mi->cyItem};
MENU_AdjustMenuItemRect(Menu, &Rect); /* 滚动偏移修正 */
if (RECTL_bPointInRect(&Rect, X, Y)) break; /* 命中 */
}
Ret = (mi ? i : NO_SELECTED_ITEM);
MENU_AdjustMenuItemRect(L1461-1471)在菜单可滚动(dwArrowsOn)时按 iTop 上下平移矩形。内核还有按屏幕点查项的 MENU_FindItemByCoords(L1480)与 IntMenuItemFromPoint(L1517),后者被自绘回调等内部路径使用。
5.15.4 菜单循环的边界函数(下册详述,此处只列签名)
IntTrackPopupMenuEx(L4574-4651):MENU_InitPopup建窗口 → 发 WM_INITMENUPOPUP →MENU_InitTracking→MENU_ShowPopup→MENU_TrackMenu模态循环 →MENU_ExitTracking→ 销毁弹出窗口、发 WM_UNINITMENUPOPUP。本册只挂起,循环内部(MENU_TrackMenuL4050、按键处理 L3735-4048、鼠标处理 L3558-3715)留待下册。MENU_InitTracking(L4404-4459):设置top_popup/top_popup_hmenu/fInsideMenuLoop,发 WM_ENTERMENULOOP/WM_INITMENU/WM_SETCURSOR,MsqSetStateWindow(MSQ_STATE_MENUOWNER)+co_UserSetCapture捕获鼠标,QF_CAPTURELOCKED锁定捕获,发EVENT_SYSTEM_MENUSTART。MENU_ExitTracking(L4464-4479):发 EVENT_SYSTEM_MENUEND/WM_EXITMENULOOP,清top_popup,恢复光标。MENU_EndMenu(L2662-2682):pwnd属于当前菜单(menu->hWnd或spwndNotify)时,置fInsideMenuLoop=FALSE并向top_popup投递 WM_CANCELMODE(由 defwnd.c L1070 在 WM_CANCELMODE 默认处理里调用)。NtUserEndMenu(L5897-5922):对全局循环态执行同样的 WM_CANCELMODE 收尾;MENU_IsMenuActive(L2650-2653)返回top_popup校验结果。
5.15.5 菜单窗口类:PopupMenuWndProc(L4656 起)
WC_MENU 类的内核窗口过程(user32 的 POPUPMENU_builtin_class L48-57 注册同名类,PopupMenuWndProcW 转发进内核)。WM_CREATE 时(L4708)从 cs->lpCreateParams(菜单句柄)取 PMENU 存入 ((PMENUWND)Wnd)->ppopupmenu->spmenu;WM_GETMENU 之类私有消息由 MM_GETMENUHANDLE/MM_SETMENUHANDLE(L30-31)辅助。弹出菜单窗口用 WS_POPUP | WS_CLIPSIBLINGS | WS_BORDER 与 WS_EX_PALETTEWINDOW | WS_EX_DLGMODALFRAME 创建(MENU_InitPopup L2788-2808)。
5.15.6 上下文帮助与 RTL:NtUserSetMenuContextHelpId / NtUserSetMenuFlagRtoL
NtUserSetMenuContextHelpId(L6465-6485)→IntSetMenuContextHelpId(L870-875):写Menu->dwContextHelpId。NtUserSetMenuFlagRtoL(L6517-6536)→IntSetMenuFlagRtoL(L862-868):置MNF_RTOL,用于从右到左布局(配合MENU_InitPopupL2837-2841 的fDroppedLeft)。
5.15.7 绘制辅助入口:NtUserCalcMenuBar / NtUserPaintMenuBar / NtUserDrawMenuBarTemp
NtUserCalcMenuBar(L5563-5603):MENU_DrawMenuBar(hdc, &Rect, Window, TRUE)纯测量,返回菜单栏高度。NtUserPaintMenuBar(L6359-6391):MENU_DrawMenuBar(hDC, &Rect, Window, FALSE)实绘(非客户区外部绘制用,如主题渲染路径)。NtUserDrawMenuBarTemp(L6245-6298)→IntDrawMenuBarTemp(Window, hDC, &Rect, Menu, hFont):带自定义字体/菜单/矩形的临时绘制入口(user32DrawMenuBarTemp用,Explorer 等绘制扁平菜单栏时调用)。
6. 菜单项插入算法
6.1 插入位置确定(MENU_InsertItem L638-687)
输入:menu, pos(位置或命令ID), flags(MF_BYPOSITION|MF_BYCOMMAND)
│
▼
MF_BYPOSITION ──► pos > cItems ? 钳制到 cItems(追加到末尾)
MF_BYCOMMAND ──► MENU_FindItem 全树查找;未找到 → pos = cItems(追加)
│
▼
MDI 魔法位图钳制:while (pos>0 && 前一项是 HBMMENU_SYSTEM..HBMMENU_MBAR_CLOSE_D) pos--
│ (保证 MDI 最小化/还原/关闭按钮永远在菜单栏右端)
▼
分配新数组 sizeof(ITEM)*(cItems+1)
├─ pos>0 :memcpy [0,pos)
├─ pos<cItems :memcpy [pos+1, cItems)(整体右移一位)
└─ 释放旧数组
cItems++,新项清零,cyMenu=0(尺寸脏)
复杂度 :插入 O(n)(数组搬运),按位置查找 O(1),按命令查找 O(树深度 × 项数)(深度优先递归)。对菜单这种"小 n、低频率修改、高频遍历绘制"的场景,数组是比链表更好的选择------MENU_DrawMenuItem 循环里对 rgItems[i] 的顺序访问能充分利用缓存。
6.2 四种项的填充路径(IntSetMenuItemInfo 按 fType 分流)
| 项类型 | fMask 组合 | ITEM 字段落点 |
|---|---|---|
| 文本 MF_STRING | MIIM_STRING(+MIIM_TYPE) | lpstr 深拷贝到桌面堆,Xlpstr 同步,cch 记录字符数 |
| 位图 MF_BITMAP | MIIM_BITMAP 或 MIIM_TYPE+MFT_BITMAP | hbmp;魔法位图范围(HBMMENU_CALLBACK...HBMMENU_POPUP_MINIMIZE)置 MFS_HBMMENUBMP |
| 自绘 MF_OWNERDRAW | MIIM_FTYPE+MIIM_DATA | fType 置位;dwItemData 保存应用数据(绘制回调经 WM_DRAWITEM 取回) |
| 分隔符 MF_SEPARATOR | MIIM_FTYPE 或自动判定 | fType 置位;无文本无位图非自绘的"空项"自动升级(L1193-1198) |
| 弹出 MF_POPUP | MIIM_SUBMENU | spSubMenu 指针 + UserReferenceObject 引用;MNF_POPUP 标记;MENU_depth 防环 |
6.3 子菜单链接的引用计数协议
AppendMenu(hParent, MF_POPUP, (UINT_PTR)hSub, "Sub")
└─ IntSetMenuItemInfo(MIIM_SUBMENU)
├─ 旧 spSubMenu ? IntReleaseMenuObject(旧) ← 释放旧引用
├─ UserReferenceObject(SubMenuObject) ← 持有新引用
└─ MENU_depth 检查(>30 拒绝,防自环/深环)
DestroyMenu(hParent)
└─ IntDestroyMenu → 每项:
├─ IntReleaseMenuObject(spSubMenu) ← 释放插入时引用
└─ (bRecurse) IntDestroyMenuObject(spSubMenu) ← 递归销毁子菜单对象
协议要点:子菜单对象被插入即 +1 引用 ,无论父菜单后来是否被销毁,引用都要对称释放;IntDestroyMenuObject 自身不对子菜单持有引用(它只销毁自己),这正是 RemoveMenu(bRecurse=FALSE)能让子菜单"存活并改嫁"的原因。
6.4 移除算法(IntRemoveMenuItem L594-631)
MENU_FindItem(定位) → 释放文本 → (bRecurse) 销毁子菜单
→ cItems--
→ cItems==0 ? 释放整个数组
: 左移覆盖 [nPos, cItems) → DesktopHeapReAlloc 收缩(失败容忍)
DeleteMenu(NtUserDeleteMenu L5635)与 RemoveMenu(NtUserRemoveMenu L6397)的唯一区别就是 bRecurse:前者连带销毁子菜单对象,后者仅摘项。user32.spec 中两者分别映射 DeleteMenu/RemoveMenu 到这两个系统调用。
7. 系统菜单构建
7.1 层次结构
窗口 Wnd
├─ Wnd->IDMenu ← 菜单栏(可选,SetMenu 挂接)
└─ Wnd->SystemMenu ← 系统菜单外壳(MENU_GetSystemMenu 创建)
└─ 项0(MF_POPUP, MNF_SYSMENU|MNF_POPUP)
└─ spSubMenu = SC_* 子菜单(克隆自 SYSMENU 资源模板)
├─ SC_RESTORE (还原)
├─ SC_MOVE (移动)
├─ SC_SIZE (大小)
├─ SC_MINIMIZE (最小化)
├─ SC_MAXIMIZE (最大化)
├─ SC_CLOSE (关闭,MFS_DEFAULT)
└─ 应用可在此追加自定义项
关键点:Wnd->SystemMenu 指向的外壳 本身几乎透明,应用拿到的句柄是 IntGetSystemMenu 返回的 SC_ 子菜单 *(L5435-5447 用 IntGetSubMenu(Wnd->SystemMenu, 0) 取出并返回)------所以应用对 GetSystemMenu 返回句柄的 AppendMenu 修改都落在这个子菜单上。
7.2 MENU_GetSystemMenu(L5304-5406)构建流程
c
PMENU FASTCALL MENU_GetSystemMenu(PWND Window, PMENU Popup)
{
hSysMenu = UserCreateMenu(Window->head.rpdesk, FALSE); /* 外壳:菜单栏型 */
SysMenu = UserGetMenuObject(hSysMenu);
SysMenu->fFlags |= MNF_SYSMENU;
SysMenu->hWnd = UserHMGetHandle(Window);
if (!Popup) /* 首次:从资源模板构建 */
{
/* 经 user32 回调加载系统菜单资源:MDI 子窗口用 SYSMENUMDI */
if (Window->ExStyle & WS_EX_MDICHILD)
RtlInitUnicodeString(&MenuName, L"SYSMENUMDI");
else
RtlInitUnicodeString(&MenuName, L"SYSMENU");
hNewMenu = co_IntCallLoadMenu(hModClient, &MenuName);
Menu = UserGetMenuObject(hNewMenu);
Menu->fFlags |= MNS_CHECKORBMP | MNF_SYSMENU | MNF_POPUP;
/* 给 SC_* 项设置魔法位图(标题栏按钮用 Marlett 符号绘制) */
ItemInfoSet.fMask = MIIM_BITMAP;
ItemInfoSet.hbmpItem = HBMMENU_POPUP_CLOSE; IntMenuItemInfo(Menu, SC_CLOSE, ...);
ItemInfoSet.hbmpItem = HBMMENU_POPUP_RESTORE; IntMenuItemInfo(Menu, SC_RESTORE, ...);
ItemInfoSet.hbmpItem = HBMMENU_POPUP_MAXIMIZE; IntMenuItemInfo(Menu, SC_MAXIMIZE, ...);
ItemInfoSet.hbmpItem = HBMMENU_POPUP_MINIMIZE; IntMenuItemInfo(Menu, SC_MINIMIZE, ...);
NewMenu = IntCloneMenu(Menu); /* 模板 → 私有副本 */
IntReleaseMenuObject(NewMenu);
UserSetMenuDefaultItem(NewMenu, SC_CLOSE, FALSE); /* 默认项 = 关闭 */
IntDestroyMenuObject(Menu, FALSE); /* 模板用完即毁 */
}
else NewMenu = Popup; /* SetSystemMenu 提供的外部菜单 */
NewMenu->fFlags |= MNF_SYSMENU | MNF_POPUP;
if (Window->pcls->style & CS_NOCLOSE) /* 类样式禁止关闭 */
IntRemoveMenuItem(NewMenu, SC_CLOSE, MF_BYCOMMAND, TRUE);
/* 外壳只有一个弹出项,指向 SC_* 子菜单 */
ItemInfo.fMask = MIIM_FTYPE|MIIM_STRING|MIIM_STATE|MIIM_SUBMENU;
ItemInfo.fType = MF_POPUP; ItemInfo.fState = MFS_ENABLED;
ItemInfo.hSubMenu = UserHMGetHandle(NewMenu);
IntInsertMenuItem(SysMenu, (UINT)-1, TRUE, &ItemInfo, NULL); /* 追加 */
return SysMenu;
}
要点:
- 资源模板来源 :
co_IntCallLoadMenu(hModClient, &MenuName)(callback.c L903)经 KeUserModeCallback 回调 user32,用LoadMenuIndirectW解析 win32k 自带的 SYSMENU 资源(user32 的菜单模板资源)。MDI 子窗口换用 SYSMENUMDI 模板。 - 魔法位图 :SC_CLOSE/SC_RESTORE/SC_MAXIMIZE/SC_MINIMIZE 项的 hbmp 设为 HBMMENU_POPUP_* 魔法值,绘制时由
MENU_DrawPopupGlyph(L1398)用 Marlett 字体画出符号,而非真实位图。 - 克隆 :模板是全局共享的,每窗口
IntCloneMenu一份,避免相互污染。 - CS_NOCLOSE :窗口类带 CS_NOCLOSE 时移除 SC_CLOSE 项(与
MENU_InitSysMenuPopupL1377 的置灰逻辑互补)。 - 外壳(SysMenu)的
hWnd指向窗口------所以系统菜单"归属"窗口,销毁窗口时随窗口清理。
7.3 IntGetSystemMenu(L5408-5449)------GetSystemMenu API 内核侧
c
PMENU FASTCALL IntGetSystemMenu(PWND Window, BOOL bRevert)
{
if (bRevert) /* 恢复默认:销毁当前副本 */
{
if (Window->SystemMenu)
{
Menu = UserGetMenuObject(Window->SystemMenu);
if (Menu && !(Menu->fFlags & MNF_SYSDESKMN)) /* 桌面系统菜单不动 */
{ IntDestroyMenuObject(Menu, TRUE); Window->SystemMenu = NULL; }
}
}
else /* 取用:没有则按需构建 */
{
Menu = Window->SystemMenu ? UserGetMenuObject(Window->SystemMenu) : NULL;
if ((!Menu || Menu->fFlags & MNF_SYSDESKMN) && Window->style & WS_SYSMENU)
{
Menu = MENU_GetSystemMenu(Window, NULL);
Window->SystemMenu = Menu ? UserHMGetHandle(Menu) : NULL;
}
}
if (Window->SystemMenu)
{
hMenu = IntGetSubMenu(Window->SystemMenu, 0); /* 剥外壳,返回 SC_* 子菜单 */
Menu = UserGetMenuObject(hMenu);
if (Menu)
{
Menu->spwndNotify = Window; /* 自绘/通知指向窗口 */
Menu->fFlags |= MNF_SYSSUBMENU; /* 标记"系统子菜单"(EnableMenuItem 联动用) */
}
return Menu; /* 应用见到的就是这个 */
}
return NULL;
}
NtUserGetSystemMenu(L5684-5710)只是取窗口对象后转发;bRevert=TRUE 返回 NULL(MSDN 语义:重置后旧菜单销毁)。NtUserSetSystemMenu(L5719-5753)→ IntSetSystemMenu(L5451-5482):销毁旧外壳 → MENU_GetSystemMenu(Window, Menu) 用应用提供的菜单做 SC_* 子菜单重新组装。
7.4 动态置灰:MENU_InitSysMenuPopup(L1362-1389)
系统菜单每次弹出前按窗口当前状态置灰/加粗各项(在 IntTrackPopupMenuEx L4619-4620 处调用):
c
gray = !(style & WS_THICKFRAME) || (style & (WS_MAXIMIZE | WS_MINIMIZE));
IntEnableMenuItem( menu, SC_SIZE, (gray ? MF_GRAYED : MF_ENABLED) ); /* 无边框或已最大化→禁大小 */
gray = ((style & (WS_MAXIMIZE | WS_MINIMIZE)) != 0);
IntEnableMenuItem( menu, SC_MOVE, (gray ? MF_GRAYED : MF_ENABLED) ); /* 最大化/最小化→禁移动 */
gray = !(style & WS_MINIMIZEBOX) || (style & WS_MINIMIZE);
IntEnableMenuItem( menu, SC_MINIMIZE, (gray ? MF_GRAYED : MF_ENABLED) );
gray = !(style & WS_MAXIMIZEBOX) || (style & WS_MAXIMIZE);
IntEnableMenuItem( menu, SC_MAXIMIZE, (gray ? MF_GRAYED : MF_ENABLED) );
gray = !(style & (WS_MAXIMIZE | WS_MINIMIZE));
IntEnableMenuItem( menu, SC_RESTORE, (gray ? MF_GRAYED : MF_ENABLED) ); /* 正常态→禁还原 */
gray = (clsStyle & CS_NOCLOSE) != 0;
if (gray) IntEnableMenuItem( menu, SC_CLOSE, MF_GRAYED ); /* 保持禁用位 */
/* 默认项:最小化→还原;标题栏点击→还原/最大化;否则→关闭 */
if (style & WS_MINIMIZE) DefItem = SC_RESTORE;
else if (HitTest == HTCAPTION) DefItem = ((style & (WS_MAXIMIZE|WS_MINIMIZE)) ? SC_RESTORE : SC_MAXIMIZE);
else DefItem = SC_CLOSE;
UserSetMenuDefaultItem(menu, DefItem, MF_BYCOMMAND);
8. 菜单栏绘制(MENU_DrawMenuBar 调用链)
8.1 完整调用链
DrawMenuBar(hWnd) [user32 menu.c L846]
└─ NtUserxDrawMenuBar(hWnd) [ntwrapper.h L778]
└─ NtUserCallHwndLock(DRAWMENUBAR) [simplecall.c L611]
└─ co_WinPosSetWindowPos(SWP_FRAMECHANGED) ← 请求重算框架
▼ WM_NCPAINT
NC_Paint(hDC, pWnd, ...) [nonclient.c L1073/L1252/L1358]
└─ MENU_DrawMenuBar(hDC, &TempRect, pWnd, FALSE) [menu.c L2742]
├─ UserGetMenuObject(Wnd->IDMenu) ← 取菜单栏对象
├─ 无菜单 → 返回 0(不占空间)
└─ IntDrawMenuBarTemp(pWnd, hDC, lprect, lppop, NULL) [L2684]
├─ SPI_GETFLATMENU → 扁平样式选 COLOR_MENUBAR
├─ MENU_MenuBarCalcSize(hDC, rect, lppop, pWnd) [L2101]
│ └─ 逐行 MENU_CalcItemSize(...) [L1825]
│ ├─ MFT_OWNERDRAW → WM_MEASUREITEM(回调应用)
│ ├─ 文本 → DrawTextW(DT_CALCRECT) 测宽(MFS_DEFAULT 用粗体)
│ └─ 位图 → MENU_GetBitmapItemSize
├─ FillRect(COLOR_MENU/COLOR_MENUBAR) ← 背景
├─ MoveTo/LineTo 底部 3D 边框线
└─ for each item: MENU_DrawMenuItem(...) [L2204]
├─ MFT_OWNERDRAW → WM_DRAWITEM(回调应用自绘)
├─ MFT_BITMAP → MENU_DrawBitmapItem [L1653]
│ └─ 魔法位图 → MENU_DrawPopupGlyph [L1398](Marlett 字体符号)
└─ MF_STRING → 文本 + 勾选列 + 高亮 + 下划线
8.2 菜单栏 vs 弹出菜单的绘制差异
| 维度 | 菜单栏(menuBar=TRUE) | 弹出菜单(menuBar=FALSE) |
|---|---|---|
| 尺寸入口 | MENU_MenuBarCalcSize(L2101) |
MENU_PopupMenuCalcSize(L2012) |
| 布局方向 | 项沿 X 轴横排,MF_MENUBREAK 断行 | 项沿 Y 轴纵排,MF_MENUBREAK 分列 |
| 宽度 | 恒等于客户区宽(lprect 决定) | 取最长项宽(含勾选列 + 快捷键列 + 子菜单箭头) |
| 高度 | SM_CYMENUSIZE/SM_CYMENU |
各项累加,超 cyMax(默认屏幕高)出现滚动箭头 |
| 绘制函数 | IntDrawMenuBarTemp(L2684) |
MENU_DrawPopupMenu(L2599) |
| 项矩形基准 | 窗口 rcWindow | 菜单窗口 rcClient |
| 菜单项间隔 | MENU_BAR_ITEMS_SPACE(12px) |
MENU_ITEM_HBMP_SPACE(5px)/MENU_TAB_SPACE(8px) |
8.3 勾选列与快捷键列(弹出菜单专有布局)
MENU_CalcItemSize 对弹出菜单项按如下顺序累加宽度(L1924-1930 + L1954-1976):
[勾选列: SM_CXMENUCHECK] + 4 + MenuCharSize.cx ...dxTab...
[文本(Tab 前)] + MenuCharSize.cx + [Tab 后快捷键文本]
+ [子菜单箭头: OBI_MNARROW 宽]
MNS_NOCHECK菜单样式可取消勾选列(L1907/L1925 的MNS_STYLE_MASK & MNS_NOCHECK判断)。dxTab记录文本后、快捷键前的 X 坐标,绘制时(MENU_DrawMenuItem)用来对齐快捷键列。MNS_CHECKORBMP(系统菜单 L5356 设置):勾选列与位图列合一,勾选标记直接覆盖在位图列上。
8.4 滚动箭头
菜单高度超过 MENU_GetMaxPopupHeight(L1995-2005,cyMax 或屏幕高减边框)时,MENU_PopupMenuCalcSize 设置 dwArrowsOn/iTop/iMaxTop;MENU_DrawScrollArrows(L2176-2197)在菜单顶部/底部画 OBI_UPARROW/OBI_DNARROW 箭头,MENU_AdjustMenuItemRect(L1461)在命中测试时按 iTop 平移项矩形。滚动是菜单循环(下册)与绘制共享的状态。
9. 与周边模块的关系
9.1 defwnd.c(WM_SYSCOMMAND / WM_CANCELMODE)
IntDefWindowProc的 WM_SYSCOMMAND 默认处理把SC_MOUSEMENU/SC_KEYMENU转交菜单子系统(defwnd.c L192-203):
c
case SC_MOUSEMENU:
MENU_TrackMouseMenuBar(pWnd, wParam & 0x000f, Pt); /* 鼠标点标题栏/菜单栏 → 跟踪 */
break;
case SC_KEYMENU:
MENU_TrackKbdMenuBar(pWnd, wParam, (WCHAR)lParam); /* Alt 键 → 键盘跟踪 */
break;
MENU_TrackMouseMenuBar(menu.c L4486-4514)按命中区域选菜单:HTSYSMENU→IntGetSystemMenu(pWnd, FALSE),否则IntGetMenu(hWnd)(菜单栏),然后以TPM_BUTTONDOWN|TPM_LEFTALIGN|TPM_LEFTBUTTON|TPM_VERTICAL进入跟踪。MENU_TrackKbdMenuBar(L4516-4550)处理 Alt 激活菜单栏:按wParam(SC_KEYMENU 的 hiword 是 Alt+ 的键码/虚拟键)定位首项,MNF_UNDERLINE显示下划线。- WM_CANCELMODE 默认处理(defwnd.c L1070)调用
MENU_EndMenu(Wnd),把"取消模式"广播成"结束菜单"。
9.2 window.c / winpos.c(窗口字段 IDMenu/SystemMenu)
Wnd->IDMenu:菜单栏句柄字段。IntSetMenu写入;IntGetMenu/MENU_DrawMenuBar/NtUserGetMenuBarInfo(OBJID_MENU)读取。窗口销毁时(winpos/UserDestroyWindow路径)不主动销毁菜单------菜单是独立对象,由应用DestroyMenu或进程清理回收。Wnd->SystemMenu:系统菜单外壳句柄。IntGetSystemMenu/MENU_GetSystemMenu维护。Wnd->pcls->style & CS_NOCLOSE:影响系统菜单 SC_CLOSE 项(MENU_GetSystemMenuL5390 移除 /MENU_InitSysMenuPopupL1377 置灰)。
9.3 draw.c / nonclient.c(绘制)
UITOOLS95_DrawFrameMenu(draw.c,menu.c L14 声明):绘制 DFCS_MENUARROWUP/DOWN 滚动箭头等 UI 元素,被MENU_DrawScrollArrows调用。- nonclient.c 的
NC_Paint在计算客户区偏移时调用MENU_DrawMenuBar(L1073/L1252/L1358),返回高度累加进rcClient------这就是菜单栏"占用客户区顶部"的实现方式。 UserPaintCaption(IntEnableMenuItem L1249 调用)在 SC_* 项状态变化时重绘标题栏按钮。
9.4 callback.c(user32 回调)
co_IntCallLoadMenu(callback.c L903):内核 → user32 的 KeUserModeCallback,加载 SYSMENU 资源模板(MENU_GetSystemMenuL5333/L5338 使用)。- 自绘消息(WM_MEASUREITEM/WM_DRAWITEM)经
co_IntSendMessage走正常消息路径送达spwndNotify窗口。
9.5 mdi.c(MDI 框架)
- MDI 子窗口的
SystemMenu用 SYSMENUMDI 模板(L5332);MDI 框架用GetMenu/GetSubMenu/SetMenu/DrawMenuBar/DestroyMenu(mdi.c L872/L945/L955/L966 等)维护窗口菜单与系统菜单切换(MDISetMenumdi.c L336)。 NtUserGetMenuIndex(L6087)提供"按子菜单找 wID"的辅助,MDI 的WM_MDIREFRESHMENU用它定位子窗口菜单。
9.6 accelerator.c(加速键)
NtUserTranslateAccelerator(accelerator.c L102/L116)用 UserGetMenuObject 取窗口菜单,逐项比对 ITEM.wID 与加速键表------菜单项 ID 是加速键分发的目标。
10. 调用链图
10.1 创建 → 填充 → 挂窗 → 绘制(完整时序)
渲染错误: Mermaid 渲染失败: Parse error on line 17: ... hMenu->>hWnd = hWnd K->>K: co_WinPo -----------------------^ Expecting 'TXT', got 'NEWLINE'
10.2 系统菜单构建时序
渲染错误: Mermaid 渲染失败: Parse error on line 11: ...efaultItem(SC_CLOSE) K->>K: 外壳 SysMe -----------------------^ Expecting '()', 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'SOLID_ARROW_TOP', 'SOLID_ARROW_BOTTOM', 'STICK_ARROW_TOP', 'STICK_ARROW_BOTTOM', 'SOLID_ARROW_TOP_DOTTED', 'SOLID_ARROW_BOTTOM_DOTTED', 'STICK_ARROW_TOP_DOTTED', 'STICK_ARROW_BOTTOM_DOTTED', 'SOLID_ARROW_TOP_REVERSE', 'SOLID_ARROW_BOTTOM_REVERSE', 'STICK_ARROW_TOP_REVERSE', 'STICK_ARROW_BOTTOM_REVERSE', 'SOLID_ARROW_TOP_REVERSE_DOTTED', 'SOLID_ARROW_BOTTOM_REVERSE_DOTTED', 'STICK_ARROW_TOP_REVERSE_DOTTED', 'STICK_ARROW_BOTTOM_REVERSE_DOTTED', 'BIDIRECTIONAL_SOLID_ARROW', 'DOTTED_ARROW', 'BIDIRECTIONAL_DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', got 'NEWLINE'
10.3 菜单对象生命周期状态图
#mermaid-svg-tGEV0va0JSb85mkH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tGEV0va0JSb85mkH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tGEV0va0JSb85mkH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tGEV0va0JSb85mkH .error-icon{fill:#552222;}#mermaid-svg-tGEV0va0JSb85mkH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tGEV0va0JSb85mkH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tGEV0va0JSb85mkH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tGEV0va0JSb85mkH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tGEV0va0JSb85mkH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tGEV0va0JSb85mkH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tGEV0va0JSb85mkH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tGEV0va0JSb85mkH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tGEV0va0JSb85mkH .marker.cross{stroke:#333333;}#mermaid-svg-tGEV0va0JSb85mkH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tGEV0va0JSb85mkH p{margin:0;}#mermaid-svg-tGEV0va0JSb85mkH defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-tGEV0va0JSb85mkH g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-tGEV0va0JSb85mkH g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-tGEV0va0JSb85mkH g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-tGEV0va0JSb85mkH g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-tGEV0va0JSb85mkH g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-tGEV0va0JSb85mkH .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-tGEV0va0JSb85mkH .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-tGEV0va0JSb85mkH .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-tGEV0va0JSb85mkH .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-tGEV0va0JSb85mkH .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-tGEV0va0JSb85mkH .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-tGEV0va0JSb85mkH .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-tGEV0va0JSb85mkH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tGEV0va0JSb85mkH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tGEV0va0JSb85mkH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tGEV0va0JSb85mkH .edgeLabel .label text{fill:#333;}#mermaid-svg-tGEV0va0JSb85mkH .label div .edgeLabel{color:#333;}#mermaid-svg-tGEV0va0JSb85mkH .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-tGEV0va0JSb85mkH .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-tGEV0va0JSb85mkH .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-tGEV0va0JSb85mkH .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-tGEV0va0JSb85mkH .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-tGEV0va0JSb85mkH .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tGEV0va0JSb85mkH .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tGEV0va0JSb85mkH #statediagram-barbEnd{fill:#333333;}#mermaid-svg-tGEV0va0JSb85mkH .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tGEV0va0JSb85mkH .cluster-label,#mermaid-svg-tGEV0va0JSb85mkH .nodeLabel{color:#131300;}#mermaid-svg-tGEV0va0JSb85mkH .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-tGEV0va0JSb85mkH .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-tGEV0va0JSb85mkH .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-tGEV0va0JSb85mkH .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-tGEV0va0JSb85mkH .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-tGEV0va0JSb85mkH .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-tGEV0va0JSb85mkH .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-tGEV0va0JSb85mkH .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-tGEV0va0JSb85mkH .note-edge{stroke-dasharray:5;}#mermaid-svg-tGEV0va0JSb85mkH .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-tGEV0va0JSb85mkH .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-tGEV0va0JSb85mkH .statediagram-note text{fill:black;}#mermaid-svg-tGEV0va0JSb85mkH .statediagram-note .nodeLabel{color:black;}#mermaid-svg-tGEV0va0JSb85mkH .statediagram .edgeLabel{color:red;}#mermaid-svg-tGEV0va0JSb85mkH #dependencyStart,#mermaid-svg-tGEV0va0JSb85mkH #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-tGEV0va0JSb85mkH .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tGEV0va0JSb85mkH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} IntCreateMenu (fFlags=MNF_POPUP, cItems=0)
IntInsertMenuItem (数组扩容+填充)
IntSetMenu (Wnd->IDMenu=hMenu, hMenu->hWnd=hWnd)
IntSetMenu(NULL) / SetMenu 换菜单
MENU_InitTracking (top_popup, 捕获鼠标)
MENU_ExitTracking
IntDestroyMenuObject (UserMarkObjectDestroy)
IntDestroyMenu (递归释放子菜单/文本/位图) + UserDeleteObject
空菜单
有项
挂窗
跟踪中
销毁中
11. 源码索引
11.1 本册核心函数索引(menu.c,6699 行)
| 函数 | 行号 | 一句话职责 |
|---|---|---|
IntGetMenuObject |
L191-199 | 句柄→PMENU 并加锁 |
VerifyMenu |
L201-236 | SEH 验证 PMENU 可用性 |
IntIsMenu |
L238-244 | 判断句柄是否菜单 |
IntGetMenu / get_win_sys_menu |
L247-267 | 取窗口菜单栏/系统菜单 |
IntDestroyMenu |
L269-307 | 递归销毁项数组 |
UserDestroyMenuObject |
L311-314 | 对象管理器析构回调 |
IntDestroyMenuObject |
L317-356 | 菜单对象销毁主函数 |
MenuInit |
L358-395 | 菜单字体与内置位图初始化 |
MENU_depth |
L403-426 | 菜单树深度/环路检测 |
MENU_GetStartOfNextColumn/PrevColumn |
L436-498 | 分列导航辅助 |
MENU_FindItem |
L507-554 | 按位置/命令递归查找项 |
MENU_FindSubMenu |
L563-592 | 按子菜单对象查位置 |
IntRemoveMenuItem |
L594-631 | 移除项(数组左移) |
MENU_InsertItem |
L638-687 | 插入项(数组扩容) |
IntInsertMenuItem |
L690-725 | 插入 + 填充 + 回滚 |
IntCreateMenu |
L728-767 | 创建 MENU 对象 |
IntCloneMenuItems/IntCloneMenu |
L770-860 | 菜单深拷贝 |
IntSetMenuFlagRtoL/IntSetMenuContextHelpId |
L863-875 | 标志/帮助 ID |
IntGetMenuInfo/IntSetMenuInfo |
L878-958 | 菜单级信息读写 |
IntGetMenuItemInfo |
L961-1029 | 项信息读出 |
IntSetMenuItemInfo |
L1032-1213 | 项信息写入(核心填充) |
IntEnableMenuItem |
L1216-1256 | 使能/置灰(SC_* 联动标题栏) |
IntCheckMenuItem |
L1258-1271 | 勾选位切换 |
UserSetMenuDefaultItem/IntGetMenuDefaultItem |
L1274-1344 | 默认项设置/查询 |
co_IntGetSubMenu |
L1346-1355 | 按位置取子菜单(内部) |
MENU_InitSysMenuPopup |
L1362-1389 | 系统菜单动态置灰 |
MENU_DrawPopupGlyph |
L1398-1454 | Marlett 字体画系统符号 |
MENU_AdjustMenuItemRect |
L1461-1471 | 滚动偏移修正 |
MENU_FindItemByCoords/IntMenuItemFromPoint |
L1480-1533 | 坐标命中 |
MENU_FindItemByKey |
L1535-1587 | 键盘首字母导航 |
MENU_GetBitmapItemSize/MENU_DrawBitmapItem |
L1588-1824 | 位图测量/绘制 |
MENU_CalcItemSize |
L1825-1990 | 单项尺寸计算 |
MENU_PopupMenuCalcSize |
L2012-2100 | 弹出菜单整体尺寸 |
MENU_MenuBarCalcSize |
L2101-2169 | 菜单栏整体尺寸 |
MENU_DrawScrollArrows |
L2176-2197 | 滚动箭头 |
MENU_DrawMenuItem |
L2204-2598 | 单项绘制(自绘/位图/文本) |
MENU_DrawPopupMenu |
L2599-2649 | 弹出菜单绘制 |
MENU_IsMenuActive |
L2650-2653 | 当前活动菜单 |
MENU_EndMenu |
L2662-2682 | 结束菜单(WM_CANCELMODE 路径) |
IntDrawMenuBarTemp |
L2684-2740 | 菜单栏实际绘制 |
MENU_DrawMenuBar |
L2742-2774 | 菜单栏绘制主入口 |
MENU_InitPopup |
L2781-2843 | 弹出菜单窗口初始化 |
MENU_ShowPopup |
L2939-3101 | 弹出菜单定位与显示 |
MENU_SelectItem/MENU_MoveSelection |
L3144-3266 | 选择/移动高亮 |
MENU_HideSubPopups/MENU_ShowSubPopup |
L3267-3477 | 子菜单展开/收起 |
MENU_TrackMenu |
L4050-4403 | 模态菜单循环(下册详述) |
MENU_InitTracking/MENU_ExitTracking |
L4404-4479 | 捕获/通知进入退出 |
MENU_TrackMouseMenuBar/MENU_TrackKbdMenuBar |
L4486-4550 | 鼠标/键盘激活菜单栏 |
IntTrackPopupMenuEx |
L4574-4651 | 弹出菜单跟踪(本册挂起) |
PopupMenuWndProc |
L4656-4801 | WC_MENU 窗口过程 |
IntHiliteMenuItem |
L4803-4827 | 高亮项 |
UserInsertMenuItem |
L4960-4999 | 用户插入(结构校验) |
IntGetMenuState/IntGetSubMenu/IntFindSubMenu |
L5001-5055 | 内部查询三件套 |
UserCreateMenu |
L5058-5099 | 菜单创建(窗口站校验) |
IntMenuItemInfo/UserMenuItemInfo/UserMenuInfo |
L5102-5253 | 项/菜单信息通道 |
IntGetMenuItemRect |
L5255-5302 | 项屏幕矩形 |
MENU_GetSystemMenu |
L5304-5406 | 系统菜单构建 |
IntGetSystemMenu/IntSetSystemMenu |
L5409-5482 | 系统菜单取用/替换 |
IntSetMenu |
L5484-5553 | 挂接菜单到窗口 |
NtUserCalcMenuBar/NtUserCheckMenuItem/NtUserDeleteMenu |
L5563-5655 | 测量/勾选/删除 |
NtUserGetSystemMenu/NtUserSetSystemMenu |
L5685-5753 | 系统菜单 API |
UserDestroyMenu/NtUserDestroyMenu |
L5820-5866 | 销毁 API |
NtUserEnableMenuItem/NtUserEndMenu |
L5872-5922 | 使能/结束菜单 |
NtUserGetMenuBarInfo |
L5928-6082 | 菜单栏信息 |
NtUserGetMenuIndex/NtUserGetMenuItemRect |
L6088-6204 | 索引/矩形 API |
NtUserHiliteMenuItem/NtUserDrawMenuBarTemp |
L6210-6298 | 高亮/临时绘制 |
NtUserMenuItemFromPoint/NtUserPaintMenuBar |
L6303-6391 | 命中/绘制 |
NtUserRemoveMenu/NtUserSetMenu |
L6397-6460 | 移除/挂接 API |
NtUserSetMenuContextHelpId/NtUserSetMenuDefaultItem/NtUserSetMenuFlagRtoL |
L6465-6536 | 菜单属性 API |
NtUserThunkedMenuInfo/NtUserThunkedMenuItemInfo |
L6541-6623 | 通用信息通道 |
NtUserTrackPopupMenuEx |
L6634-6699 | TrackPopupMenuEx 系统调用 |
11.2 配套文件
| 文件 | 内容 |
|---|---|
win32ss/user/ntuser/menu.h |
MENUSTATE/ROSMENUINFO/ROSMENUITEMINFO、内部函数声明(L125-180) |
win32ss/include/ntuser.h |
ITEM(L370)、MENULIST(L395)、MENU(L415)、POPUPMENU(L441)、MNF_*(L401-410) |
win32ss/user/user32/windows/menu.c |
user32 侧:POPUPMENU_builtin_class(L48)、MENU_FindItem(L83)、MENU_ParseResource(L410)、MENUEX_ParseResource(L455)、MENU_mnu2mnuii(L521)、AppendMenu(L676/714)、CreateMenu(L829)、GetMenu 系(L867-975)、GetMenuState(L1038)、GetMenuString(L1067/1102)、GetSubMenu(L1140)、LoadMenuIndirectW(L1358)、ModifyMenu(L1415/1455)、SetMenu(L1489)、SetMenuItemInfo(L1543/1591)、TrackPopupMenu(L1644) |
win32ss/user/user32/include/ntwrapper.h |
NtUserxCreateMenu(L551)、NtUserxCreatePopupMenu(L556)、NtUserxDrawMenuBar(L778)、NtUserxSetMenuBarHeight(L816) |
win32ss/user/user32/user32.spec |
CreateMenu(L100)、DestroyMenu(L161)、GetMenu(L324)、SetMenu(L662)、TrackPopupMenu(L743) |
win32ss/user/ntuser/simplecall.c |
NtUserCallNoParam(L59,CREATEMENU L68/CREATEMENUPOPUP L72)、NtUserCallHwndLock DRAWMENUBAR(L611) |
win32ss/user/ntuser/defwnd.c |
SC_MOUSEMENU/SC_KEYMENU(L192-203)、WM_CANCELMODE→MENU_EndMenu(L1070) |
win32ss/user/ntuser/nonclient.c |
NC_Paint 调 MENU_DrawMenuBar(L1073/L1252/L1358) |
win32ss/user/ntuser/callback.c |
co_IntCallLoadMenu(L903) |
sdk/include/psdk/winuser.h |
MENUITEMINFOW(L3367)、MENUINFO(L3842)、MF_(L128-153)、MIIM_ /MIM_/MNS_ /MFT_/MFS_ (L732-771)、TPM_(L2412-2438)、MNC_(L2602-2605) |
11.3 本册知识点速查
- 菜单是内核对象 (TYPE_MENU),分配在桌面堆,
rgItems是动态数组。 - 创建:CreateMenu/CreatePopupMenu → NtUserCallNoParam → UserCreateMenu → IntCreateMenu。
- 填充:所有"改项"调用汇聚到 NtUserThunkedMenuItemInfo(bInsert 分流插入/修改)→ UserInsertMenuItem/UserMenuItemInfo → IntInsertMenuItem/IntMenuItemInfo。
- 查找:MENU_FindItem 是心脏,MF_BYCOMMAND 深度优先递归子菜单树,MF_BYPOSITION O(1) 索引。
- 挂窗 :IntSetMenu 写
Wnd->IDMenu与Menu->hWnd,一菜单一窗口互斥。 - 移除:IntRemoveMenuItem 左移覆盖 + ReAlloc 收缩;DeleteMenu 与 RemoveMenu 只差 bRecurse。
- 系统菜单:SYSMENU 资源模板 → 魔法位图 → IntCloneMenu 每窗口一份;应用拿到的是 SC_* 子菜单。
- 绘制:DrawMenuBar 只发 SWP_FRAMECHANGED,真正绘制在 WM_NCPAINT 的 MENU_DrawMenuBar → IntDrawMenuBarTemp → MENU_DrawMenuItem。
- 查询类 API(GetMenuState/GetMenuString/GetSubMenu/GetMenuItemID/GetMenuItemCount)在 user32 直读共享桌面堆,不经过系统调用。
- 安全:跨模式拷贝(MmCopyFrom/ToCaller)、SEH 探测、会话校验、引用计数(子菜单插入 +1)、MENU_depth 防环、cbSize 版本兼容。
本册完。菜单循环(MENU_TrackMenu、按键/鼠标导航、子菜单展开、WM_MENUSELECT 消息交互)见《ReactOS 窗口系统分析(12):菜单(下)--- 循环与跟踪》。