【WinForm UI控件系列】NavigationMenuEx 导航菜单控件(类似HeaderButton)

支持纵向和横向菜单,12种展示样式

类似控件:buttonsGroup

一、效果图



暗色模式下显示:

二、使用说明

控件简介

NavigationMenuEx 是一个功能强大的导航菜单控件,支持横向和纵向两种布局方式,提供多种图标和文字显示组合,支持多级子菜单和主题适配。

主要特性

  • 双布局方向:支持横向(顶部导航)和纵向(侧边栏)布局
  • 多种显示模式
    • 仅文字
    • 仅图标
    • 图标左文字右(横向排列)
    • 图标上文字下(纵向排列)
  • 图标支持:支持 Image 图标和 SVG 图标(使用 SvgIconManager)
  • 图标背景圆:支持为图标添加圆形背景,增强视觉效果
  • 多级子菜单:支持嵌套子菜单,可展开/折叠
  • 选中指示器:纵向布局时,选中项左侧显示竖线指示器
  • 主题适配:实现 IThemeable 接口,支持亮色/暗色主题切换(默认关闭,需手动启用)
  • 完整交互:支持鼠标悬停、点击选中、禁用状态
  • 均匀分布 :支持横向/纵向自动平分空间(ItemWidth=0ItemHeight=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 图标大小 图标大小

⚠️ 注意事项:

  1. ItemHeight/ItemWidth = 0 表示自动平分:所有可见菜单项平分控件的可用空间(宽度或高度)。
  2. ItemWidth = 0 在两种布局中的默认行为:横向是"平分",纵向是"充满"。
  3. 纵向布局的控件高度可以自动计算,包含:内边距 + 所有可见项高度 + 展开的子菜单高度 + 间隙/分割线高度。
  4. ItemHeight 只影响菜单项的绘制高度,不影响控件本身的高度
  5. ItemPadding 同时影响背景和内容:背景绘制和内容区域都会应用 ItemPadding,使选中效果与内容区域一致。
  6. 纵向布局背景区域:选中/悬停状态的背景色块会正确考虑 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,需手动启用

修复记录

版本更新

横向布局问题修复
  1. 图标和文字重叠问题

    • 问题描述:横向布局时,图标和文字距离太近甚至重叠
    • 修复方案:在 DrawIconLeftTextRight 方法中,文字位置计算现在正确考虑了图标左侧偏移量
    • 修改内容:int textX = contentRect.X + iconOffsetX + actualIconSize + spacing;
    • 图标背景影响 :当启用图标背景圆时,会额外增加 8 像素偏移(bgOffsetX = showBg ? 8 : 0),确保背景圆完全在背景色块内
  2. 横向子菜单宽度问题

    • 问题描述:子菜单宽度不足导致文本换行
    • 修复方案:大幅增加 CalculateOptimalWidth() 方法中的所有参数(最小宽度、内边距、图标区域宽度等)
    • 修改内容:最小宽度从 120 增加到 150,内边距从 40 增加到 60
  3. 图标左侧间隙问题

    • 问题描述:图标前面的间隙太小,贴边显示

    • 修复方案:在 DrawIconLeftTextRight 方法中增加基础偏移量

    • 修改内容:

      csharp 复制代码
      int baseOffsetX = 8;           // 基础间隙(始终存在)
      int bgOffsetX = showBg ? 8 : 0; // 背景圆额外偏移
      int iconOffsetX = baseOffsetX + bgOffsetX;
    • 图标背景影响:无论是否显示图标背景圆,都至少有 8 像素基础间隙;显示背景圆时再额外增加 8 像素,确保图标与背景圆有适当间距

  4. 横向子菜单弹出窗口内边距问题

    • 问题描述:子菜单弹出窗口太靠边,最底部一行显示不全
    • 修复方案:从父菜单获取内边距设置,计算窗口高度时包含上下内边距和间隙
纵向布局问题修复
  1. 二级菜单高度不一致问题

    • 问题描述:二级菜单高度比一级菜单高,不协调
    • 修复方案:在 DrawSubItems 方法中,自适应模式下使用当前层级的可见项数量计算高度,而非顶级菜单的数量
    • 修改内容:将 _items 改为 subItems 来计算可见项数量
  2. 一级菜单和二级菜单间隙问题

    • 问题描述:当 ItemPadding 设置为 0 时,一级菜单和二级菜单之间没有间隙
    • 修复方案:在绘制子菜单前添加间隙
    • 修改内容:y += _itemGap / 2;
  3. 背景绘制不一致问题

    • 问题描述:纵向布局时,不同状态下背景绘制区域不一致
    • 修复方案:统一纵向布局的背景绘制逻辑,不再区分选中/悬停状态
    • 修改内容:纵向布局背景始终布满整个宽度(只考虑上下 padding)

三、后记

陆续补充完善中,敬请关注,如有需求,有好的建议,请留言(xue5zhijing)