菜单MenuItem 与 ContextMenu 控件调用文档
命名空间:CConUI
依赖:System.Windows.Forms、System.Drawing、System.Drawing.Design、CConPlus(CConPlus.Enum.ImageSizeMode、CConPlus.GraphicsC、CConPlus.Windows.API.USER32)
本程序集公开 2 个类:
| 类名 | 可见性 | 基类 | 作用 |
|---|---|---|---|
MenuItem | public | object | 单个菜单项(含子项、分隔线、快捷键等) |
ContextMenu | public | System.ComponentModel.Component | 完全自绘的上下文菜单 |
一、MenuItem 菜单项
1.1 作用
表示上下文菜单中的单个条目,可作为:
普通可点击项
带快捷键提示的项
分隔线
带多级子菜单的父项
禁用项 / 隐藏项
层级结构通过
Items(List<MenuItem>)实现,可无限嵌套。
1.2 构造函数
public MenuItem()
创建一个空菜单项(
Text="",其余使用默认值)。
var item = new MenuItem(); item.Text = "打开";
public MenuItem(string text)
| 参数 | 类型 | 说明 |
|---|---|---|
text | string | 菜单项显示文本;为 null 时存为 "" |
var item = new MenuItem("复制");public MenuItem(string text, EventHandler onClick)
| 参数 | 类型 | 说明 |
|---|---|---|
text | string | 显示文本;为 null 时存为 "" |
onClick | EventHandler | 点击事件处理器;null 时不订阅 |
var item = new MenuItem("退出", (s, e) => Application.Exit());public MenuItem(bool isSeparator)
| 参数 | 类型 | 说明 |
|---|---|---|
isSeparator | bool | true 表示创建分隔线 |
var sep = new MenuItem(true);
1.3 静态方法
public static MenuItem CreateSeparator()
| 项目 | 说明 |
|---|---|
| 参数 | 无 |
| 返回 | 一个新的分隔线菜单项(内部等价 new MenuItem(true)) |
| 作用 | 便捷创建分隔线 |
var sep = MenuItem.CreateSeparator();
1.4 公开属性
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
Text | string | "" | 显示文本;分隔线时忽略 |
ShortcutKeyDisplayString | string | "" | 快捷键显示文本(如 "Ctrl+C");仅显示不注册快捷键;有子菜单时不显示(右侧改为箭头) |
Flag | string | "" | 自定义标识字符串,例如命令名、分组名、类型标记 |
Enabled | bool | true | 是否启用;禁用时显示为禁用色且点击不响应 |
Visible | bool | true | 是否可见;不可见时不占位置、不绘制、不参与命中 |
IsSeparator | bool | false | 是否为分隔线;为 true 时忽略 Text、ShortcutKeyDisplayString 等 |
ClickableWithSubMenu | bool | false | 拥有子菜单时是否仍可被点击;true 时点击同时触发展开和 Click |
Items | List<MenuItem> | 空列表 | 子菜单集合(只读属性,但返回 List 对象本身,可直接 Add/Remove/Clear) |
HasSubMenu | bool | 只读 | 是否拥有至少一个可见子项;为 true 时才绘制右箭头并允许展开 |
IsClickable | bool | 只读 | 综合判定是否可点: ① 分隔线 → false ② Enabled=false → false③ 有子菜单且 ClickableWithSubMenu=false → false④ 否则 → true |
Tag | object | null | 自定义数据;不参与设计器序列化 |
var openItem = new MenuItem("打开");
openItem.ShortcutKeyDisplayString = "Ctrl+O";
openItem.Flag = "cmd.open";
openItem.Tag = 12345;1.5 公开事件
public event EventHandler Click
当菜单项被真正点击时触发。
触发时机:由
ContextMenu在鼠标左键按下与松开都命中同一项时调用,或通过PerformClick()手动触发。
item.Click += (s, e) => MessageBox.Show("点击了:" + ((MenuItem)s).Text);1.6 公开方法
public void PerformClick()
| 项目 | 说明 |
|---|---|
| 参数 | 无 |
| 作用 | 手动触发 Click 事件 |
通常在
ContextMenu内部使用;外部若需模拟点击也可以调用(不会关闭菜单,需要自己再调用ContextMenu.Close())。
myMenuItem.PerformClick(); contextMenu.Close();
public MenuItem AddSubItem(MenuItem item)
| 项目 | 说明 |
|---|---|
| 参数 | item:要添加的子菜单项;null 时忽略 |
| 返回 | 传入的 item(便于链式使用);传入 null 时返回 null |
| 作用 | 向当前项的 Items 追加一个子项 |
var file = new MenuItem("文件");
file.AddSubItem(new MenuItem("新建", (s, e) => NewFile()));public MenuItem AddSubItem(string text)
| 项目 | 说明 |
|---|---|
| 参数 | text:子菜单项显示文本 |
| 返回 | 新建的子菜单项(无点击事件) |
| 作用 | 便捷添加一个只有文本的子项 |
var file = new MenuItem("文件");
MenuItem itemNew = file.AddSubItem("新建");
itemNew.Click += (s, e) => { /* ... */ }; // 事后补点击事件public MenuItem AddSubItem(string text, EventHandler onClick)
| 项目 | 说明 |
|---|---|
| 参数 | text:子菜单文本onClick:点击事件处理器(可为 null) |
| 返回 | 新建的子菜单项 |
var file = new MenuItem("文件");
file.AddSubItem("打开", (s, e) => OpenFile());public MenuItem AddSeparator()
| 项目 | 说明 |
|---|---|
| 参数 | 无 |
| 返回 | 新建的分隔线子项 |
| 作用 | 向当前项的 Items 追加一条分隔线 |
file.AddSubItem("保存", Save);
file.AddSeparator(); // 分隔线
file.AddSubItem("退出", Exit);public override string ToString()
| 项目 | 说明 |
|---|---|
| 返回 | 分隔线返回 "(分隔线)",其他返回 Text |
| 用途 | 调试输出、日志 |
1.7 常见用法示例
单个菜单项
var item = new MenuItem("复制")
{
ShortcutKeyDisplayString = "Ctrl+C",
Flag = "cmd.copy",
Tag = CopyAction
};
item.Click += (s, e) => Clipboard.SetText("Hello");带子菜单
var fileMenu = new MenuItem("文件");
fileMenu.AddSubItem("新建", (s, e) => NewFile());
fileMenu.AddSubItem("打开", (s, e) => OpenFile());
fileMenu.AddSeparator();
fileMenu.AddSubItem("退出", (s, e) => Application.Exit());分隔线 + 禁用项
var root = new List<MenuItem>
{
new MenuItem("刷新", (s, e) => Refresh()),
new MenuItem(true), // 分隔线
new MenuItem("粘贴") { Enabled = false }, // 禁用项
new MenuItem("隐藏项") { Visible = false } // 不显示
};二、ContextMenu 上下文菜单
2.1 作用
完全自绘的右键上下文菜单:
支持多级子菜单、键盘导航、屏幕边缘自动调整位置;
支持纯色背景 + 可选背景图片(可设置是否画在边框内);
支持单列 / 多列布局、固定宽度;
所有颜色支持 ARGB(可在属性面板通过
ArgbColorEditor可视化编辑);弹出时会在屏幕外点击左键 / 按 ESC / 按 Enter 时自动关闭。
2.2 构造
继承自 Component,无自定义构造函数,直接用默认无参构造:
var menu = new ContextMenu(); // 可放入容器管理(例如 Form 的 components 容器)
也可以通过 Visual Studio 工具箱拖入(
[ToolboxItem(true)])。
2.3 公开事件
| 事件 | 类型 | 说明 |
|---|---|---|
MenuOpened | EventHandler | 菜单打开后触发 |
MenuClosed | EventHandler | 菜单关闭后触发(该类默认事件,双击组件即生成) |
ItemClicked | EventHandler<MenuItem> | 某个菜单项被点击时触发,e 为被点击的 MenuItem(不是 EventArgs) |
menu.MenuOpened += (s, e) => Console.WriteLine("菜单打开");
menu.MenuClosed += (s, e) => Console.WriteLine("菜单关闭");
menu.ItemClicked += (s, item) =>
{
Console.WriteLine($"点击项:{item.Text}, Flag={item.Flag}");
// item.Click 也会独立触发(先 Click 后 ItemClicked)
};注意
ItemClicked是EventHandler<MenuItem>,其e直接是MenuItem对象,与EventArgs无关。
2.4 公开属性
2.4.1 外观(Category:CCon_外观)
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
MenuFont | Font | Microsoft YaHei, 9pt | 菜单字体。设置 null 会被忽略 |
ShowShortcutKeys | bool | true | 是否显示快捷键文本;false 时不绘制 ShortcutKeyDisplayString |
menu.MenuFont = new Font("Segoe UI", 9.5f);
menu.ShowShortcutKeys = true;2.4.2 颜色(Category:CCon_颜色)
全部支持 ARGB(含透明度);在属性面板中显示为色块预览,点击可弹出 ARGB 选择器(
ArgbColorEditor)。
| 属性 | 默认值 | 说明 |
|---|---|---|
MenuBackColor | (45, 45, 48) | 菜单背景色(当绘制图片背景时,作为铺底色) |
MenuBorderColor | (80, 80, 84) | 菜单边框色 |
ItemTextColor | (220, 220, 220) | 普通菜单项文本色 |
ShortcutTextColor | (140, 140, 140) | 普通菜单项快捷键文本色 |
HoverBackColor | (62, 62, 66) | 悬停背景色。支持 半透明,会与下层背景(纯色或图片)Alpha 混合 |
HoverTextColor | White | 悬停项文本色 |
HoverShortcutTextColor | White | 悬停项快捷键文本色 |
DisabledTextColor | (100, 100, 100) | 禁用项文本色 |
DisabledShortcutTextColor | (70, 70, 70) | 禁用项快捷键文本色 |
SeparatorColor | (60, 60, 64) | 分隔线颜色 |
menu.MenuBackColor = Color.FromArgb(230, 30, 30, 30); // 半透明深灰 menu.HoverBackColor = Color.FromArgb(180, 0, 120, 215); // 半透明蓝
2.4.3 尺寸(Category:CCon_尺寸)
| 属性 | 类型 | 默认 | 范围 | 说明 |
|---|---|---|---|---|
ItemHeight | int | 24 | ≥12 | 普通菜单项高度(像素) |
SeparatorHeight | int | 7 | ≥2 | 分隔线高度(像素) |
BorderThickness | int | 1 | ≥0 | 边框粗细;0 表示无边框 |
LeftPadding | int | 10 | ≥0 | 菜单项文本左内边距 |
RightPadding | int | 3 | ≥0 | 菜单项文本右内边距 |
ShortcutGap | int | 8 | ≥0 | 主文本与快捷键/箭头之间的最小间距 |
MinWidth | int | 60 | ≥20 | 菜单最小宽度;仅在单列自动宽度模式下生效 |
FixedWidth | bool | false | — | true 时使用 MenuWidth 作为菜单宽度,并强制单列;false 时按内容自动计算宽度 |
MenuWidth | int | 200 | ≥20 | 固定宽度值;仅在 FixedWidth=true 时生效 |
menu.ItemHeight = 28; menu.SeparatorHeight = 8; menu.FixedWidth = true; menu.MenuWidth = 240;
2.4.4 分列(Category:CCon_分列)
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
EnableColumnLimit | bool | false | 启用单列项目数限制;超限自动分列显示。FixedWidth=true 时忽略(强制单列) |
MaxItemsPerColumn | int | 8 | 单列最多显示项目数,≥1;仅在 EnableColumnLimit=true 时生效 |
menu.EnableColumnLimit = true; menu.MaxItemsPerColumn = 10; // 超过 10 项自动分成多列
分列时,每列独立计算宽度;列与列之间无间隙(按列宽累加)。
2.4.5 图片背景(Category:CCon_图片背景)
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
UseImageBackground | bool | false | 是否使用图片作为背景 |
ImageBackground | Image | null | 背景图片 |
ImageBackgroundLayout | CConPlus.Enum.ImageSizeMode | Tile | 背景图片显示方式(支持 Tile 平铺 / 拉伸 / 居中 / 缩放等,依 CConPlus.GraphicsC.DrawImageInRect 的实现) |
DrawImageInsideBorder | bool | true | 是否将图片绘制在边框内部:true:先画边框,再在边框内侧绘制图片(图片不覆盖边框)false:先画图片铺满客户区,再画边框(边框盖住图片边缘) |
menu.UseImageBackground = true;
menu.ImageBackground = Image.FromFile("bg.png");
menu.ImageBackgroundLayout = CConPlus.Enum.ImageSizeMode.Stretch;
menu.DrawImageInsideBorder = true;2.4.6 数据(Category:隐藏 / Browsable(false))
| 属性 | 类型 | 说明 |
|---|---|---|
Items | List<MenuItem> | 根级菜单项集合。只读属性,但返回 List 本身,可直接 Add / Remove / Clear |
IsOpen | bool | 只读,菜单当前是否处于打开状态 |
LastClickedItem | MenuItem | 只读,最近一次被点击的菜单项;从未点击或本次是自动关闭时可能为 null |
menu.Items.Add(new MenuItem("复制", (s, e) => Copy()));
menu.Items.Add(MenuItem.CreateSeparator());
menu.Items.Add(new MenuItem("粘贴", (s, e) => Paste()));
if (menu.IsOpen) menu.Close();
if (menu.LastClickedItem != null)
Console.WriteLine("上次点击了:" + menu.LastClickedItem.Text);2.5 公开方法
public void Show(Point screenLocation)
| 参数 | 类型 | 说明 |
|---|---|---|
screenLocation | Point | 屏幕坐标(不是控件坐标) |
| 返回 | 说明 |
|---|---|
void | 无返回值 |
| 项目 | 说明 |
|---|---|
| 作用 | 在指定屏幕坐标弹出菜单(未指定 owner) |
| 特殊行为 | ① 会先调用 Close() 关闭之前打开的菜单;② 会重置点击状态(_lastClickedIndex = -1、_lastClickedItem = null);③ 若无任何可见根项,则直接返回不弹出;④ 自动调整位置防止超出屏幕工作区 |
// 右键菜单场景:将鼠标客户区坐标转为屏幕坐标
void panel_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
menu.Show(panel1.PointToScreen(e.Location));
}public void Show(Control owner, Point screenLocation)
| 参数 | 类型 | 说明 |
|---|---|---|
owner | Control | 所有者控件;用于判断菜单应在哪个屏幕上工作(Screen.FromControl(owner))。可为 null(等价于单参数重载) |
screenLocation | Point | 屏幕坐标 |
| 返回 | 说明 |
|---|---|
void | 无返回值 |
| 项目 | 说明 |
|---|---|
| 作用 | 同单参数重载,但通过 owner 确定屏幕范围,多显示器场景下更准确 |
| 特殊行为 | 与单参数版本一致;此外会调用 USER32.SetForegroundWindow 和 Activate() 确保弹窗获得焦点 |
menu.Show(this, this.PointToScreen(e.Location));
位置自动调整逻辑:
若
x + 菜单宽度 > 屏幕右边缘,则x = 右边缘 - 宽度若
y + 菜单高度 > 屏幕下边缘,则y = 下边缘 - 高度最后兜底,保证
x、y不超出屏幕工作区左/上边界
public void Close()
| 项目 | 说明 |
|---|---|
| 参数 | 无 |
| 返回 | void |
| 作用 | 关闭当前弹出的菜单以及所有层级的子菜单 |
| 副作用 | ① 若本轮未通过点击触发关闭(即自动关闭、外部调用、ESC 等),会清空 _lastClickedIndex = -1、_lastClickedItem = null;② 若本轮已通过点击触发关闭(即点击项后由 NotifyItemClicked 关闭),则保留索引与项;③ 触发 MenuClosed |
| 幂等 | _popup == null 时直接返回,无副作用 |
menu.Close();
程序退出、切换窗体或用户点击外部区域时应显式调用
Close()(内部全局鼠标监视器也会自动触发关闭,但显式调用更保险)。
public int GetClickedItemIndex()
| 项目 | 说明 |
|---|---|
| 参数 | 无 |
| 返回 | int:最近一次被点击的菜单项索引;未点击过或本次为自动关闭时返回 -1 |
| 索引规则 | 深度优先前序,从 0 开始,依次是「父项 → 其子项(递归)→ 下一父项」 |
索引示例:
根菜单: A (0) ← 索引 0 A1 (1) ← 索引 1 A2 (2) ← 索引 2 A2a (3) ← 索引 3 B (4) ← 索引 4 B1 (5) ← 索引 5
menu.ItemClicked += (s, item) =>
{
int idx = menu.GetClickedItemIndex();
Console.WriteLine($"点击项 {item.Text} 的索引:{idx}");
};public MenuItem GetItemByIndex(int index)
| 参数 | 类型 | 说明 |
|---|---|---|
index | int | 目标索引(使用与 GetClickedItemIndex 相同的编号规则) |
| 返回 | 说明 |
|---|---|
MenuItem | 对应菜单项;index < 0 或越界时返回 null |
MenuItem item = menu.GetItemByIndex(3); if (item != null) Console.WriteLine(item.Text);
public int IndexOf(MenuItem item)
| 参数 | 类型 | 说明 |
|---|---|---|
item | MenuItem | 要查找的菜单项(按引用比较) |
| 返回 | 说明 |
|---|---|
int | 该菜单项的索引;item 为 null 或未找到时返回 -1 |
int idx = menu.IndexOf(myMenuItem); // 未找到返回 -1
2.6 隐藏的继承成员
ContextMenu 继承自 Component,不是 Control,所以没有 BackColor、Font 等属性;外观通过上面列出的专用属性控制。Dispose 由 Component 提供,内部已重写并会关闭菜单、释放内部字体。
三、组合调用示例
示例 1:最简菜单
public partial class Form1 : Form
{
private ContextMenu _menu;
public Form1()
{
InitializeComponent();
_menu = new ContextMenu();
_menu.Items.Add(new MenuItem("打开", (s, e) => MessageBox.Show("打开")));
_menu.Items.Add(new MenuItem("保存", (s, e) => MessageBox.Show("保存")));
_menu.Items.Add(MenuItem.CreateSeparator());
_menu.Items.Add(new MenuItem("退出", (s, e) => Close()));
// 在某个控件(或窗体)上绑定右键事件
panel1.MouseUp += Panel1_MouseUp;
MouseUp += Panel1_MouseUp;
}
private void Panel1_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Right) return;
Control c = sender as Control;
_menu.Show(c, c.PointToScreen(e.Location));
}
}示例 2:多级子菜单
var fileMenu = new MenuItem("文件");
fileMenu.AddSubItem("新建", (s, e) => NewFile());
fileMenu.AddSubItem("打开", (s, e) => OpenFile());
var recent = fileMenu.AddSubItem("最近打开"); // 只有文本
recent.AddSubItem("a.txt", (s, e) => OpenFile("a.txt"));
recent.AddSubItem("b.txt", (s, e) => OpenFile("b.txt"));
fileMenu.AddSeparator();
fileMenu.AddSubItem("退出", (s, e) => Application.Exit());
_menu.Items.Add(fileMenu);示例 3:带背景图 + ARGB 悬停色
var menu = new ContextMenu();
menu.MenuFont = new Font("Microsoft YaHei", 10F);
menu.ItemHeight = 28;
menu.MenuBackColor = Color.FromArgb(240, 30, 30, 34);
menu.HoverBackColor = Color.FromArgb(180, 0, 120, 215);
menu.MenuBorderColor = Color.FromArgb(255, 80, 80, 84);
menu.UseImageBackground = true;
menu.ImageBackground = Image.FromFile("bg.png");
menu.ImageBackgroundLayout = CConPlus.Enum.ImageSizeMode.Tile;
menu.DrawImageInsideBorder = true;示例 4:分列显示(超长菜单)
var menu = new ContextMenu();
menu.EnableColumnLimit = true;
menu.MaxItemsPerColumn = 8; // 每列最多 8 项,超出自动分列
for (int i = 1; i <= 25; i++)
{
int n = i;
menu.Items.Add(new MenuItem("选项 " + n, (s, e) => MessageBox.Show("选中 " + n)));
}示例 5:处理点击与索引
menu.ItemClicked += (s, item) =>
{
int idx = menu.GetClickedItemIndex();
if (item.Flag == "cmd.exit") Close();
else MessageBox.Show($"item={item.Text} 索引={idx} flag={item.Flag}");
};
// 事后根据点击项反查索引:
MenuItem m = menu.GetItemByIndex(menu.GetClickedItemIndex());示例 6:ClickableWithSubMenu 让父项也能被点击
var tools = new MenuItem("工具");
tools.ClickableWithSubMenu = true; // 点击父项本身也会触发 Click
tools.Click += (s, e) => MessageBox.Show("直接点击了【工具】");
tools.AddSubItem("选项", (s, e) => MessageBox.Show("选项"));
tools.AddSubItem("扩展", (s, e) => MessageBox.Show("扩展"));
menu.Items.Add(tools);ClickableWithSubMenu = false(默认):点击父项只展开子菜单,不触发Click。ClickableWithSubMenu = true:点击父项同时触发展开和Click。
示例 7:禁用 / 隐藏 / 分隔线
var paste = new MenuItem("粘贴")
{
Enabled = false, // 禁用(灰显,点击无效)
ShortcutKeyDisplayString = "Ctrl+V"
};
var hidden = new MenuItem("隐藏项") { Visible = false }; // 完全不显示
var sep = MenuItem.CreateSeparator();
menu.Items.Add(new MenuItem("复制", (s, e) => Copy()));
menu.Items.Add(paste);
menu.Items.Add(hidden);
menu.Items.Add(sep);
menu.Items.Add(new MenuItem("全选", (s, e) => SelectAll()));四、公开成员速查表
MenuItem
| 成员 | 类型 | 说明 |
|---|---|---|
MenuItem() / MenuItem(string) / MenuItem(string, EventHandler) / MenuItem(bool) | 构造函数 | 4 个重载 |
CreateSeparator() | static 方法 | 创建分隔线 |
PerformClick() | 方法 | 手动触发 Click |
AddSubItem(MenuItem) | 方法 | 添加子项并返回 |
AddSubItem(string) | 方法 | 添加文本子项并返回 |
AddSubItem(string, EventHandler) | 方法 | 添加带点击事件的子项并返回 |
AddSeparator() | 方法 | 添加分隔线子项并返回 |
ToString() | override 方法 | 调试文本 |
Text / ShortcutKeyDisplayString / Flag | 属性 | 文本类 |
Enabled / Visible / IsSeparator / ClickableWithSubMenu | 属性 | 行为开关 |
Items | 属性(只读引用) | 子项集合 |
HasSubMenu / IsClickable | 属性(只读) | 状态判定 |
Tag | 属性 | 自定义数据 |
Click | 事件 | 点击时触发 |
ContextMenu
| 成员 | 类型 | 说明 |
|---|---|---|
Show(Point) | 方法 | 按屏幕坐标弹出 |
Show(Control, Point) | 方法 | 按屏幕坐标弹出(指定 owner) |
Close() | 方法 | 关闭所有层级的菜单 |
GetClickedItemIndex() | 方法 | 返回最近点击项索引(-1 表示未点击/自动关闭) |
GetItemByIndex(int) | 方法 | 按索引取项 |
IndexOf(MenuItem) | 方法 | 按项取索引(-1 表示未找到) |
MenuFont / ShowShortcutKeys | 属性 | 外观 |
MenuBackColor / MenuBorderColor / ItemTextColor / ShortcutTextColor / HoverBackColor / HoverTextColor / HoverShortcutTextColor / DisabledTextColor / DisabledShortcutTextColor / SeparatorColor | 属性(ARGB) | 颜色 |
ItemHeight / SeparatorHeight / BorderThickness / LeftPadding / RightPadding / ShortcutGap / MinWidth / FixedWidth / MenuWidth | 属性 | 尺寸 |
EnableColumnLimit / MaxItemsPerColumn | 属性 | 分列 |
UseImageBackground / ImageBackground / ImageBackgroundLayout / DrawImageInsideBorder | 属性 | 图片背景 |
Items / IsOpen / LastClickedItem | 属性(只读引用 / 只读) | 数据 |
MenuOpened / MenuClosed / ItemClicked | 事件 | 生命周期与点击 |
五、注意事项
坐标必须是屏幕坐标。
Show(Point)/Show(Control, Point)的参数是屏幕坐标,而不是控件客户区坐标。请使用control.PointToScreen(e.Location)转换。Show会先关闭旧菜单。如果你连续调用Show,之前打开的菜单会被自动关闭并触发MenuClosed。无可见项不会弹出。
Show会先调用HasVisibleItems(_items)检查根级是否有可见项,若全不可见则直接返回,MenuOpened不会触发。ItemClicked与MenuItem.Click的触发顺序:先触发被点击
MenuItem自身的Click事件再触发
ContextMenu.ItemClicked事件(e是MenuItem)索引与
LastClickedItem的保留/清空规则:通过点击项 → 内部调用
NotifyItemClicked→ 记录索引/项 → 关闭菜单时保留通过 ESC / 点击外部 / 显式
Close()关闭 → 清空(-1/null)因此如果你需要在
MenuClosed中判断用户到底有没有点击某项,可以看GetClickedItemIndex() >= 0或LastClickedItem != null。索引规则是深度优先前序。
GetClickedItemIndex()、GetItemByIndex(int)、IndexOf(MenuItem)三者共用同一套编号规则,可互相一致。HasSubMenu判断的是"至少有一个可见子项"。父项若只有不可见的子项(Visible=false),则不绘制箭头、不可展开,IsClickable也会按无子菜单处理。ClickableWithSubMenu:false(默认):有子菜单的项点击只展开,不触发Click;true:有子菜单的项点击同时展开并触发Click。键盘操作(自动开启,无需设置):
↑/↓:在当前层移动活动项(跳过不可见、禁用、分隔线)→:展开子菜单←:收起当前层子菜单Enter:点击当前活动项(若带子菜单则展开)Esc:关闭整个菜单链子菜单展开有 180ms 悬停延迟(避免鼠标经过父项时误开)。
FixedWidth = true时忽略EnableColumnLimit与MaxItemsPerColumn,强制单列显示。MinWidth仅在单列自动宽度模式下生效。多列时每列独立按内容测量,MinWidth不影响。HoverBackColor支持 Alpha,绘制时使用CompositingMode.SourceOver与下层内容(纯色 / 图片)混合。DrawImageInsideBorder语义:true(默认):边框 → 边框内侧图片 → 菜单项false:图片铺满整个客户区 → 边框盖住边缘 → 菜单项ContextMenu不是Control,不能直接放到窗体上作为子控件;它是Component,通过menu.Show(...)使用。设计器中可放入components容器以便自动释放。Dispose时会自动关闭菜单并释放内部MenuFont(不释放SystemFonts共享实例),因此请通过menu.Dispose()或using (var menu = new ContextMenu()) { ... }管理生命周期。弹窗会用
SetForegroundWindow+Activate获取焦点(依赖CConPlus.Windows.API.USER32),保证键盘事件能被弹窗捕获。ImageBackgroundLayout的类型是CConPlus.Enum.ImageSizeMode,需要引用CConPlus程序集;DrawImageInRect来自CConPlus.GraphicsC。如果你的项目没有CConPlus,需要在工程中添加引用。
转载请注明出处。