ReactOS 窗口系统分析(11):菜单(上)— 对象与构造 — menu.c

系列:窗口系统三大主线之窗口管理 · 第 11 册

对象文件:win32ss/user/ntuser/menu.c(6699 行,拆两册,本册为对象与构造

配套头文件:win32ss/user/ntuser/menu.hwin32ss/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 的职责归纳为五条主线:

  1. 菜单对象PMENU/PITEMMENUINFO 包含菜单样式、项数组;菜单内存分配自桌面堆(Desktop Heap)。
  2. 菜单构造CreateMenu/CreatePopupMenu/AppendMenu/InsertMenuItem/LoadMenu(用户态解析资源后逐项插入)。
  3. 菜单栏IntSetMenu 把菜单挂到窗口,MENU_DrawMenuBar 绘制顶层菜单栏。
  4. 菜单循环TrackPopupMenuEx(NtUserTrackPopupMenuEx L6634)进入模态菜单循环(本册仅挂起,下册详述)。
  5. 消息交互: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/SetMenuItemInfoInsertMenuItemModifyMenuAppendMenu user32 组装后 → NtUserThunkedMenuItemInfo 单一通用入口(bInsert 区分插入/修改)
Get/SetMenuInfo user32 → NtUserThunkedMenuInfo 单一通用入口
DestroyMenu/RemoveMenu/DeleteMenu/EnableMenuItem/CheckMenuItem/GetSystemMenu/SetMenu user32 → 对应 NtUser* 系统调用 真系统调用

这解释了为什么 menu.c 里见不到 NtUserGetMenuStateNtUserGetMenuStringNtUserModifyMenuNtUserCreateMenu 这些名字------它们不存在 。内核只保留 IntGetMenuStateIntGetSubMenuIntFindSubMenuIntGetMenuItemRect 等内部函数供 win32k 自身(系统菜单、菜单栏信息、自绘回调)使用,用户态应用一律走 user32 的直读路径或通用 thunk 入口。这个"共享桌面堆 + 少量通用入口"的设计是 ReactOS 菜单子系统与 Windows 内核菜单实现的重要差异点。


2. 设计动机

2.1 菜单资源 vs 运行时菜单

Windows 的菜单有两种来源:

  • 编译期资源 :应用在 .rc 文件里用 MENU/MENUEX 模板声明,LoadMenu/LoadMenuIndirect 在运行时解析模板,逐项插入生成一个普通 HMENU。
  • 运行时构造 :应用调用 CreateMenu/CreatePopupMenu 拿空菜单,再用 AppendMenu/InsertMenuItem 等逐项填充。

ReactOS 对两种来源走同一条内核路径:资源解析完全发生在 user32MENU_ParseResource/MENUEX_ParseResource),内核只提供"创建空对象 + 插入项"的原子操作。这样内核菜单对象保持纯净,不需要理解资源格式。

2.2 菜单对象为何必须是内核对象

菜单句柄 HMENU 是 USER 对象(TYPE_MENU),理由是:

  1. 进程无关性 :一个菜单可以被挂到多个窗口,甚至被 SetMenu 到别的进程创建的窗口(经 handle 复制语义),需要内核对象管理器统一引用计数。
  2. 安全:菜单项文本、子菜单指针都在桌面堆中,用户态无法伪造指针(只能通过系统调用或共享只读映射访问)。
  3. 窗口联动 :菜单与窗口(Wnd->IDMenuWnd->SystemMenu)相互引用,销毁窗口时要能反向清理菜单;销毁菜单时要能通知窗口重绘(IntEnableMenuItem 中 SC_* 项变化 → UserPaintCaption)。
  4. 自绘回调:菜单需要向拥有窗口发送 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. 核心数据结构

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)。
  • fStatefType 在同一个 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 还给进程(IntDestroyMenu L287 有 !(item->fState & MFS_HBMMENUBMP) 判断)。
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),保证向后兼容。

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 0x10000TF_SUSPENDPOPUP 0x20000TF_SKIPREMOVE 0x40000
  • 内部 TPM 标志(L88-90):TPM_INTERNAL 0xF0000000TPM_BUTTONDOWN 0x40000000TPM_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_POPUPCreateMenu 创建的菜单栏对象不带;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;
}

IntGetMenuMENU_TrackMouseMenuBar(L4488)、MENU_DoNextMenu(L3794/L3832)等菜单循环代码使用;get_win_sys_menuWnd->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;   /* 可能返回"子菜单项本身"作为后备 */
}

三个要点:

  1. MF_BYPOSITION 直接索引 rgItems[*nPos],越界返回 NULL。
  2. MF_BYCOMMAND深度优先递归 :先深入子菜单树找 wID;若子菜单树没有而父级项本身 wID 匹配,把该弹出项 作为后备返回。*pmenu 可能被改写为子菜单指针------调用方据此知道"找到了子菜单里的项"。
  3. 这正是 GetMenuItemID(hMenu, nPos)MENU_FindItem 在 user32 侧的同名实现(user32 menu.c L83)共用的语义,保证命令查找能穿透子菜单层级。

MENU_FindItem 的调用方几乎覆盖所有"按 ID/位置操作项"的函数:IntGetMenuStateIntGetSubMenuIntRemoveMenuItemIntMenuItemInfoIntEnableMenuItemIntCheckMenuItemIntGetMenuItemRectNtUserMenuItemFromPoint 等。

相关: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 != NULLMmCopyToCallermin(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)除反向赋值外还有两点:

  1. MIM_APPLYTOSUBMENUS(L925-936):递归对每个子菜单应用同样的设置。
  2. 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;
}

四条规则:

  1. 子窗口禁挂菜单栏(WS_CHILD 且非 WS_POPUP)。
  2. 同句柄幂等Changed 为 FALSE 直接成功返回。
  3. 互斥占用NewMenu->hWnd 非空表示已被别的窗口占用,SetMenu 失败(ERROR_INVALID_MENU_HANDLE)。hWnd 在这里充当"占用标记 + 反向引用"。
  4. 所有权转移 :旧菜单 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|fStateDesktopPtrToUser 把桌面堆指针换算成用户态可读的共享映射地址。内核侧对应的 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 回拷,供 user32 GetMenuItemRect(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_CalcItemSize L1943);
  • 两个字体都设为 GDI_OBJ_HMGR_PUBLIC(公开对象,跨进程共享句柄);
  • co_IntSetupOBM():初始化系统内置位图(OBI_UPARROW/OBI_DNARROW/OBI_MNARROW 等),供滚动箭头与弹出箭头绘制(MENU_DrawScrollArrows L2176、MENU_CalcItemSize L1836 读取 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_InitTrackingMENU_ShowPopupMENU_TrackMenu 模态循环 → MENU_ExitTracking → 销毁弹出窗口、发 WM_UNINITMENUPOPUP。本册只挂起,循环内部(MENU_TrackMenu L4050、按键处理 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->hWndspwndNotify)时,置 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_BORDERWS_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_InitPopup L2837-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):带自定义字体/菜单/矩形的临时绘制入口(user32 DrawMenuBarTemp 用,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 修改都落在这个子菜单上。

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_InitSysMenuPopup L1377 的置灰逻辑互补)。
  • 外壳(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/iMaxTopMENU_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)按命中区域选菜单:HTSYSMENUIntGetSystemMenu(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_GetSystemMenu L5390 移除 / MENU_InitSysMenuPopup L1377 置灰)。

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_GetSystemMenu L5333/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 等)维护窗口菜单与系统菜单切换(MDISetMenu mdi.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 本册知识点速查

  1. 菜单是内核对象 (TYPE_MENU),分配在桌面堆,rgItems 是动态数组。
  2. 创建:CreateMenu/CreatePopupMenu → NtUserCallNoParam → UserCreateMenu → IntCreateMenu。
  3. 填充:所有"改项"调用汇聚到 NtUserThunkedMenuItemInfo(bInsert 分流插入/修改)→ UserInsertMenuItem/UserMenuItemInfo → IntInsertMenuItem/IntMenuItemInfo。
  4. 查找:MENU_FindItem 是心脏,MF_BYCOMMAND 深度优先递归子菜单树,MF_BYPOSITION O(1) 索引。
  5. 挂窗 :IntSetMenu 写 Wnd->IDMenuMenu->hWnd,一菜单一窗口互斥。
  6. 移除:IntRemoveMenuItem 左移覆盖 + ReAlloc 收缩;DeleteMenu 与 RemoveMenu 只差 bRecurse。
  7. 系统菜单:SYSMENU 资源模板 → 魔法位图 → IntCloneMenu 每窗口一份;应用拿到的是 SC_* 子菜单。
  8. 绘制:DrawMenuBar 只发 SWP_FRAMECHANGED,真正绘制在 WM_NCPAINT 的 MENU_DrawMenuBar → IntDrawMenuBarTemp → MENU_DrawMenuItem。
  9. 查询类 API(GetMenuState/GetMenuString/GetSubMenu/GetMenuItemID/GetMenuItemCount)在 user32 直读共享桌面堆,不经过系统调用。
  10. 安全:跨模式拷贝(MmCopyFrom/ToCaller)、SEH 探测、会话校验、引用计数(子菜单插入 +1)、MENU_depth 防环、cbSize 版本兼容。

本册完。菜单循环(MENU_TrackMenu、按键/鼠标导航、子菜单展开、WM_MENUSELECT 消息交互)见《ReactOS 窗口系统分析(12):菜单(下)--- 循环与跟踪》。

相关推荐
码行山野赴时序归途5 小时前
算法效率的度量(上):时间复杂度中的对数思维
c语言·数据结构·算法
16月6日-晴5 小时前
Java面向对象进阶—多态
java·开发语言
冯汉栩6 小时前
Swift Control RollingCountdownView(动画倒计时)
开发语言·ios·swift
lisanmengmeng6 小时前
搭建elk环境并接入frostmourne,实现监控报警效果(五)
开发语言·elk·日志·日志监控
oh,huoyuyan6 小时前
JS 动态网页抓不到?试试火车采集器网页抓取工具
开发语言·javascript·ecmascript
风流 少年7 小时前
hutool
java·服务器·开发语言
单片机仿真设计7 小时前
【proteus仿真】基于 STM32 的智能垃圾桶设计(仿真图+程序)
c语言·stm32·stm32单片机·proteus·毕设·仿真
一木 之林7 小时前
四、STL容器与数据结构
开发语言·数据结构·c++
QX_hao7 小时前
【Go】--Cobra-cli的用法
开发语言·后端·golang
艾醒(AiXing-w)7 小时前
LangChain 1.0 入门(三):稳定性双核心——重试机制+速率限速器参数详解与实战
开发语言·langchain·php