[WPF 自定義控制元件]在MenuItem上使用RadioButton
1. 需求
上圖這種包含多選(CheckBox)和單選(RadioButton)的選單十分常見,可是在WPF中只提供了多選的MenuItem。順便一提,要使MenuItem可以多選,只需要將MenuItem的IsCheckable
屬性設定為True:
<MenuItem IsCheckable="True"/>
不知出於何種考慮,WPF沒有為MenuItem提供單選的功能。為了在MenuItem中新增RadioButton,可以嘗試修改樣式並在CodeBehind找那個處理MenuItem的Click事件,但這種事做多了還是做成一個自定義控制元件比較方便。這篇文章將介紹如何自定義一個RadioButtonMenuItem
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