I recently began working with WPF for the first time. I was doing this outside of my job on my own time (which is sometimes quite limited). I had never worked with WPF before this venture. I decided to download a trial copy of Microsoft Expression Blend 2 to check out the designer side of WPF. The first thing that I noticed was the menus. The menus had a black background with white text and the submenus had white borders and white menu separators (pictured below). I decided that I liked this look and set out to implement this using Visual Studio 2008. This entry describes how to create this menu.
I implemented the menu using two custom Styles. The first for the Separator objects and the second for the MenuItem objects. The XAML for the Separator style is as follows:
1<Style x:Key="MLB_Separator" TargetType="{x:Type Separator}">
2 <Setter Property="Margin" Value="0,3,0,3" />
3 <Setter Property="Template">
4 <Setter.Value>
5 <ControlTemplate TargetType="{x:Type Separator}">
6 <Grid>
7 <Rectangle Height="1" Stroke="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Menu}}}" />
8 </Grid>
9 </ControlTemplate>
10 </Setter.Value>
11 </Setter>
12</Style>
As you can see in the code, The Separator style simply sets the default margins (line 2) and draws a 1px high rectangle using the parent Menu’s foreground color (line 7). For the MenuItem style, I started with the SimpleMenuItem style that was included when I was using Expression Blend. I found that this style was not complete and simply did not work correctly in some cases. After cleaning up that code and adding some additional features, I ended up with the XAML below:
1<Style x:Key="MLB_MenuItem" TargetType="{x:Type MenuItem}">
2 <Setter Property="Foreground" Value="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Menu}}}"/>
3 <Setter Property="Template">
4 <Setter.Value>
5 <ControlTemplate TargetType="{x:Type MenuItem}">
6 <Border x:Name="Border"
7 Background="{TemplateBinding Background}"
8 BorderBrush="{TemplateBinding BorderBrush}"
9 BorderThickness="{TemplateBinding BorderThickness}">
10 <Grid>
11 <Grid.ColumnDefinitions>
12 <ColumnDefinition x:Name="Col0" MinWidth="17" Width="Auto" SharedSizeGroup="MenuItemIconColumnGroup"/>
13 <ColumnDefinition Width="Auto" SharedSizeGroup="MenuTextColumnGroup"/>
14 <ColumnDefinition Width="Auto" SharedSizeGroup="MenuItemIGTColumnGroup"/>
15 <ColumnDefinition x:Name="Col3" Width="14"/>
16 </Grid.ColumnDefinitions>
17 <!-- ContentPresenter to show an Icon if needed -->
18 <ContentPresenter Grid.Column="0" Margin="4,0,6,0" x:Name="Icon" VerticalAlignment="Center" ContentSource="Icon"/>
19 <!-- Glyph is a checkmark if needed for a checkable menu -->
20 <Grid Grid.Column="0" Visibility="Hidden" Margin="4,0,6,0" x:Name="GlyphPanel" VerticalAlignment="Center">
21 <Path x:Name="GlyphPanelpath" VerticalAlignment="Center" Fill="{TemplateBinding Foreground}" Data="M0,2 L0,4.8 L2.5,7.4 L7.1,2.8 L7.1,0 L2.5,4.6 z" FlowDirection="LeftToRight"/>
22 </Grid>
23 <!-- Content for the menu text etc -->
24 <ContentPresenter Grid.Column="1"
25 Margin="{TemplateBinding Padding}"
26 x:Name="HeaderHost"
27 RecognizesAccessKey="True"
28 ContentSource="Header"/>
29 <!-- Content for the menu IGT -->
30 <ContentPresenter Grid.Column="2"
31 Margin="8,1,8,1"
32 x:Name="IGTHost"
33 ContentSource="InputGestureText"
34 VerticalAlignment="Center"/>
35 <!-- Arrow drawn path which points to the next level of the menu -->
36 <Grid Grid.Column="3" Margin="4,0,6,0" x:Name="ArrowPanel" VerticalAlignment="Center">
37 <Path x:Name="ArrowPanelPath" HorizontalAlignment="Right" VerticalAlignment="Center" Fill="{TemplateBinding Foreground}" Data="M0,0 L0,8 L4,4 z"/>
38 </Grid>
39 <!-- The Popup is the body of the menu which expands down or across depending on the level of the item -->
40 <Popup IsOpen="{Binding Path=IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}" Placement="Right" x:Name="SubMenuPopup" Focusable="false" PopupAnimation="{DynamicResource {x:Static SystemParameters.MenuPopupAnimationKey}}">
41 <Border x:Name="SubMenuBorder" BorderBrush="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Menu}}}" BorderThickness="1" Padding="2,2,2,2">
42 <Grid x:Name="SubMenu" Grid.IsSharedSizeScope="True">
43 <!-- StackPanel holds children of the menu. This is set by IsItemsHost=True -->
44 <StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Cycle"/>
45 </Grid>
46 </Border>
47 </Popup>
48 </Grid>
49 </Border>
50 <!-- These triggers re-configure the four arrangements of MenuItem to show different levels of menu via Role -->
51 <ControlTemplate.Triggers>
52 <!-- Role = TopLevelHeader : this is the root menu item in a menu; the Popup expands down -->
53 <Trigger Property="Role" Value="TopLevelHeader">
54 <Setter Property="Padding" Value="6,1,6,1"/>
55 <Setter Property="Placement" Value="Bottom" TargetName="SubMenuPopup"/>
56 <Setter Property="MinWidth" Value="0" TargetName="Col0"/>
57 <Setter Property="Width" Value="Auto" TargetName="Col3"/>
58 <Setter Property="Visibility" Value="Collapsed" TargetName="Icon"/>
59 <Setter Property="Visibility" Value="Collapsed" TargetName="GlyphPanel"/>
60 <Setter Property="Visibility" Value="Collapsed" TargetName="IGTHost"/>
61 <Setter Property="Visibility" Value="Collapsed" TargetName="ArrowPanel"/>
62 </Trigger>
63 <!-- Role = TopLevelItem : this is a child menu item from the top level without any child items-->
64 <Trigger Property="Role" Value="TopLevelItem">
65 <Setter Property="Padding" Value="6,1,6,1"/>
66 <Setter Property="Visibility" Value="Collapsed" TargetName="ArrowPanel"/>
67 </Trigger>
68 <!-- Role = SubMenuHeader : this is a child menu item which does not have children -->
69 <Trigger Property="Role" Value="SubmenuHeader">
70 <Setter Property="DockPanel.Dock" Value="Top"/>
71 <Setter Property="Padding" Value="0,2,0,2"/>
72 </Trigger>
73 <!-- Role = SubMenuItem : this is a child menu item which has children-->
74 <Trigger Property="Role" Value="SubmenuItem">
75 <Setter Property="DockPanel.Dock" Value="Top"/>
76 <Setter Property="Padding" Value="0,2,0,2"/>
77 <Setter Property="Visibility" Value="Collapsed" TargetName="ArrowPanel"/>
78 </Trigger>
79 <Trigger Property="IsSuspendingPopupAnimation" Value="true">
80 <Setter Property="PopupAnimation" Value="None" TargetName="SubMenuPopup"/>
81 </Trigger>
82 <!-- If no Icon is present the we collapse the Icon Content -->
83 <Trigger Property="Icon" Value="{x:Null}">
84 <Setter Property="Visibility" Value="Collapsed" TargetName="Icon"/>
85 </Trigger>
86 <!-- The GlyphPanel contains the CheckMark -->
87 <Trigger Property="IsChecked" Value="true">
88 <Setter Property="Visibility" Value="Visible" TargetName="GlyphPanel"/>
89 <Setter Property="Visibility" Value="Collapsed" TargetName="Icon"/>
90 </Trigger>
91 <!-- Using the system colors for the Menu Highlight and IsEnabled-->
92 <Trigger Property="IsHighlighted" Value="true">
93 <Setter Property="Background" Value="LightGray" TargetName="Border"/>
94 <Setter Property="Foreground" Value="Black"/>
95 </Trigger>
96 <Trigger Property="IsEnabled" Value="false">
97 <Setter Property="Foreground" Value="LightGray"/>
98 </Trigger>
99 </ControlTemplate.Triggers>
100 </ControlTemplate>
101 </Setter.Value>
102 </Setter>
103</Style>
The MenuItem style first sets the foreground color to the parent Menu’s foreground color (line 2). Then comes the Template, which is where it gets interesting. The Template starts with a Border around a Grid. The Grid has 4 ColumnDefinition items. The first is for the icon or the checkbox (depending on the type of MenuItem). The second is for the text of the MenuItem. The third is for the input gesture text (e.g. Ctrl-S). The last is for the arrow, if the MenuItem contains a sub-menu.
The contents of the columns are then defined. First is a ContentPresenter that will display the value of the “Icon” property. Next is a Grid that will hold the checkbox, if needed. These are both displayed in the first column of the main Grid (but not at the same time). After this is another ContentPresenter that will display the value of the “Header” property. Next is another ContentPresenter that will display the value of the “InputGestureText” property. Then there is a Grid that will display the arrow if there is a sub-menu. Finally, there is a Popup that will display the sub-menu (if one is defined).
The remainder of the Style contains the Trigger items that will manipulate what is displayed based on the type of MenuItem (top-level menu item, sub-menu meu item, etc.) that is being rendered. I will leave these items for a future entry.