[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