zoukankan      html  css  js  c++  java
  • [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

  • 相关阅读:
    CSS 单行超出隐藏
    python开发学习day12 (函数参数;函数对象)
    作业day11
    python开发学习day11 (函数; 返回值 ; 形参与实参 )
    python开发学习day10 (文件指针; 文本修改)
    作业day09
    python开发学习day09 (with上下文处理; b模式; +模式; 文件操作)
    作业day08
    python开发学习day08 (集合; 文件操作)
    作业day07
  • 原文地址:https://www.cnblogs.com/dino623/p/Uising_RadioButton_in_MenuItem.html
Copyright © 2011-2022 走看看