如何以最爽的方式创建 ContextMenu/Strip 右键菜单,不需要设计器

本文最后更新于:11 天前

前言

最近我在开发项目 PlainCEETimer 时,由于应用程序涉及到多个右键菜单的创建,在设计器里创建未免有些繁琐。今天就给大家分享一下如何以最爽的方式创建 ContextMenu/Strip 右键菜单,不需要设计器,欢迎在评论区分享您的观点及想法。顺便区分一下 ContextMenuContextMenuStrip。

如果你正在使用 .NET Framework 开发 WinForms 应用程序的话,你或许会觉得 ContextMenuStrip 的样式不美观,不贴合 Windows 原生的右键菜单样式。所以 System.Windows.Forms.ContextMenu 是一个不错的选择,而它底层其实就是 Windows 原生样式的右键菜单。不过这个类也只在 .NET Framework 存在,在 .NET (Core) 中已被移除。下图分别展示了 ContextMenuContextMenuStrip,各位觉得哪个更好看呢?

ContextMenu

ContextMenuStrip

好了,直接开始正题!

ContextMenu

先贴上示例代码以及效果图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Form1()
{
InitializeComponent();
this.ContextMenu = CreateNew
([
AddItem("测试"),
AddItem("测试"),
AddSubMenu("测试",
[
AddItem("测试"),
AddItem("测试"),
AddItem("测试", (_, _) => MessageBox.Show("Test"))
]),
AddSeparator(),
AddItem("测试")
]);
}

这是全套的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private ContextMenu CreateNew(MenuItem[] Items)
=> new(Items);

private MenuItem AddItem(string Text)
=> new(Text);

private MenuItem AddItem(string Text, EventHandler OnClickHandler)
=> new(Text, OnClickHandler);

private MenuItem AddSubMenu(string Text, MenuItem[] Items)
=> new(Text, Items);

private MenuItem AddSeparator()
=> new("-");

private ContextMenu Merge(ContextMenu Target, ContextMenu Reference)
{
Target.MergeMenu(Reference);
return Target;
}

ContextMenuStrip

同样也先贴上示例代码以及效果图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Form1()
{
InitializeComponent();
this.ContextMenuStrip = CreateNewStrip
([
AddStripItem("测试"),
AddStripItem("测试"),
AddSubStrip("测试",
[
AddStripItem("测试"),
AddStripItem("测试"),
AddStripItem("测试", (_, _) => MessageBox.Show("Test"))
]),
AddStripSeparator(),
AddStripItem("测试")
]);
}

这是全套的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private ContextMenuStrip CreateNewStrip(ToolStripItem[] Items)
{
var Strip = new ContextMenuStrip();
Strip.Items.AddRange(Items);
return Strip;
}

private ToolStripMenuItem AddStripItem(string Text)
=> new(Text);

private ToolStripMenuItem AddStripItem(string Text, EventHandler OnClickHandler)
=> new(Text, null, OnClickHandler);

private ToolStripMenuItem AddSubStrip(string Text, ToolStripItem[] SubItems)
{
var Item = new ToolStripMenuItem(Text);
Item.DropDownItems.AddRange(SubItems);
return Item;
}

private ToolStripSeparator AddStripSeparator()
=> new();

private ContextMenuStrip MergeStrip(ContextMenuStrip Target, ToolStripItem[] Reference)
{
Target.Items.AddRange(Reference);
return Target;
}

原理分析

原理也很简单,首先大家要知道创建菜单最重要的就是准备一个 MenuItem[]ToolStripItem[] 数组,该数组包括了这个右键菜单的菜单项,常见的也就是 普通菜单项 和 分割线。也就是说我们只需要通过编写一系列方法在这个数组里面返回 MenuItem 或 ToolStripItem 元素就大功告成了。怎么样,是不是感觉很爽!

对于微软广为推荐的 ContextMenuStrip,其 ToolStripItem 是各种菜单项的抽象基类,并派生出了更多类型的菜单,比如分隔线是 ToolStripSeparator,普通菜单项是 ToolStripMenuItem,它又是派生自 ToolStripDropDownItem,进而实现继承 ToolStripItem,如下图

也就是说它们本质上都是 ToolStripItem,直接放在 ToolStripItem[] 数组里是可行的。因此这又会带来其他问题。比如你想从一个 ContextMenuStrip 里面拿出一个菜单项并设置为选中状态,你可能会直接使用以下代码:(本文最后还有更好的方法)

1
MyContextMenuStrip.Items[0].Checked = true;

然而实际上前面那个索引器返回的是 ToolStripItem,是没有 Checked 这个属性的。正确方法应该是添加显式转换为 ToolStripMenuItem,然后就可以设置属性了:(若是其他类型的菜单需转换为相应的类型)

1
((ToolStripMenuItem)MyContextMenuStrip.Items[0]).Checked = true;

(上面扯远了) 另外在上文贴出的全套方法中,除了常用的添加菜单/分隔线以外,还有合并菜单的方法 MergeMergeStrip,分别适用于 ContextMenuContextMenuStrip。因为在实际开发中,我们有可能会在多个地方使用同样内容的菜单并加上一些特定区域的内容。比如在窗体中,我们可以创建 “设置”、”关于” 这样的菜单项,然后在托盘通知图标 (NotifyIcon) 的右键菜单中,除了有 “设置”、”关于” 以外,我们通常会加上 “退出” 等选项。这时合并两个菜单就显得非常有用了,大大降低了代码的重复。

然而并非直接合并那么简单,仍然需要对窗体和托盘图标各创建一个右键菜单的实例,否则在现有实例上合并的话,最后所有的右键菜单都变成同一个了。所以我们应该再创建一个方法来创建并返回一个通用的菜单的实例,使各实例独立出来,这里就以 ContextMenu 为例,ContextMenuStrip 如法炮制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private NotifyIcon MyNotifyIcon;

private ContextMenu BaseContextMenu() => CreateNew
([
AddItem("设置(&S)", ContextSettings_Click),
AddItem("关于(&A)", ContextAbout_Click)
]);

private void InitializeComponent2()
{
this.ContextMenu = BaseContextMenu();

MyNotifyIcon.ContextMenu = Merge(BaseContextMenu(), CreateNew
([
AddSeparator(),
AddItem("退出(&Q)", ContextQuit_Click)
]));
}

不过需要注意的是,ContextMenuMerge 方法传入的是两个 ContextMenu 实例。而 ContextMenuStripMergeStrip 方法传入的一个是目标 ContextMenuStrip 实例,另一个是 ToolStripItem[] 数组,也就是最终效果应该是这样:

1
2
3
4
5
this.ContextMenuStrip = MergeStrip(Target,
[
AddStripSeparator(),
AddStripItem("退出(&Q)", ContextQuit_Click)
]);

FAQ

Q: 动态创建菜单可用上述方法吗?

A: 理论上可以,原理类似于动态创建数组。不过更推荐操纵背后的 Items 集合。以从现有数据对象中将各数据添加到右键菜单中为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 如果是 ContextMenu
var Original = MyContextMenu.MenuItems[0].MenuItems;

// 如果是 ContextMenuStrip
var Original = ((ToolStripMenuItem)MyContextMenuStrip.Items[0]).DropDownItems;

foreach (var sth in Something)
{
var Item = new MenuItem() 或 ToolStripMenuItem()
{
Text = sth,
};

Item.Click += Items_Click;
Original.Add(Item);
}

Q: 怎么获取通过上述代码创建的右键菜单的某个菜单项的实例?

A: 这个大家可能不太熟悉,因为赋值语句本来就具有返回值,所以创建数组时可以将变量/字段名和值一起写入到表达式里面。就像这样:

1
2
3
4
5
6
7
8
int a;
int b;
int c;

var arr = new int[] { a = 1, b = 2, c = 3 };

// 新版 C# 语法糖可以用 集合表达式:
int[] arr = [ a = 1, b = 2, c = 3 ];

也就是说我们可以在创建数组时将元素赋值给一个字段或变量,这样就可以在其他地方调用这个菜单项了。比如:(个别特殊情况也可以使用索引器,前文有提到)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private MenuItem MenuItemDelete;
private ToolStripMenuItem StripItemDelete;

public Form1()
{
InitializeComponent();

this.ContextMenu = CreateNew
([
MenuItemDelete = AddItem("删除(&D)")
]);
MenuItemDelete.Enabled = false;

this.ContextMenuStrip = CreateNewStrip
([
StripItemDelete = AddStripItem("删除(&D)")
]);
StripItemDelete.Enabled = false;
}

// 注意:以上代码仅供演示,实际开发中不能同时存在 ContextMenu 和 ContextMenuStrip
// 若同时存在优先显示前者

注意

可以结合实际需求尝试自己开发出更多类似的方法以扩展功能

最后

由于时间有限,部分内容描述可能比较草率,还请各位积极指出不合适的地方,我将在之后逐个修正相关内容。

(*本文内容均为作者原创,如有雷同纯属巧合)

相关链接


如何以最爽的方式创建 ContextMenu/Strip 右键菜单,不需要设计器
https://wanghaonie.github.io/posts/95b4fdac793d/
作者
WangHaonie
发布于
2025-05-03 20:00:59
更新于
2025-05-03 20:32:10
许可协议