支持纵向和横向菜单,12种展示样式
类似控件:buttonsGroup
一、效果图




暗色模式下显示:

二、使用说明
NavigationMenuEx 导航菜单控件
控件简介
NavigationMenuEx 是一个功能强大的导航菜单控件,支持横向和纵向两种布局方式,提供多种图标和文字显示组合,支持多级子菜单和主题适配。
主要特性
- 双布局方向:支持横向(顶部导航)和纵向(侧边栏)布局
- 多种显示模式 :
- 仅文字
- 仅图标
- 图标左文字右(横向排列)
- 图标上文字下(纵向排列)
- 图标支持:支持 Image 图标和 SVG 图标(使用 SvgIconManager)
- 图标背景圆:支持为图标添加圆形背景,增强视觉效果
- 多级子菜单:支持嵌套子菜单,可展开/折叠
- 选中指示器:纵向布局时,选中项左侧显示竖线指示器
- 主题适配:实现 IThemeable 接口,支持亮色/暗色主题切换(默认关闭,需手动启用)
- 完整交互:支持鼠标悬停、点击选中、禁用状态
- 均匀分布 :支持横向/纵向自动平分空间(
ItemWidth=0或ItemHeight=0) - 灵活边距 :
MenuPadding控制控件边界内边距,ItemPadding控制菜单项内容和背景区域 - 内置滚动:纵向布局支持内置滚动条,当内容超出控件高度时可滚动查看
- 滚动条自定义:支持自定义滚动条颜色
- 横向子菜单:横向布局时自动以弹出窗口形式显示子菜单
- 焦点保持:点击子菜单项不会导致父窗体失去焦点
重要概念:布局方向与属性关系
控件的行为会根据 Orientation 属性(横向/纵向)而有所不同。理解这种差异对正确使用属性至关重要。
横向布局 (Horizontal)
- 控件高度由外部容器(如 Dock/Anchor)或手动设置 Size 决定
- 控件宽度由外部容器决定
- 菜单项水平排列
ItemWidth决定每个菜单项的宽度(0=自动平分)ItemHeight决定每个菜单项的绘制高度
纵向布局 (Vertical)
- 控件高度由菜单项数量自动计算(包括展开的子菜单),或由外部容器决定
- 控件宽度由外部容器决定
- 菜单项垂直排列
ItemWidth决定每个菜单项的宽度(0=充满,>0=居中固定宽度)ItemHeight决定每个菜单项的绘制高度
属性说明
布局属性
| 属性名 | 类型 | 默认值 | 横向布局效果 | 纵向布局效果 |
|---|---|---|---|---|
| Orientation | NavigationMenuOrientation | Horizontal | - | - |
| DisplayMode | NavigationMenuDisplayMode | IconLeftTextRight | - | - |
| ItemHeight | int | 64 | 0=自动平分控件高度,>0=固定高度 | 0=自动平分控件高度,>0=固定高度 |
| ItemWidth | int | 120 | 0=自动平分控件宽度,>0=固定宽度 | 0=充满可用宽度,>0=固定宽度并居中显示 |
| MenuPadding | Padding | 8 | 左右内边距 | 上下左右内边距 |
| ItemPadding | Padding | 4 | 背景和内容的内边距 | 背景和内容的内边距 |
| IconSize | int | 20 | 图标大小 | 图标大小 |
⚠️ 注意事项:
- ItemHeight/ItemWidth = 0 表示自动平分:所有可见菜单项平分控件的可用空间(宽度或高度)。
- ItemWidth = 0 在两种布局中的默认行为:横向是"平分",纵向是"充满"。
- 纵向布局的控件高度可以自动计算,包含:内边距 + 所有可见项高度 + 展开的子菜单高度 + 间隙/分割线高度。
- ItemHeight 只影响菜单项的绘制高度,不影响控件本身的高度。
- ItemPadding 同时影响背景和内容:背景绘制和内容区域都会应用 ItemPadding,使选中效果与内容区域一致。
- 纵向布局背景区域:选中/悬停状态的背景色块会正确考虑 MenuPadding 和 ItemPadding,左右两侧都会留有间隙,不会紧贴控件边框。
显示模式枚举
csharp
public enum NavigationMenuDisplayMode
{
TextOnly, // 仅文字
IconOnly, // 仅图标
IconLeftTextRight, // 图标在左,文字在右
IconTopTextBottom // 图标在上,文字在下
}
外观属性
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| BorderRadius | int | 6 | 圆角半径(0为直角) |
| ColorType | ColorType | Primary | 功能色类型(影响选中色和指示器颜色) |
| IndicatorColor | Color | #1890FF | 选中指示器颜色(纵向布局) |
| IndicatorWidth | int | 3 | 选中指示器宽度(纵向布局) |
| SubMenuIndent | int | 20 | 子菜单缩进距离(纵向布局) |
| ShowSubMenu | bool | true | 是否显示子菜单 |
子菜单属性
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| AutoExpandSubMenu | bool | true | 是否自动展开子菜单(悬停时) |
| SingleExpandMode | bool | false | 是否只允许同时展开一个子菜单 |
边框与分隔属性
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| ShowBorder | bool | true | 是否显示边框 |
| BorderWidth | int | 1 | 边框宽度(0为无边框) |
| BorderColor | Color | #E8E8E8 | 边框颜色 |
| ItemDividerColor | Color | #C8C8C8 | 菜单项分割线颜色 |
| ItemDividerWidth | int | 0 | 分割线宽度(0表示不显示,>0显示到边的分割线) |
| ItemGap | int | 6 | 菜单项间隙(>0时菜单项之间有间隙) |
分割线与间隙说明:
| 配置 | 效果 |
|---|---|
ItemGap = 0, ItemDividerWidth = 0 |
无分隔(默认) |
ItemGap > 0 |
间隙模式:菜单项之间有间隙,通过间隙显示控件背景色 |
ItemGap = 0, ItemDividerWidth > 0 |
分割线模式:菜单项之间显示到边的完整分割线 |
简单配置:
csharp
// 方式1:间隙模式(推荐)
menu.ItemGap = 4;
// 方式2:分割线模式
menu.ItemDividerWidth = 1;
滚动属性(纵向布局)
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| EnableScroll | bool | false | 是否启用内置滚动条(纵向布局时有效) |
| ScrollBarTrackColor | Color | 半透明灰 | 滚动条轨道背景色 |
| ScrollBarThumbColor | Color | 浅灰 | 滚动条滑块正常颜色 |
| ScrollBarThumbHoverColor | Color | 中灰 | 滚动条滑块悬停颜色 |
| ScrollBarThumbActiveColor | Color | 深灰 | 滚动条滑块按下颜色 |
滚动功能说明:
- 仅在纵向布局且
EnableScroll = true时生效 - 当菜单内容超出控件高度时,自动显示滚动条
- 支持鼠标滚轮滚动
- 滚动条默认自动隐藏/显示
使用示例:
csharp
// 启用内置滚动
var sidebar = new NavigationMenuEx
{
Orientation = NavigationMenuOrientation.Vertical,
EnableScroll = true, // 启用滚动
Dock = DockStyle.Left,
Width = 200
};
// 自定义滚动条颜色
sidebar.ScrollBarThumbColor = Color.FromArgb(180, 180, 180);
sidebar.ScrollBarThumbHoverColor = Color.FromArgb(150, 150, 150);
子菜单层级颜色
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| SubMenuBackColor | Color | Empty | 子菜单背景色(Empty表示自动计算,比一级菜单浅) |
| UseSubMenuLevelColor | bool | true | 是否使用层级颜色区分(一级深、二级浅) |
层级颜色说明:
- UseSubMenuLevelColor = true(默认):自动计算层级颜色,一级菜单使用控件背景色,二级及以上菜单使用更浅的背景色(每层增加15的亮度)
- SubMenuBackColor 不为 Empty:所有子菜单使用统一的自定义背景色
- 两种模式二选一:自动层级颜色或自定义统一颜色
建议配置:
csharp
// 方式1:自动层级颜色(推荐,一级深、二级浅,有层次感)
menu.UseSubMenuLevelColor = true; // 默认值
// 方式2:自定义子菜单背景色(所有子菜单统一颜色)
menu.SubMenuBackColor = Color.FromArgb(245, 245, 245); // 浅灰色
menu.UseSubMenuLevelColor = false;
横向子菜单(弹出窗口)
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| AutoExpandSubMenu | bool | true | 是否自动展开子菜单(悬停时弹出) |
| ShowSubMenu | bool | true | 是否显示子菜单 |
横向子菜单说明:
- 横向布局时,子菜单以弹出窗口形式显示在父菜单项下方
- 支持自动悬停展开(AutoExpandSubMenu = true)
- 支持鼠标平滑移动:从父菜单项移向子菜单时不会立即关闭
- 点击子菜单项不会导致父窗体失去焦点
使用示例:
csharp
// 横向菜单带子菜单
var menu = new NavigationMenuEx
{
Orientation = NavigationMenuOrientation.Horizontal,
Dock = DockStyle.Top,
Height = 48,
AutoExpandSubMenu = true // 悬停自动展开子菜单
};
// 添加带子菜单的菜单项
var fileItem = menu.Items.AddItem("文件", "file");
fileItem.SubItems.AddItem("新建", "file-add");
fileItem.SubItems.AddItem("打开", "folder-open");
fileItem.SubItems.AddSeparator();
fileItem.SubItems.AddItem("退出", "close");
// 处理点击事件
menu.ItemClick += (s, e) =>
{
if (e.IsSubItem)
MessageBox.Show($"点击了子菜单: {e.Item.Text}");
else
MessageBox.Show($"点击了主菜单: {e.Item.Text}");
};
主题属性
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| FollowGlobalTheme | bool | false | 是否跟随全局主题。默认 false,需手动设置为 true 才启用 |
注意: 设置 FollowGlobalTheme = true 后,控件会自动注册到主题管理器,跟随全局主题变化;设置为 false 时会自动注销。
使用示例
基本用法
csharp
// 创建横向导航菜单
var menu = new NavigationMenuEx
{
Orientation = NavigationMenuOrientation.Horizontal,
DisplayMode = NavigationMenuDisplayMode.IconLeftTextRight,
Dock = DockStyle.Top,
Height = 48 // 控件高度由 Dock/Height 决定,ItemHeight 只影响菜单项绘制高度
};
// 添加菜单项
menu.AddItem("首页", "home");
menu.AddItem("产品", "appstore");
menu.AddItem("关于", "info-circle");
// 绑定点击事件
menu.ItemClick += (s, e) =>
{
MessageBox.Show($"点击了: {e.Item.Text}");
};
this.Controls.Add(menu);
纵向侧边栏
csharp
// 创建纵向导航菜单(侧边栏)
var sidebar = new NavigationMenuEx
{
Orientation = NavigationMenuOrientation.Vertical,
DisplayMode = NavigationMenuDisplayMode.IconLeftTextRight,
Dock = DockStyle.Left,
Width = 200, // 纵向布局时,控件宽度由外部决定
IndicatorColor = Color.FromArgb(24, 144, 255),
IndicatorWidth = 3
};
// 添加带图标的菜单项
var item1 = sidebar.AddItem("仪表盘", "dashboard");
var item2 = sidebar.AddItem("用户管理", "user");
var item3 = sidebar.AddItem("系统设置", "setting");
// 添加带子菜单的项
var moreItem = new NavigationMenuItem
{
Text = "更多",
IconSvg = "more"
};
moreItem.SubItems.Add(new NavigationMenuItem { Text = "选项1", IconSvg = "option" });
moreItem.SubItems.Add(new NavigationMenuItem { Text = "选项2", IconSvg = "option" });
sidebar.Items.Add(moreItem);
this.Controls.Add(sidebar);
纵向布局固定宽度
csharp
// 纵向布局时,使用 ItemWidth 控制菜单项宽度(默认充满整个控件)
var sidebar = new NavigationMenuEx
{
Orientation = NavigationMenuOrientation.Vertical,
DisplayMode = NavigationMenuDisplayMode.IconTopTextBottom,
Dock = DockStyle.Left,
Width = 200,
ItemWidth = 120, // 菜单项固定120像素宽度,居中显示
ItemHeight = 64,
IconSize = 24
};
sidebar.AddItem("首页", "home");
sidebar.AddItem("分类", "appstore");
sidebar.AddItem("购物车", "shopping-cart");
sidebar.AddItem("我的", "user");
this.Controls.Add(sidebar);
设置内边距
csharp
// MenuPadding:控制菜单项与控件边界的距离
// 横向布局:设置左右内边距
var topMenu = new NavigationMenuEx
{
Orientation = NavigationMenuOrientation.Horizontal,
MenuPadding = new Padding(16, 0, 16, 0), // 左右16像素内边距
ItemWidth = 100,
Dock = DockStyle.Top,
Height = 48
};
// 纵向布局:设置上下内边距
var sidebar = new NavigationMenuEx
{
Orientation = NavigationMenuOrientation.Vertical,
MenuPadding = new Padding(8, 16, 8, 16), // 上下16像素,左右8像素内边距
Dock = DockStyle.Left,
Width = 200
};
// ItemPadding:控制菜单项内容与菜单项边界的距离(同时影响背景绘制区域)
var menuWithPadding = new NavigationMenuEx
{
Orientation = NavigationMenuOrientation.Horizontal,
MenuPadding = new Padding(8, 0, 8, 0), // 控件边界内边距
ItemPadding = new Padding(8, 4, 8, 4), // 菜单项内边距(背景和内容都会应用)
Dock = DockStyle.Top,
Height = 48
};
// 选中状态的背景只会填充应用了 ItemPadding 后的区域,四周留出间隙
图标上文字下模式
csharp
var menu = new NavigationMenuEx
{
Orientation = NavigationMenuOrientation.Horizontal,
DisplayMode = NavigationMenuDisplayMode.IconTopTextBottom,
ItemHeight = 64, // 需要更大的高度
IconSize = 24
};
menu.AddItem("首页", "home");
menu.AddItem("分类", "appstore");
menu.AddItem("购物车", "shopping-cart");
menu.AddItem("我的", "user");
仅图标模式
csharp
var iconMenu = new NavigationMenuEx
{
Orientation = NavigationMenuOrientation.Vertical,
DisplayMode = NavigationMenuDisplayMode.IconOnly,
ItemWidth = 48,
IconSize = 24
};
iconMenu.AddItem("", "home");
iconMenu.AddItem("", "search");
iconMenu.AddItem("", "setting");
靠右对齐菜单项
csharp
var menu = new NavigationMenuEx
{
Orientation = NavigationMenuOrientation.Horizontal,
ItemWidth = 120,
Dock = DockStyle.Top,
Height = 48
};
// 添加靠左的菜单项
menu.AddItem("首页", "home");
menu.AddItem("产品", "appstore");
menu.AddItem("关于", "info");
// 添加靠右的菜单项(如用户、设置等)
var userItem = new NavigationMenuItem
{
Text = "用户",
IconSvg = "user",
DockRight = true // 设置为靠右对齐
};
menu.Items.Add(userItem);
var settingItem = new NavigationMenuItem
{
Text = "设置",
IconSvg = "setting",
DockRight = true // 设置为靠右对齐
};
menu.Items.Add(settingItem);
圆角边框与功能色
csharp
var menu = new NavigationMenuEx
{
Orientation = NavigationMenuOrientation.Horizontal,
BorderRadius = 8, // 圆角半径
ShowBorder = true,
BorderWidth = 1,
BorderColor = Color.FromArgb(200, 200, 200),
ColorType = ColorType.Primary, // 使用主题主色(影响选中状态)
Dock = DockStyle.Top,
Height = 48
};
常见问题
Q: 如何控制横向布局的控件高度?
A: 横向布局的控件高度由外部容器或手动设置决定。ItemHeight 只影响菜单项的绘制高度:
csharp
var menu = new NavigationMenuEx
{
Orientation = NavigationMenuOrientation.Horizontal,
ItemHeight = 48, // 菜单项绘制高度为48
Dock = DockStyle.Top // 控件高度由 Dock 决定
};
// 或者手动设置高度
menu.Height = 60; // 控件高度为60,菜单项在内部居中绘制
Q: 纵向布局的控件高度如何控制?
A: 纵向布局的高度可以自动计算(包含所有可见菜单项和展开的子菜单),也可以由外部容器控制。如需自动计算,请确保控件没有设置 Dock 或 Anchor;如需固定高度,使用 Dock 或设置 Height。
Q: ItemPadding 设置太大导致图标消失?
A: ItemPadding 会压缩内容区域。如果设置过大,图标可能因空间不足而被隐藏。建议保持默认值 4,或根据实际需求调整。
Q: ItemPadding 如何影响背景绘制?
A: ItemPadding 同时影响背景和内容区域。选中状态的背景色只会填充内容区域(即应用了 ItemPadding 后的区域),而不是整个菜单项。这样可以使选中效果更美观,四周留出间隙。
Q: 如何在两种布局中都实现"均匀分布"?
A:
- 横向布局 :设置
ItemWidth = 0,菜单项会自动平分控件宽度 - 纵向布局 :设置
ItemHeight = 0,菜单项会自动平分控件可用高度(扣除 MenuPadding 和 ItemGap)
csharp
// 横向均匀分布
var hMenu = new NavigationMenuEx
{
Orientation = NavigationMenuOrientation.Horizontal,
ItemWidth = 0, // 自动平分宽度
Dock = DockStyle.Top,
Height = 48
};
// 纵向均匀分布
var vMenu = new NavigationMenuEx
{
Orientation = NavigationMenuOrientation.Vertical,
ItemHeight = 0, // 自动平分高度
ItemGap = 4, // 间隙会参与高度计算
Dock = DockStyle.Left,
Width = 200
};
Q: 如何启用纵向滚动条?
A: 纵向布局时,设置 EnableScroll = true 即可启用内置滚动条:
csharp
var sidebar = new NavigationMenuEx
{
Orientation = NavigationMenuOrientation.Vertical,
EnableScroll = true, // 启用滚动
Dock = DockStyle.Left,
Width = 200
};
// 当菜单项总高度超过控件高度时,自动显示滚动条
Q: 横向布局的子菜单如何显示?
A: 横向布局时,子菜单自动以弹出窗口形式显示在父菜单项下方:
csharp
var menu = new NavigationMenuEx
{
Orientation = NavigationMenuOrientation.Horizontal,
Dock = DockStyle.Top,
Height = 48,
AutoExpandSubMenu = true // 悬停自动显示子菜单
};
var fileItem = menu.Items.AddItem("文件", "file");
fileItem.SubItems.AddItem("新建", "file-add");
fileItem.SubItems.AddItem("打开", "folder-open");
// 鼠标悬停"文件"时会自动弹出子菜单
Q: 点击横向子菜单会导致主窗体失去焦点吗?
A: 不会。子菜单弹出窗口设置了 ShowWithoutActivation = true,点击子菜单项不会抢夺焦点,主窗体保持活跃状态。
Q: 纵向布局的装饰条(选中指示器)如何控制?
A: 纵向布局时,选中项左侧会显示装饰条:
csharp
var sidebar = new NavigationMenuEx
{
Orientation = NavigationMenuOrientation.Vertical,
IndicatorColor = Color.FromArgb(24, 144, 255), // 装饰条颜色
IndicatorWidth = 3, // 装饰条宽度(像素)
ItemPadding = new Padding(4) // 装饰条高度 = ItemHeight - ItemPadding.Top - ItemPadding.Bottom
};
属性对照表
| 属性 | 横向布局 | 纵向布局 | 备注 |
|---|---|---|---|
| ItemHeight | 0=平分高度, >0=固定高度 | 0=平分高度, >0=固定高度 | 不影响控件本身高度 |
| ItemWidth | 0=平分宽度, >0=固定宽度 | 0=充满, >0=居中固定 | 效果不同 |
| MenuPadding | 主要左右生效 | 上下左右都生效 | 纵向会影响均匀分布计算 |
| ItemPadding | 背景和内容的内边距 | 背景和内容的内边距 | 影响背景和内容区域 |
| ItemGap | 菜单项间隙 | 菜单项间隙 | 纵向会影响均匀分布计算 |
| EnableScroll | 无效 | 生效 | 仅纵向支持内置滚动 |
| ScrollBarTrackColor | 无效 | 生效 | 仅纵向且 EnableScroll=true 时有效 |
| ScrollBarThumbColor | 无效 | 生效 | 仅纵向且 EnableScroll=true 时有效 |
| IndicatorColor | 无效 | 生效 | 仅纵向有选中指示器 |
| SubMenuIndent | 无效 | 生效 | 仅纵向有子菜单缩进 |
| AutoExpandSubMenu | 悬停弹出子菜单 | 点击展开子菜单 | 横向和纵向行为不同 |
| FollowGlobalTheme | 跟随全局主题 | 跟随全局主题 | 默认 false,需手动启用 |
修复记录
版本更新
横向布局问题修复
-
图标和文字重叠问题
- 问题描述:横向布局时,图标和文字距离太近甚至重叠
- 修复方案:在
DrawIconLeftTextRight方法中,文字位置计算现在正确考虑了图标左侧偏移量 - 修改内容:
int textX = contentRect.X + iconOffsetX + actualIconSize + spacing; - 图标背景影响 :当启用图标背景圆时,会额外增加 8 像素偏移(
bgOffsetX = showBg ? 8 : 0),确保背景圆完全在背景色块内
-
横向子菜单宽度问题
- 问题描述:子菜单宽度不足导致文本换行
- 修复方案:大幅增加
CalculateOptimalWidth()方法中的所有参数(最小宽度、内边距、图标区域宽度等) - 修改内容:最小宽度从 120 增加到 150,内边距从 40 增加到 60
-
图标左侧间隙问题
-
问题描述:图标前面的间隙太小,贴边显示
-
修复方案:在
DrawIconLeftTextRight方法中增加基础偏移量 -
修改内容:
csharpint baseOffsetX = 8; // 基础间隙(始终存在) int bgOffsetX = showBg ? 8 : 0; // 背景圆额外偏移 int iconOffsetX = baseOffsetX + bgOffsetX; -
图标背景影响:无论是否显示图标背景圆,都至少有 8 像素基础间隙;显示背景圆时再额外增加 8 像素,确保图标与背景圆有适当间距
-
-
横向子菜单弹出窗口内边距问题
- 问题描述:子菜单弹出窗口太靠边,最底部一行显示不全
- 修复方案:从父菜单获取内边距设置,计算窗口高度时包含上下内边距和间隙
纵向布局问题修复
-
二级菜单高度不一致问题
- 问题描述:二级菜单高度比一级菜单高,不协调
- 修复方案:在
DrawSubItems方法中,自适应模式下使用当前层级的可见项数量计算高度,而非顶级菜单的数量 - 修改内容:将
_items改为subItems来计算可见项数量
-
一级菜单和二级菜单间隙问题
- 问题描述:当
ItemPadding设置为 0 时,一级菜单和二级菜单之间没有间隙 - 修复方案:在绘制子菜单前添加间隙
- 修改内容:
y += _itemGap / 2;
- 问题描述:当
-
背景绘制不一致问题
- 问题描述:纵向布局时,不同状态下背景绘制区域不一致
- 修复方案:统一纵向布局的背景绘制逻辑,不再区分选中/悬停状态
- 修改内容:纵向布局背景始终布满整个宽度(只考虑上下 padding)
三、后记
陆续补充完善中,敬请关注,如有需求,有好的建议,请留言(xue5zhijing)