WPF MenuItem 小傳 (1) -- MenuItem.Role 屬性

這個系列是為了記錄自訂 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 列舉 可以得知有四種不同的值:

SubmenuHeader3子功能表的標頭。
SubmenuItem2子功能表內可叫用命令的功能表項目。
TopLevelHeader1最上層功能表項目的標頭。
TopLevelItem0可叫用命令的最上層功能表項目。

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 的執行結果

先看第一層:

  1. 沒有帶子項目的 MenuItem,它的 Role 是 TopLevelItem
  2. 帶有子項目的 MenuItem,它的 Role 則是 TopLevelHeader

第二層和以後:

  1. 沒有帶子項目的 MenuItem,它的 Role 是 SubmenuItem
  2. 帶有子項目的 MenuItem,它的 Role 則是 SubmenuHeader
ContextMenu 執行結果

ContextMenu 沒有 TopLevel,都是 Submenu :

  1. 沒有帶子項目的 MenuItem,它的 Role 是 SubmenuItem
  2. 帶有子項目的 MenuItem,它的 Role 則是 SubmenuHeader
結語

當我們理解了 MenuItem 在不同情形下的 Role 之後,就可以知道為什麼裡面會出現四個 MenuItem 的 ControlTemplate 了。

本篇所使用的範例在此