1. 需求

上图这种包含多选(CheckBox)和单选(RadioButton)的菜单十分常见,可是在WPF中只提供了多选的MenuItem。顺便一提,要使MenuItem可以多选,只需要将MenuItem的IsCheckable属性设置为True:

<MenuItem IsCheckable="True"/>

不知出于何种考虑,WPF没有为MenuItem提供单选的功能。为了在MenuItem中添加RadioButton,可以尝试修改样式并在CodeBehind找那个处理MenuItem的Click事件,但这种事做多了还是做成一个自定义控件比较方便。这篇文章将介绍如何自定义一个RadioButtonMenuItem控件实现MenuItem的单选功能。

2. 实现代码

RadioButtonMenuItem的代码比较简单(换言之,样式部分比较难),首先继承自MenuItem,然后模仿RadioButton添加一个GroupName属性:

public class RadioButtonMenuItem : MenuItem
{
/// <summary>
/// 标识 GroupName 依赖属性。
/// </summary>
public static readonly DependencyProperty GroupNameProperty =
DependencyProperty.Register(nameof(GroupName), typeof(string), typeof(RadioButtonMenuItem), new PropertyMetadata(default(string))); static RadioButtonMenuItem()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(RadioButtonMenuItem), new FrameworkPropertyMetadata(typeof(RadioButtonMenuItem)));
} /// <summary>
/// 获取或设置GroupName的值
/// </summary>
public string GroupName
{
get { return (string)GetValue(GroupNameProperty); }
set { SetValue(GroupNameProperty, value); }
}

RadioButtonMenuItem的分组规则很简单,只要同一个MenuItem下的RadioButtonMenuItem为一组,然后再根据GroupName分组。因为我很少会更改GroupName,所以就难得监视GroupName的改变了。

因为MenuItem派生自ItemsControl,所以需要重写GetContainerForItemOverride以确定它的Items也是用RadioButtonMenuItem作为默认的ItemContainer:

protected override DependencyObject GetContainerForItemOverride()
{
return new RadioButtonMenuItem();
}

然后重写OnClick,让RadioButtonMenuItem每次点击都被选中,这个行为和RadioButton一致:

protected override void OnClick()
{
base.OnClick();
IsChecked = true;
}

最后重写OnClick函数,在这个函数里面找出在同一个MenuItem下且GroupName一样的RadioButtonMenuItem,将他们的IsChecked 全部设置为False,这样就实现了MenuItem的单选功能:

protected override void OnChecked(RoutedEventArgs e)
{
base.OnChecked(e); if (this.Parent is MenuItem parent)
{
foreach (var menuItem in parent.Items.OfType<RadioButtonMenuItem>())
{
if (menuItem != this && menuItem.GroupName == GroupName && (menuItem.DataContext == parent.DataContext || menuItem.DataContext != DataContext))
{
menuItem.IsChecked = false;
}
}
}
}

3. 实现样式

MenuItem有一个Role属性,它的类型为MenuItemRole,定义如下:

//
// 摘要:
// Defines the different roles that a System.Windows.Controls.MenuItem can have.
public enum MenuItemRole
{
//
// 摘要:
// Top-level menu item that can invoke commands.
TopLevelItem = 0,
//
// 摘要:
// Header for top-level menus.
TopLevelHeader = 1,
//
// 摘要:
// Menu item in a submenu that can invoke commands.
SubmenuItem = 2,
//
// 摘要:
// Header for a submenu.
SubmenuHeader = 3
}

根据MenuItem所处的位置,它的Role会有不同的值,大致上如下面例子所示:

<Menu x:Name="Men">
<MenuItem Header="TopLevelItem" />
<MenuItem Header="TopLevelHeader">
<MenuItem Header="SubMenuHeader">
<MenuItem Header="SubMenuItem" />
</MenuItem>
<MenuItem Header="SubMenuItem" />
</MenuItem>
</Menu>

MenuItem的样式麻烦之处就在这里。因为微软并没有在文档中提供Aero2的样式,所以在以前要获取一个控件的样式标准的做法是使用Blend选中控件后编辑控件的模板,但因为MenuItem会有不同的Role,所以它当前的模板会不一样,用Blend很难获取到它的全部的模板。大致上它的样式定义如下:

<ControlTemplate x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type MenuItem}, ResourceId=TopLevelItemTemplateKey}"
TargetType="{x:Type MenuItem}">
</ControlTemplate>
<ControlTemplate x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type MenuItem}, ResourceId=TopLevelHeaderTemplateKey}"
TargetType="{x:Type MenuItem}"> </ControlTemplate> <ControlTemplate x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type MenuItem}, ResourceId=SubmenuItemTemplateKey}"
TargetType="{x:Type MenuItem}">
</ControlTemplate> <ControlTemplate x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type MenuItem}, ResourceId=SubmenuHeaderTemplateKey}"
TargetType="{x:Type MenuItem}">
</ControlTemplate> <Style x:Key="{x:Type local:RadioButtonMenuItem}"
TargetType="{x:Type local:RadioButtonMenuItem}">
<Setter Property="Control.Template"
Value="{StaticResource {ComponentResourceKey TypeInTargetAssembly={x:Type MenuItem}, ResourceId=SubmenuItemTemplateKey}}" />
<Style.Triggers>
<Trigger Property="MenuItem.Role"
Value="TopLevelHeader">
<Setter Property="Control.Template"
Value="{StaticResource {ComponentResourceKey TypeInTargetAssembly={x:Type MenuItem}, ResourceId=TopLevelHeaderTemplateKey}}" />
<Setter Property="Control.Padding"
Value="6,0" />
</Trigger>
<Trigger Property="MenuItem.Role"
Value="TopLevelItem">
<Setter Property="Control.Template"
Value="{StaticResource {ComponentResourceKey TypeInTargetAssembly={x:Type MenuItem}, ResourceId=TopLevelItemTemplateKey}}" />
<Setter Property="Control.Padding"
Value="6,0" />
</Trigger>
<Trigger Property="MenuItem.Role"
Value="SubmenuHeader">
<Setter Property="Control.Template"
Value="{StaticResource {ComponentResourceKey TypeInTargetAssembly={x:Type MenuItem}, ResourceId=SubmenuHeaderTemplateKey}}" />
</Trigger>
</Style.Triggers>
</Style>

除了使用Blend,以前还可以使用ILSpy反编译出它的资源文件获取控件的样式。幸好现在WPF开元了,Aero2的样式也可以在 Github 上找到。大概500行的样子,虽然大致上只需要将CheckBox的换成一个圆点,但分别搞四次加上些细微的调整把我搞糊涂了。因为它只提供了Aero2的样式,如果要用在Win7最好再定义一个Aero的样式,或者直接将全局样式改为Aero2,我在 这篇文章 里介绍了如何在Win7使用Aero2的样式,可供参考。

修改完模板后效果就如文章开头的图片一样了,使用方法如下:

<kino:RadioButtonMenuItem Header="MoreOptions">
<kino:RadioButtonMenuItem Header="Option 1"
GroupName="GroupA" />
<kino:RadioButtonMenuItem Header="Option 2"
GroupName="GroupA" />
<kino:RadioButtonMenuItem Header="Option 3"
GroupName="GroupA" />
<Separator />
<kino:RadioButtonMenuItem Header="Option 4"
GroupName="GroupB" />
<kino:RadioButtonMenuItem Header="Option 5"
GroupName="GroupB" />
<kino:RadioButtonMenuItem Header="Option 6"
GroupName="GroupB" /> <Separator />
<kino:RadioButtonMenuItem Header="Options ">
<kino:RadioButtonMenuItem Header="Option 7"
GroupName="GroupC" />
<kino:RadioButtonMenuItem Header="Option 8"
GroupName="GroupC" />
<kino:RadioButtonMenuItem Header="Option 9"
GroupName="GroupC" />
</kino:RadioButtonMenuItem>
<Separator />
<MenuItem IsCheckable="True"
Header="Option X" />
<MenuItem IsCheckable="True"
Header="Option Y" />
<MenuItem IsCheckable="True"
Header="Option Z" />
</kino:RadioButtonMenuItem>

4. 参考

MenuItem Class (System.Windows.Controls) _ Microsoft Docs

MenuItemRole Enum (System.Windows.Controls) _ Microsoft Docs

RadioButton Class (System.Windows.Controls) _ Microsoft Docs

» WPF MenuItem as a RadioButton WPF

wpf_MenuItem.xaml at master · dotnet_wpf

5. 源码

RadioButtonMenuItem.cs at master

[WPF 自定义控件]在MenuItem上使用RadioButton的更多相关文章

  1. WPF自定义控件与样式(4)-CheckBox/RadioButton自定义样式

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: Che ...

  2. 【转】WPF自定义控件与样式(4)-CheckBox/RadioButton自定义样式

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等 本文主要内容: CheckBox复选框的自定义样式,有两种不同的风格实现: RadioB ...

  3. WPF自定义控件与样式(1)-矢量字体图标(iconfont)

    一.图标字体 图标字体在网页开发上运用非常广泛,具体可以网络搜索了解,网页上的运用有很多例子,如Bootstrap.但在C/S程序中使用还不多,字体图标其实就是把矢量图形打包到字体文件里,就像使用一般 ...

  4. WPF自定义控件与样式(2)-自定义按钮FButton

    一.前言.效果图 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 还是先看看效果 ...

  5. WPF自定义控件与样式(15)-终结篇 & 系列文章索引 & 源码共享

    系列文章目录  WPF自定义控件与样式(1)-矢量字体图标(iconfont) WPF自定义控件与样式(2)-自定义按钮FButton WPF自定义控件与样式(3)-TextBox & Ric ...

  6. WPF自定义控件与样式(5)-Calendar/DatePicker日期控件自定义样式及扩展

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: 日历控 ...

  7. WPF自定义控件与样式(6)-ScrollViewer与ListBox自定义样式

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: Scr ...

  8. WPF自定义控件与样式(8)-ComboBox与自定义多选控件MultComboBox

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: 下拉选 ...

  9. WPF自定义控件与样式(9)-树控件TreeView与菜单Menu-ContextMenu

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: 菜单M ...

随机推荐

  1. 深入学习MySQL 03 Schema与数据类型优化

    Schema是什么鬼 schema就是数据库对象的集合,这个集合包含了各种对象如:表.视图.存储过程.索引等.为了区分不同的集合,就需要给不同的集合起不同的名字,默认情况下一个用户对应一个集合,用户的 ...

  2. .NET Core Install for Ubuntu 14.04

      Add the dotnet apt-get feed In order to install .NET Core on Ubuntu or Linux Mint, you need to fir ...

  3. typescript step by step two

  4. mongoskin 是让 Node.js 支持 MongoDB 的内嵌访问层。

    mongoskin 是让 Node.js 支持 MongoDB 的内嵌访问层.

  5. initramfs打包集成rootfs到image镜像及linux rootfs的正常启动

    最近的项目中需要在仿真机haps及VDK上集成rootfs,中间遇到一些问题,在此整理记录以备忘. rootfs里面集成的busybox版本1.29.3 (buildroot环境中自带) kernel ...

  6. python调用matlab脚本

    在MATLAB和Python之间建个接口,从Python中调用MATLAB脚本或者是MATLAB的函数.内容不是很难,毕竟现成的接口已经有了,在这儿记录一下API使用的一些事项. 注:本篇使用的是MA ...

  7. Python学习初级python3.6的安装配置

    首先我们来安装python 1.首先进入网站下载:点击打开链接(或自己输入网址https://www.python.org/downloads/),进入之后如下图,选择图中红色圈中区域进行下载. 2. ...

  8. 【WPF学习】第四章 加载和编译XAML

    前面已经介绍过,尽管XAML和WPF这两种技术具有相互补充的作用,但他们也是相互独立的.因此,完全可以创建不使用XAML和WPF应用程序. 总之,可使用三种不同的编码方式来创建WPF应用程序: 只使用 ...

  9. sublime sftp插件安装及时更新网站

    Sublime Text 2 本身并不强大,但是它方便使用插件扩展功能,所以变得很强大.今天介绍一个很实用的插件 SFTP ,可以大大提高前端工作效率. 常见的工作流程 有时候修改一些网站上的文件,通 ...

  10. Spring boot 学习中代码遇到的几个问题

    1.报一大段红错 此时对象还是能创建成功的,解决方案参考链接https://blog.csdn.net/wanglin199709/article/details/99121487 2.无法创建对象 ...