這個系列是為了記錄自訂 Menu 和 ContextMenu 的文章,因為總是有人嫌原來的 MenuItem 樣式不好看,又因為這件事情有一些小細節需要注意,所以寫下這系列文章免得自己忘記。
我們可以從微軟文件中的 Menu 樣式和範本#MenuItem ControlTemplate 這個章節看到 MenuItem 的範本與樣式。這邊有一段值得先注意的部分是 MenuItem 的 Style:
<Style x:Key="MenuItemStyle1" TargetType="{x:Type MenuItem}">
<Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="ScrollViewer.PanningMode" Value="Both"/>
<Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
<Setter Property="Template" Value="{DynamicResource {ComponentResourceKey ResourceId=SubmenuItemTemplateKey, TypeInTargetAssembly={x:Type MenuItem}}}"/>
<Style.Triggers>
<Trigger Property="Role" Value="TopLevelHeader">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource Menu.Static.Foreground}"/>
<Setter Property="Template" Value="{DynamicResource {ComponentResourceKey ResourceId=TopLevelHeaderTemplateKey, TypeInTargetAssembly={x:Type MenuItem}}}"/>
<Setter Property="Padding" Value="6,0"/>
</Trigger>
<Trigger Property="Role" Value="TopLevelItem">
<Setter Property="Background" Value="{StaticResource Menu.Static.Background}"/>
<Setter Property="BorderBrush" Value="{StaticResource Menu.Static.Border}"/>
<Setter Property="Foreground" Value="{StaticResource Menu.Static.Foreground}"/>
<Setter Property="Template" Value="{DynamicResource {ComponentResourceKey ResourceId=TopLevelItemTemplateKey, TypeInTargetAssembly={x:Type MenuItem}}}"/>
<Setter Property="Padding" Value="6,0"/>
</Trigger>
<Trigger Property="Role" Value="SubmenuHeader">
<Setter Property="Template" Value="{DynamicResource {ComponentResourceKey ResourceId=SubmenuHeaderTemplateKey, TypeInTargetAssembly={x:Type MenuItem}}}"/>
</Trigger>
</Style.Triggers>
</Style>
仔細看 Template Property ,可以發現 Trigger 會影響這個屬性所對應的值,而這個 Trigger 是依據 MenuItem.Role 屬性變化。依據另一份文件 MenuItemRole 列舉 可以得知有四種不同的值:
SubmenuHeader | 3 | 子功能表的標頭。 |
SubmenuItem | 2 | 子功能表內可叫用命令的功能表項目。 |
TopLevelHeader | 1 | 最上層功能表項目的標頭。 |
TopLevelItem | 0 | 可叫用命令的最上層功能表項目。 |
MenuItem.Role 是個唯讀屬性,基本上是在 Menu 或 ContextMenu 在生成的選項的時候所賦予,所以我們得先搞清楚在甚麼狀況會是甚麼 Role 才能正確的設計 MenuItem 範本對應。我們先做一個簡單的測試:
<Window x:Class="WpfMenuItemStorySamples.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfMenuItemStorySamples"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions >
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Menu>
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}"/>
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}"/>
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}">
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}"/>
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}">
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}"/>
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}"/>
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}">
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}"/>
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}"/>
</MenuItem>
</MenuItem>
</MenuItem>
</Menu>
</Grid>
<Window.ContextMenu>
<ContextMenu>
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}"/>
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}">
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}"/>
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}">
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}"/>
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}"/>
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}">
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}"/>
<MenuItem Header="{Binding RelativeSource={RelativeSource Self}, Path=Role}"/>
</MenuItem>
</MenuItem>
</MenuItem>
</ContextMenu>
</Window.ContextMenu>
</Window>
這個 XAML 範例建立的 Menu 與 ContextMenu ,Header 屬性繫結到 MenuItem 本身的 Role 屬性,執行後就可以明瞭甚麼樣的狀況會是甚麼 Role。
Menu 的執行結果

先看第一層:
- 沒有帶子項目的 MenuItem,它的 Role 是 TopLevelItem。
- 帶有子項目的 MenuItem,它的 Role 則是 TopLevelHeader。
第二層和以後:
- 沒有帶子項目的 MenuItem,它的 Role 是 SubmenuItem。
- 帶有子項目的 MenuItem,它的 Role 則是 SubmenuHeader 。
ContextMenu 執行結果

ContextMenu 沒有 TopLevel,都是 Submenu :
- 沒有帶子項目的 MenuItem,它的 Role 是 SubmenuItem。
- 帶有子項目的 MenuItem,它的 Role 則是 SubmenuHeader 。
結語
當我們理解了 MenuItem 在不同情形下的 Role 之後,就可以知道為什麼裡面會出現四個 MenuItem 的 ControlTemplate 了。

本篇所使用的範例在此。