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=https://www.cnblogs.com/childking/p/"{StaticResource {ComponentResourceKey TypeInTargetAssembly={x:Type MenuItem}, ResourceId=SubmenuItemTemplateKey}}" />
<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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/65249.html
標籤:其他
下一篇:C#后臺異步訊息佇列實作
