WPF 为ContextMenu使用Fluent风格的亚克力材质特效

书接上回,我们的Fluent WPF的版图已经完成了:

先来看看效果图(win11):

有以下xaml代码:

复制代码
 <TextBlock.ContextMenu>
     <ContextMenu>
         <MenuItem Header="Menu Item 1" Icon="Cd" InputGestureText="aa?" />
         <MenuItem Header="Menu Item 2" >
             <MenuItem Header="Child Item 1" Icon="Ab" />
             <MenuItem Header="Child Item 2" Icon="Ad" />
             <MenuItem Header="Child Item 3" IsCheckable="True" IsChecked="True"/>
         </MenuItem>
         <MenuItem Header="Menu Item 3" Icon="cd" />
     </ContextMenu>
 </TextBlock.ContextMenu>

(由于我的win10虚拟机坏了,暂时没有测试)

前面的工作已经解决了让任意窗口支持Acrylic材质的问题,本文重点介绍ContentMenu和MenuItem的样式适配和实现。

本文的Demo:

TwilightLemon/WindowEffectTest: 测试win10/11的模糊效果 (github.com)

一、为什么需要一个新的ContextMenu和MenuItem模板

如果你直接给ContextMenu应用WindowMaterial特效,可能会出现以下丑陋的效果:

或者:

原因在于古老的ContextMenu和MenuItem模板和样式并不能通过简单修改Background实现我们想要的布局和交互效果。

二、ContextMenu与MenuItem的结构

1. ContextMenu 的结构

ContextMenu直观上看是一个Popup,但它的控件模板并不包含Popup,而是直接指定内部元素(包含一个ScrollViewerStackPanel)。因此不能从控件模板中替换Popup为自定义的FluentPopup,需要使用其他手段让其内部Popup也支持Acrylic材质(见下文)。

在示例代码仓库中展示了较为完整的Menu相关的结构:

MenuItem根据其在菜单树中的位置,通过Role属性分为四种形态。我们在Style.Triggers中分别为它们指定了不同的模板:

  • TopLevelHeader : 顶级菜单项,且包含子菜单Popup(例如菜单栏上的"File")。
  • TopLevelItem: 顶级菜单项,不含子菜单(例如菜单栏上的"Help")。
  • SubmenuHeader : 子菜单项,且包含下一级子菜单Popup
  • SubmenuItem: 子菜单项,不含子菜单(叶子节点)。其模板主要处理图标、文字、快捷键的布局。

三、重写Menu相关控件模板和样式

1. ContextMenu 样式

ContextMenu的样式主要参考了.NET 9自带的Fluent样式,并作了一些调整以适配Acrylic材质。主要目的是覆盖原始模板的Icon部分白框和分割线:

因为我们的WindowMaterial已经为窗口自动附加上圆角、阴影和亚克力材质的DWM效果,所以我们只需要将ContextMenu的背景设置为透明,并移除不必要的边框和分割线即可。

此外,还添加了弹出动画(是在Popup内部做的,并非对window,效果可能不会很理想)。

MenuItem的样式主要处理了图标、文字、快捷键的布局,并根据不同的角色(TopLevelHeader、TopLevelItem、SubmenuHeader、SubmenuItem)应用不同的模板。

  • TopLevelHeader: 只包含Icon和Header,以及弹出的Popup,只需要替换为自定义的FluentPopup即可。
  • TopLevelItem: 只包含Icon和Header,无Popup。
  • SubmenuHeader: 包含Icon、Header和Chevron图标(这里就是一个展开的箭头图标,但是官方叫做雪佛龙..?),以及弹出的Popup,同样替换为FluentPopup。
  • SubmenuItem: 叶子节点,包含Icon、Header和InputGestureText。 其模板主要处理图标、文字、快捷键的布局。

菜单项主要分为三个部分:Icon图标、Header文字和最右侧的提示文字或展开箭头图标。只需要保持三个部分的布局对其即可。如果IsCheckable为True,则Icon部分被自定义图标占据(√, 当IsChecked为True时)。

以下是完整的资源字典,包含了完整的注释。

复制代码
  1 <ResourceDictionary
  2     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4     xmlns:local="clr-namespace:WindowEffectTest"
  5     xmlns:sys="clr-namespace:System;assembly=mscorlib">
  6 
  7     <!--  菜单边框内边距  -->
  8     <Thickness x:Key="MenuBorderPadding">0,3,0,3</Thickness>
  9 
 10     <!--  菜单项外边距  -->
 11     <Thickness x:Key="MenuItemMargin">4,1</Thickness>
 12 
 13     <!--  菜单项内容内边距  -->
 14     <Thickness x:Key="MenuItemContentPadding">10,6</Thickness>
 15 
 16     <!--  顶级菜单项外边距  -->
 17     <Thickness x:Key="TopLevelItemMargin">4</Thickness>
 18 
 19     <!--  顶级菜单项内容边距  -->
 20     <Thickness x:Key="TopLevelContentMargin">10</Thickness>
 21 
 22     <!--  菜单圆角半径  -->
 23     <CornerRadius x:Key="MenuCornerRadius">4</CornerRadius>
 24 
 25     <!--  顶级菜单圆角半径  -->
 26     <CornerRadius x:Key="TopLevelCornerRadius">6</CornerRadius>
 27 
 28     <!--  菜单动画持续时间  -->
 29     <Duration x:Key="MenuAnimationDuration">0:0:0.167</Duration>
 30 
 31     <!--  复选标记图标路径数据  -->
 32     <PathGeometry x:Key="CheckGraph">
 33         M392.533333 806.4L85.333333 503.466667l59.733334-59.733334 247.466666 247.466667L866.133333 213.333333l59.733334 59.733334L392.533333 806.4z
 34     </PathGeometry>
 35 
 36     <!--  前进箭头图标路径数据(用于子菜单指示器)  -->
 37     <PathGeometry x:Key="ForwardGraph">
 38         M283.648 174.081l57.225-59.008 399.479 396.929-399.476 396.924-57.228-59.004 335.872-337.92z
 39     </PathGeometry>
 40 
 41     <!--  菜单项ScrollViewer样式  -->
 42     <Style
 43         x:Key="MenuItemScrollViewerStyle"
 44         BasedOn="{StaticResource {x:Type ScrollViewer}}"
 45         TargetType="{x:Type ScrollViewer}">
 46         <Setter Property="HorizontalScrollBarVisibility" Value="Disabled" />
 47         <Setter Property="VerticalScrollBarVisibility" Value="Auto" />
 48     </Style>
 49 
 50     <!--  默认集合焦点视觉样式  得到键盘焦点时显示  -->
 51     <Style x:Key="DefaultCollectionFocusVisualStyle">
 52         <Setter Property="Control.Template">
 53             <Setter.Value>
 54                 <ControlTemplate>
 55                     <Rectangle
 56                         Margin="4,0"
 57                         RadiusX="4"
 58                         RadiusY="4"
 59                         SnapsToDevicePixels="True"
 60                         Stroke="{DynamicResource AccentColor}"
 61                         StrokeThickness="2" />
 62                 </ControlTemplate>
 63             </Setter.Value>
 64         </Setter>
 65     </Style>
 66 
 67     <!--  默认上下文菜单样式  -->
 68     <Style x:Key="DefaultContextMenuStyle" TargetType="{x:Type ContextMenu}">
 69         <Setter Property="MinWidth" Value="140" />
 70         <Setter Property="Padding" Value="0" />
 71         <Setter Property="Margin" Value="0" />
 72         <Setter Property="HasDropShadow" Value="False" />
 73         <Setter Property="Grid.IsSharedSizeScope" Value="True" />
 74         <Setter Property="Popup.PopupAnimation" Value="None" />
 75         <Setter Property="SnapsToDevicePixels" Value="True" />
 76         <Setter Property="OverridesDefaultStyle" Value="True" />
 77         <Setter Property="Template">
 78             <Setter.Value>
 79                 <ControlTemplate TargetType="{x:Type ContextMenu}">
 80                     <Border
 81                         x:Name="Border"
 82                         Padding="{StaticResource MenuBorderPadding}"
 83                         Background="{TemplateBinding Background}"
 84                         BorderBrush="{TemplateBinding BorderBrush}"
 85                         BorderThickness="{TemplateBinding BorderThickness}">
 86                         <!--  用于动画的转换变换  -->
 87                         <Border.RenderTransform>
 88                             <TranslateTransform />
 89                         </Border.RenderTransform>
 90                         <ScrollViewer CanContentScroll="True" Style="{StaticResource MenuItemScrollViewerStyle}">
 91                             <!--  菜单项容器  -->
 92                             <StackPanel
 93                                 ClipToBounds="True"
 94                                 IsItemsHost="True"
 95                                 KeyboardNavigation.DirectionalNavigation="Cycle"
 96                                 Orientation="Vertical" />
 97                         </ScrollViewer>
 98                     </Border>
 99                     <ControlTemplate.Triggers>
100                         <!--  菜单打开时的动画效果  -->
101                         <Trigger Property="IsOpen" Value="True">
102                             <Trigger.EnterActions>
103                                 <BeginStoryboard>
104                                     <Storyboard>
105                                         <!--  Y轴平移动画:从-45向下滑入到0  -->
106                                         <DoubleAnimation
107                                             Storyboard.TargetName="Border"
108                                             Storyboard.TargetProperty="(Border.RenderTransform).(TranslateTransform.Y)"
109                                             From="-45"
110                                             To="0"
111                                             Duration="{StaticResource MenuAnimationDuration}">
112                                             <DoubleAnimation.EasingFunction>
113                                                 <CircleEase EasingMode="EaseOut" />
114                                             </DoubleAnimation.EasingFunction>
115                                         </DoubleAnimation>
116                                     </Storyboard>
117                                 </BeginStoryboard>
118                             </Trigger.EnterActions>
119                         </Trigger>
120                     </ControlTemplate.Triggers>
121                 </ControlTemplate>
122             </Setter.Value>
123         </Setter>
124     </Style>
125 
126     <!--  顶级菜单项头部模板(带子菜单)  -->
127     <ControlTemplate x:Key="{x:Static MenuItem.TopLevelHeaderTemplateKey}" TargetType="{x:Type MenuItem}">
128         <Border
129             x:Name="Border"
130             Margin="{StaticResource TopLevelItemMargin}"
131             Background="{TemplateBinding Background}"
132             BorderBrush="{TemplateBinding BorderBrush}"
133             BorderThickness="{TemplateBinding BorderThickness}"
134             CornerRadius="{StaticResource TopLevelCornerRadius}">
135             <Grid>
136                 <Grid.RowDefinitions>
137                     <RowDefinition Height="*" />
138                     <RowDefinition Height="Auto" />
139                 </Grid.RowDefinitions>
140 
141                 <!--  菜单项内容区域  -->
142                 <Grid Margin="{StaticResource TopLevelContentMargin}">
143                     <Grid.ColumnDefinitions>
144                         <ColumnDefinition Width="Auto" />
145                         <ColumnDefinition Width="*" />
146                     </Grid.ColumnDefinitions>
147 
148                     <!--  菜单项图标  -->
149                     <ContentPresenter
150                         x:Name="Icon"
151                         Grid.Column="0"
152                         Margin="0,0,6,0"
153                         VerticalAlignment="Center"
154                         Content="{TemplateBinding Icon}"
155                         SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
156 
157                     <!--  菜单项标题  -->
158                     <ContentPresenter
159                         x:Name="HeaderPresenter"
160                         Grid.Column="1"
161                         Margin="{TemplateBinding Padding}"
162                         VerticalAlignment="Center"
163                         ContentSource="Header"
164                         RecognizesAccessKey="True"
165                         SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
166                         TextElement.Foreground="{TemplateBinding Foreground}" />
167                 </Grid>
168 
169                 <!--  子菜单弹出窗口(使用自定义FluentPopup)  -->
170                 <local:FluentPopup
171                     x:Name="PART_Popup"
172                     Grid.Row="1"
173                     Grid.Column="0"
174                     Focusable="False"
175                     HorizontalOffset="-12"
176                     IsOpen="{TemplateBinding IsSubmenuOpen}"
177                     Placement="Bottom"
178                     PlacementTarget="{Binding ElementName=Border}"
179                     PopupAnimation="None"
180                     VerticalOffset="1">
181                     <Grid
182                         x:Name="SubmenuBorder"
183                         Background="{DynamicResource PopupWindowBackground}"
184                         SnapsToDevicePixels="True">
185                         <!--  子菜单动画变换  -->
186                         <Grid.RenderTransform>
187                             <TranslateTransform />
188                         </Grid.RenderTransform>
189                         <ScrollViewer
190                             Padding="{StaticResource MenuBorderPadding}"
191                             CanContentScroll="True"
192                             Style="{StaticResource MenuItemScrollViewerStyle}">
193                             <Grid>
194                                 <!--  子菜单项呈现器  -->
195                                 <ItemsPresenter
196                                     x:Name="ItemsPresenter"
197                                     Grid.IsSharedSizeScope="True"
198                                     KeyboardNavigation.DirectionalNavigation="Cycle"
199                                     KeyboardNavigation.TabNavigation="Cycle"
200                                     SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
201                             </Grid>
202                         </ScrollViewer>
203                     </Grid>
204                 </local:FluentPopup>
205             </Grid>
206         </Border>
207         <ControlTemplate.Triggers>
208             <!--  无图标时隐藏图标区域  -->
209             <Trigger Property="Icon" Value="{x:Null}">
210                 <Setter TargetName="Icon" Property="Visibility" Value="Collapsed" />
211             </Trigger>
212             <!--  无标题时隐藏标题并移除图标边距  -->
213             <Trigger Property="Header" Value="{x:Null}">
214                 <Setter TargetName="Icon" Property="Margin" Value="0" />
215                 <Setter TargetName="HeaderPresenter" Property="Visibility" Value="Collapsed" />
216             </Trigger>
217             <!--  鼠标悬停高亮效果  -->
218             <Trigger Property="IsHighlighted" Value="True">
219                 <Setter TargetName="Border" Property="Background" Value="{DynamicResource MaskColor}" />
220             </Trigger>
221             <!--  子菜单打开时的动画  -->
222             <Trigger Property="IsSubmenuOpen" Value="True">
223                 <Trigger.EnterActions>
224                     <BeginStoryboard>
225                         <Storyboard>
226                             <DoubleAnimation
227                                 Storyboard.TargetName="SubmenuBorder"
228                                 Storyboard.TargetProperty="(Border.RenderTransform).(TranslateTransform.Y)"
229                                 From="-45"
230                                 To="0"
231                                 Duration="{StaticResource MenuAnimationDuration}">
232                                 <DoubleAnimation.EasingFunction>
233                                     <CircleEase EasingMode="EaseOut" />
234                                 </DoubleAnimation.EasingFunction>
235                             </DoubleAnimation>
236                         </Storyboard>
237                     </BeginStoryboard>
238                 </Trigger.EnterActions>
239             </Trigger>
240         </ControlTemplate.Triggers>
241     </ControlTemplate>
242 
243     <!--  顶级菜单项模板(无子菜单)  -->
244     <ControlTemplate x:Key="{x:Static MenuItem.TopLevelItemTemplateKey}" TargetType="{x:Type MenuItem}">
245         <Border
246             x:Name="Border"
247             Margin="{StaticResource TopLevelItemMargin}"
248             Background="{TemplateBinding Background}"
249             BorderBrush="{TemplateBinding BorderBrush}"
250             BorderThickness="{TemplateBinding BorderThickness}"
251             CornerRadius="{StaticResource TopLevelCornerRadius}">
252             <Grid Margin="{StaticResource TopLevelContentMargin}">
253                 <Grid.ColumnDefinitions>
254                     <ColumnDefinition Width="Auto" />
255                     <ColumnDefinition Width="*" />
256                 </Grid.ColumnDefinitions>
257 
258                 <!--  图标区域  -->
259                 <ContentPresenter
260                     x:Name="Icon"
261                     Grid.Column="0"
262                     Margin="0,0,6,0"
263                     VerticalAlignment="Center"
264                     Content="{TemplateBinding Icon}"
265                     SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
266 
267                 <!--  标题区域  -->
268                 <ContentPresenter
269                     x:Name="HeaderPresenter"
270                     Grid.Column="1"
271                     Margin="{TemplateBinding Padding}"
272                     VerticalAlignment="Center"
273                     ContentSource="Header"
274                     RecognizesAccessKey="True"
275                     SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
276                     TextElement.Foreground="{TemplateBinding Foreground}" />
277             </Grid>
278         </Border>
279         <ControlTemplate.Triggers>
280             <!--  鼠标悬停效果  -->
281             <Trigger Property="IsHighlighted" Value="True">
282                 <Setter TargetName="Border" Property="Background" Value="{DynamicResource MaskColor}" />
283             </Trigger>
284             <Trigger Property="Icon" Value="{x:Null}">
285                 <Setter TargetName="Icon" Property="Visibility" Value="Collapsed" />
286             </Trigger>
287             <Trigger Property="Header" Value="{x:Null}">
288                 <Setter TargetName="Icon" Property="Margin" Value="0" />
289                 <Setter TargetName="HeaderPresenter" Property="Visibility" Value="Collapsed" />
290             </Trigger>
291         </ControlTemplate.Triggers>
292     </ControlTemplate>
293 
294     <!--  子菜单项模板(无子级)  -->
295     <ControlTemplate x:Key="{x:Static MenuItem.SubmenuItemTemplateKey}" TargetType="{x:Type MenuItem}">
296         <Border
297             x:Name="Border"
298             Margin="{StaticResource MenuItemMargin}"
299             Background="{TemplateBinding Background}"
300             BorderBrush="{TemplateBinding BorderBrush}"
301             BorderThickness="{TemplateBinding BorderThickness}"
302             CornerRadius="{StaticResource MenuCornerRadius}">
303             <Grid Margin="{StaticResource MenuItemContentPadding}">
304                 <Grid.ColumnDefinitions>
305                     <!--  图标/复选框列,使用共享大小组确保对齐  -->
306                     <ColumnDefinition Width="Auto" SharedSizeGroup="MenuItemIconCol" />
307                     <!--  标题内容列  -->
308                     <ColumnDefinition Width="*" />
309                     <!--  快捷键提示列,使用共享大小组确保对齐  -->
310                     <ColumnDefinition Width="Auto" SharedSizeGroup="MenuItemRightPartCol" />
311                 </Grid.ColumnDefinitions>
312 
313                 <!--  复选框图标容器  -->
314                 <Border
315                     x:Name="CheckBoxIconBorder"
316                     Grid.Column="0"
317                     VerticalAlignment="Center"
318                     Visibility="Collapsed">
319                     <Path
320                         x:Name="CheckBoxIcon"
321                         Width="10"
322                         Height="10"
323                         HorizontalAlignment="Left"
324                         VerticalAlignment="Center"
325                         Fill="{TemplateBinding Foreground}"
326                         Stretch="Uniform" />
327                 </Border>
328 
329                 <!--  自定义图标  -->
330                 <ContentPresenter
331                     x:Name="Icon"
332                     Grid.Column="0"
333                     Margin="0,0,6,0"
334                     VerticalAlignment="Center"
335                     Content="{TemplateBinding Icon}"
336                     SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
337 
338                 <!--  菜单项标题内容  -->
339                 <ContentPresenter
340                     Grid.Column="1"
341                     Margin="{TemplateBinding Padding}"
342                     VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
343                     ContentSource="Header"
344                     RecognizesAccessKey="True"
345                     SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
346                     TextElement.Foreground="{TemplateBinding Foreground}" />
347 
348                 <!--  快捷键提示文本  -->
349                 <TextBlock
350                     x:Name="InputGestureText"
351                     Grid.Column="2"
352                     Margin="25,0,0,0"
353                     VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
354                     DockPanel.Dock="Right"
355                     FontSize="11"
356                     Opacity="0.67"
357                     Text="{TemplateBinding InputGestureText}" />
358             </Grid>
359         </Border>
360         <ControlTemplate.Triggers>
361             <!--  高亮状态(鼠标悬停)  -->
362             <Trigger Property="IsHighlighted" Value="True">
363                 <Setter TargetName="Border" Property="Background" Value="{DynamicResource MaskColor}" />
364             </Trigger>
365             <!--  无自定义图标时隐藏图标区域  -->
366             <Trigger Property="Icon" Value="{x:Null}">
367                 <Setter TargetName="Icon" Property="Visibility" Value="Collapsed" />
368             </Trigger>
369             <!--  可复选时显示复选框图标容器  -->
370             <Trigger Property="IsCheckable" Value="True">
371                 <Setter TargetName="CheckBoxIconBorder" Property="Visibility" Value="Visible" />
372             </Trigger>
373             <!--  已选中时显示复选标记  -->
374             <Trigger Property="IsChecked" Value="True">
375                 <Setter TargetName="CheckBoxIcon" Property="Data" Value="{StaticResource CheckGraph}" />
376             </Trigger>
377             <!--  无快捷键时隐藏快捷键提示  -->
378             <Trigger Property="InputGestureText" Value="">
379                 <Setter TargetName="InputGestureText" Property="Visibility" Value="Collapsed" />
380             </Trigger>
381         </ControlTemplate.Triggers>
382     </ControlTemplate>
383 
384     <!--  子菜单头部模板(带下级子菜单)  -->
385     <ControlTemplate x:Key="{x:Static MenuItem.SubmenuHeaderTemplateKey}" TargetType="{x:Type MenuItem}">
386         <Grid>
387             <Grid.RowDefinitions>
388                 <RowDefinition Height="*" />
389                 <RowDefinition Height="Auto" />
390             </Grid.RowDefinitions>
391 
392             <!--  菜单项外观  -->
393             <Border
394                 x:Name="Border"
395                 Grid.Row="1"
396                 Height="{TemplateBinding Height}"
397                 Margin="{StaticResource MenuItemMargin}"
398                 Background="Transparent"
399                 CornerRadius="{StaticResource MenuCornerRadius}">
400                 <Grid x:Name="MenuItemContent" Margin="{StaticResource MenuItemContentPadding}">
401                     <Grid.ColumnDefinitions>
402                         <ColumnDefinition Width="Auto" SharedSizeGroup="MenuItemIconCol" />
403                         <ColumnDefinition Width="*" />
404                         <ColumnDefinition Width="Auto" SharedSizeGroup="MenuItemRightPartCol" />
405                     </Grid.ColumnDefinitions>
406 
407                     <!--  图标  -->
408                     <ContentPresenter
409                         x:Name="Icon"
410                         Grid.Column="0"
411                         Margin="0,0,6,0"
412                         VerticalAlignment="Center"
413                         Content="{TemplateBinding Icon}"
414                         SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
415 
416                     <!--  标题  -->
417                     <ContentPresenter
418                         x:Name="HeaderHost"
419                         Grid.Column="1"
420                         Margin="{TemplateBinding Padding}"
421                         VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
422                         ContentSource="Header"
423                         RecognizesAccessKey="True"
424                         SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
425 
426                     <!--  右箭头指示器(表示有子菜单)  -->
427                     <Path
428                         Grid.Column="2"
429                         Width="10"
430                         Height="10"
431                         Margin="0,0,4,0"
432                         HorizontalAlignment="Right"
433                         VerticalAlignment="Center"
434                         Data="{StaticResource ForwardGraph}"
435                         Fill="{TemplateBinding Foreground}"
436                         Opacity="0.67"
437                         Stretch="Uniform" />
438                 </Grid>
439             </Border>
440 
441             <!--  子菜单弹出窗口(向右展开)  -->
442             <local:FluentPopup
443                 x:Name="PART_Popup"
444                 Grid.Row="1"
445                 Focusable="False"
446                 IsOpen="{TemplateBinding IsSubmenuOpen}"
447                 Placement="Right"
448                 PlacementTarget="{Binding ElementName=MenuItemContent}"
449                 PopupAnimation="None">
450                 <Grid x:Name="PopupRoot" Background="{DynamicResource PopupWindowBackground}">
451                     <!--  子菜单动画变换  -->
452                     <Grid.RenderTransform>
453                         <TranslateTransform />
454                     </Grid.RenderTransform>
455                     <ScrollViewer
456                         Padding="{StaticResource MenuBorderPadding}"
457                         CanContentScroll="True"
458                         Style="{StaticResource MenuItemScrollViewerStyle}">
459                         <!--  子菜单项容器  -->
460                         <ItemsPresenter
461                             x:Name="ItemsPresenter"
462                             Grid.IsSharedSizeScope="True"
463                             KeyboardNavigation.DirectionalNavigation="Cycle"
464                             KeyboardNavigation.TabNavigation="Cycle"
465                             SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
466                     </ScrollViewer>
467                 </Grid>
468             </local:FluentPopup>
469         </Grid>
470         <ControlTemplate.Triggers>
471             <!--  无图标时优化布局  -->
472             <Trigger Property="Icon" Value="{x:Null}">
473                 <Setter TargetName="Icon" Property="Visibility" Value="Collapsed" />
474                 <Setter TargetName="Icon" Property="Margin" Value="0" />
475             </Trigger>
476             <!--  高亮效果  -->
477             <Trigger Property="IsHighlighted" Value="true">
478                 <Setter TargetName="Border" Property="Background" Value="{DynamicResource MaskColor}" />
479             </Trigger>
480             <!--  子菜单打开动画  -->
481             <Trigger Property="IsSubmenuOpen" Value="True">
482                 <Trigger.EnterActions>
483                     <BeginStoryboard>
484                         <Storyboard>
485                             <!--  Y轴平移动画:从-45向下滑入到0  -->
486                             <DoubleAnimation
487                                 Storyboard.TargetName="PopupRoot"
488                                 Storyboard.TargetProperty="(Grid.RenderTransform).(TranslateTransform.Y)"
489                                 From="-45"
490                                 To="0"
491                                 Duration="{StaticResource MenuAnimationDuration}">
492                                 <DoubleAnimation.EasingFunction>
493                                     <CircleEase EasingMode="EaseOut" />
494                                 </DoubleAnimation.EasingFunction>
495                             </DoubleAnimation>
496                         </Storyboard>
497                     </BeginStoryboard>
498                 </Trigger.EnterActions>
499             </Trigger>
500         </ControlTemplate.Triggers>
501     </ControlTemplate>
502 
503     <!--  默认菜单项样式  -->
504     <Style x:Key="DefaultMenuItemStyle" TargetType="{x:Type MenuItem}">
505         <Setter Property="FocusVisualStyle" Value="{DynamicResource DefaultCollectionFocusVisualStyle}" />
506         <Setter Property="KeyboardNavigation.IsTabStop" Value="True" />
507         <Setter Property="Background" Value="Transparent" />
508         <Setter Property="BorderBrush" Value="Transparent" />
509         <Setter Property="BorderThickness" Value="1" />
510         <Setter Property="Focusable" Value="True" />
511         <Setter Property="OverridesDefaultStyle" Value="True" />
512         <Style.Triggers>
513             <!--  根据菜单项角色应用不同模板  -->
514 
515             <!--  顶级菜单项(带子菜单)  -->
516             <Trigger Property="Role" Value="TopLevelHeader">
517                 <Setter Property="Template" Value="{StaticResource {x:Static MenuItem.TopLevelHeaderTemplateKey}}" />
518                 <Setter Property="Grid.IsSharedSizeScope" Value="True" />
519                 <Setter Property="Height" Value="{x:Static sys:Double.NaN}" />
520             </Trigger>
521 
522             <!--  顶级菜单项(无子菜单)  -->
523             <Trigger Property="Role" Value="TopLevelItem">
524                 <Setter Property="Template" Value="{StaticResource {x:Static MenuItem.TopLevelItemTemplateKey}}" />
525                 <Setter Property="Height" Value="{x:Static sys:Double.NaN}" />
526             </Trigger>
527 
528             <!--  子菜单项(带子菜单)  -->
529             <Trigger Property="Role" Value="SubmenuHeader">
530                 <Setter Property="Template" Value="{StaticResource {x:Static MenuItem.SubmenuHeaderTemplateKey}}" />
531             </Trigger>
532 
533             <!--  子菜单项(无子菜单)  -->
534             <Trigger Property="Role" Value="SubmenuItem">
535                 <Setter Property="Template" Value="{StaticResource {x:Static MenuItem.SubmenuItemTemplateKey}}" />
536             </Trigger>
537         </Style.Triggers>
538     </Style>
539 
540 </ResourceDictionary>

View Code

三、应用模板和样式到全局

将上面的资源字典合并到应用程序资源中,然后为ContextMenu和MenuItem指定默认样式:

复制代码
 <Style BasedOn="{StaticResource DefaultContextMenuStyle}" TargetType="{x:Type ContextMenu}">
     <Style.Setters>
         <Setter Property="local:FluentTooltip.UseFluentStyle" Value="True" />
         <Setter Property="Background" Value="{DynamicResource PopupWindowBackground}" />
         <Setter Property="Foreground" Value="{DynamicResource ForeColor}" />
     </Style.Setters>
 </Style>
 <Style BasedOn="{StaticResource DefaultMenuItemStyle}" TargetType="MenuItem">
     <Setter Property="Height" Value="36" />
     <Setter Property="Foreground" Value="{DynamicResource ForeColor}" />
     <Setter Property="VerticalContentAlignment" Value="Center" />
 </Style>

注意,ContextMenu需要使用之前文章中的FluentTooltip.UseFluentStyle来实现亚克力材质特效。其内部原理都是反射获取popup的hwnd句柄,然后附加WindowMaterial特效。

参考文档

  1. ContextMenu Styles and Templates WPF | Microsoft Learn

  2. Menu Styles and Templates WPF | Microsoft Learn

  3. PresentationFramework.Fluent/Themes/Fluent.xaml | GitHub

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名TwilightLemon(https://blog.twlmgatito.cn),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

文章及其代码仓库可能不时更新,查看原文:WPF 为ContextMenu使用Fluent风格的亚克力材质特效 - Twlm's Blog