欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-08-08 11:31:12
1. 需求 上图这种包含多选(CheckBox)和单选(RadioButton)的菜单十分常见,可是在WPF中只提供了多选的MenuItem。顺便一提,要使MenuItem可以多选,只需要将MenuItem的 属性设置为True: 不知出于何种考虑,WPF没有为MenuItem提供单选的功能。为了在 ......

1. 需求

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

上图这种包含多选(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>

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

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