专栏首页dino.c的专栏[WPF 自定义控件]在MenuItem上使用RadioButton

[WPF 自定义控件]在MenuItem上使用RadioButton

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • [UWP]使用Acrylic(亚克力)

    在 如何使用Fluent Design System 这篇文章里已经简单介绍过Reveal的用法,这篇再详细介绍其它内容。 自Windows 8 放弃Aero...

    dino.c
  • [UWP]实用的Shape指南

    在UWP UI系统中,使用Shape是绘制2D图形最简单的方式,小到图标,大到图表都用到Shape的派生类,可以说有举足轻重的地位。幸运的是从Silverlig...

    dino.c
  • [UWP]如何使用代码创建DataTemplate(或者ControlTemplate)

    在UWP中DataTemplate是一个十分重要的功能,并且几乎无处不在,例如DataGrid中的DataGridTemplateColumn:

    dino.c
  • 该如何设计你的 PasswordEncoder?

    来源:https://www.cnkirito.moe/spring-security-6

    芋道源码
  • 【职业规划】大数据培训完一般可以做哪些工作?

    这个时代是大数据时代,也是大数据人才稀缺的时代。由于中国人才缺口比较大,大数据也迅速成为行业和市场的热点,更多的企业无论是对人才的招聘还是在培训都成了刚需,这也...

    钱塘数据
  • 如何存储用户的密码才能算安全?

    前段时间将一个集成了 spring-security-oauth2 的旧项目改造了一番,将 springboot 升级成了 springboot 2.0,众所周...

    kirito-moe
  • 为什么Kaggle不会让你成为一名出色的数据科学家

    毫无疑问,Kaggle是非常适合学习数据科学的平台。许多数据科学家在Kaggle上投入了大量时间。

    CDA数据分析师
  • 1019. 分段函数

    1019. 分段函数 (Standard IO) 时间限制: 1000 ms  空间限制: 262144 KB  具体限制  题目描述 编写程序,计算下列分段...

    attack
  • Django -- 快速测试&页面美化

    经过前几篇的介绍,我们以及能够建立简单的网站页面,那如果嫌弃网站页面简陋,那我们就必须要为他加上样式来渲染网络页面,在Django中,我们把这些文件统称为"静态...

    cctester
  • 你适合从事数据分析吗?

    近年来,越来越多的人选择大数据行业,只看到了大数据行业前景不错、薪资待遇不错,而且培训项目、机构众多,各大名企对于大数据人才的需求也不断上涨。 但是没有对岗位和...

    企鹅号小编

扫码关注云+社区

领取腾讯云代金券