1. 需求 上圖這種包含多選(CheckBox)和單選(RadioButton)的菜單十分常見,可是在WPF中只提供了多選的MenuItem。順便一提,要使MenuItem可以多選,只需要將MenuItem的 屬性設置為True: 不知出於何種考慮,WPF沒有為MenuItem提供單選的功能。為了在 ...
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