Visual Studio这种MenuItem的绘制是如何实现的?
How does Visual Studio Achieve This Kind of MenuItem Drawing?
我正在为我的应用程序创建一个黑暗 ui,并且在使用 Visual Studio 作为参考点时遇到了一些有趣的事情。我注意到他们呈现他们的 MenuItems 几乎就像他们是 Tabcontrol 中的 Tabs 一样。这是一张照片:
这是我的样子:
我知道这可能很难看清,因为所有东西的颜色都差不多,所以我继续制作了另一张修改过的图片以更好地突出该区域。
如您所见,Visual studio 在 MenuItem 周围绘制边框,然后不在其正下方为下拉子项绘制边框。 Visual Studio 是怎么做到的呢?我怎样才能实现它?这是我的模板:
<Style x:Key="{x:Type Menu}" TargetType="Menu">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Foreground" Value="#f1f1f1" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Menu">
<Border x:Name="MainMenu" Background="#2d2d30">
<StackPanel
ClipToBounds="True"
IsItemsHost="True"
Orientation="Horizontal" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ControlTemplate x:Key="MenuItemControlTemplate1" TargetType="{x:Type MenuItem}">
<Border
x:Name="templateRoot"
Height="16"
Background="{TemplateBinding Background}"
BorderBrush="#535353"
SnapsToDevicePixels="True">
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter
Grid.Column="1"
Margin="{TemplateBinding Padding}"
Content="{TemplateBinding Header}"
ContentSource="Header"
ContentStringFormat="{TemplateBinding HeaderStringFormat}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
<Popup
x:Name="PART_Popup"
AllowsTransparency="True"
Focusable="False"
IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}"
Placement="Bottom"
PopupAnimation="{DynamicResource {x:Static SystemParameters.MenuPopupAnimationKey}}">
<Border
x:Name="SubMenuBorder"
Padding="2"
Background="#1b1b1c"
BorderBrush="#595959"
BorderThickness="1">
<ScrollViewer x:Name="SubMenuScrollViewer" Style="{DynamicResource {ComponentResourceKey ResourceId=MenuScrollViewer, TypeInTargetAssembly={x:Type FrameworkElement}}}">
<Grid RenderOptions.ClearTypeHint="Enabled">
<Canvas
Width="0"
Height="0"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<Rectangle
x:Name="OpaqueRect"
Width="{Binding ActualWidth, ElementName=SubMenuBorder}"
Height="{Binding ActualHeight, ElementName=SubMenuBorder}"
Fill="{Binding Background, ElementName=SubMenuBorder}" />
</Canvas>
<ItemsPresenter
x:Name="ItemsPresenter"
Grid.IsSharedSizeScope="True"
KeyboardNavigation.DirectionalNavigation="Cycle"
KeyboardNavigation.TabNavigation="Cycle"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Grid>
</ScrollViewer>
</Border>
</Popup>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSuspendingPopupAnimation" Value="True">
<Setter TargetName="PART_Popup" Property="PopupAnimation" Value="None" />
</Trigger>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="templateRoot" Property="Background" Value="#3e3e40" />
<Setter TargetName="templateRoot" Property="BorderBrush" Value="#2C2C2C" />
</Trigger>
<Trigger SourceName="SubMenuScrollViewer" Property="CanContentScroll" Value="False">
<Setter TargetName="OpaqueRect" Property="Canvas.Top" Value="{Binding VerticalOffset, ElementName=SubMenuScrollViewer}" />
<Setter TargetName="OpaqueRect" Property="Canvas.Left" Value="{Binding HorizontalOffset, ElementName=SubMenuScrollViewer}" />
</Trigger>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter TargetName="templateRoot" Property="Background" Value="#1b1b1c" />
<Setter Property="Header" Value="Test" />
<Setter Property="BorderBrush" Value="#2C2C2C" />
<Setter Property="BorderThickness" Value="1" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="MenuItemControlTemplate2" TargetType="{x:Type MenuItem}">
<Border
x:Name="templateRoot"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="True">
<Grid Margin="-1">
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="Auto"
MinWidth="22"
SharedSizeGroup="MenuItemIconColumnGroup" />
<ColumnDefinition Width="13" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="30" />
<ColumnDefinition Width="Auto" SharedSizeGroup="MenuItemIGTColumnGroup" />
<ColumnDefinition Width="20" />
</Grid.ColumnDefinitions>
<ContentPresenter
x:Name="Icon"
Width="16"
Height="16"
Margin="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{TemplateBinding Icon}"
ContentSource="Icon"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
<Border
x:Name="GlyphPanel"
Width="22"
Height="22"
Margin="-1,0,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="#3D26A0DA"
BorderBrush="#FF26A0DA"
BorderThickness="1"
ClipToBounds="False"
Visibility="Hidden">
<Path
x:Name="Glyph"
Width="10"
Height="11"
Data="F1M10,1.2L4.7,9.1 4.5,9.1 0,5.2 1.3,3.5 4.3,6.1 8.3,0 10,1.2z"
Fill="#FF212121"
FlowDirection="LeftToRight" />
</Border>
<ContentPresenter
x:Name="menuHeaderContainer"
Grid.Column="2"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{TemplateBinding Header}"
ContentSource="Header"
ContentStringFormat="{TemplateBinding HeaderStringFormat}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
<TextBlock
x:Name="menuGestureText"
Grid.Column="4"
Margin="{TemplateBinding Padding}"
VerticalAlignment="Center"
Opacity="0.7"
Text="{TemplateBinding InputGestureText}" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="Icon" Value="{x:Null}">
<Setter TargetName="Icon" Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="GlyphPanel" Property="Visibility" Value="Visible" />
<Setter TargetName="Icon" Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="templateRoot" Property="BorderBrush" Value="Orange" />
<Setter TargetName="templateRoot" Property="Background" Value="Yellow" />
<Setter TargetName="menuHeaderContainer" Property="TextBlock.Foreground" Value="Black" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="templateRoot" Property="TextElement.Foreground" Value="#FF707070" />
<Setter TargetName="Glyph" Property="Fill" Value="#FF707070" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsHighlighted" Value="True" />
<Condition Property="IsEnabled" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="templateRoot" Property="Background" Value="#0A000000" />
<Setter TargetName="templateRoot" Property="BorderBrush" Value="#21000000" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
将该菜单项的背景颜色设置为您想要的颜色,将前景色设置为白色。
为了弄清楚 Visual Studio 内部发生了什么,我启动了两个 Visual Studio 2017 实例并将一个附加到另一个进程,这使我可以使用实时可视化树工具来检查控件(大概您也可以为此使用 Snoop)。
事实证明,Visual Studio 中的菜单弹出窗口出现偏移,因此它覆盖了菜单栏,并绘制了某种小框以实现连续的选项卡外观。如果您使用属性 window 调整弹出窗口的 VerticalOffset
属性 使其与主菜单分开,这一点尤其明显。
在可视化树中找到 Popup
:
将 VerticalOffset
从原来的 -2 更改为正数:
以及生成的弹出窗口:
如果您查看现在分开的菜单弹出窗口的左上角,您应该能够看到弹出窗口的一个小扩展,当 VerticalOffset
最初为 -2 时,它与父 [=15] 重叠=] 边框创建一个类似选项卡的控件的错觉。
了解这一点,创建 Visual Studio 解决方案的基本版本就相当简单了:
<Window x:Class="MenuItemTest.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:local="clr-namespace:MenuItemTest"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="525"
Height="350"
mc:Ignorable="d">
<Window.Resources>
<local:SubtractingConverter x:Key="SubtractingConverter" />
<Style x:Key="{x:Type Menu}" TargetType="Menu">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Foreground" Value="#f1f1f1" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Menu">
<Border x:Name="MainMenu" Background="#2d2d30">
<StackPanel ClipToBounds="True"
IsItemsHost="True"
Orientation="Horizontal" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ControlTemplate x:Key="MenuItemControlTemplate1" TargetType="{x:Type MenuItem}">
<Border x:Name="templateRoot"
Height="16"
Background="{TemplateBinding Background}"
BorderBrush="#535353"
SnapsToDevicePixels="True">
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="1"
Margin="{TemplateBinding Padding}"
Content="{TemplateBinding Header}"
ContentSource="Header"
ContentStringFormat="{TemplateBinding HeaderStringFormat}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
<Popup x:Name="PART_Popup"
AllowsTransparency="True"
Focusable="False"
IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}"
Placement="Bottom"
PopupAnimation="{DynamicResource {x:Static SystemParameters.MenuPopupAnimationKey}}">
<Grid>
<Border x:Name="SubMenuBorder"
Padding="2"
Background="#1b1b1c"
BorderBrush="#595959"
BorderThickness="1">
<ScrollViewer x:Name="SubMenuScrollViewer" Style="{DynamicResource {ComponentResourceKey ResourceId=MenuScrollViewer, TypeInTargetAssembly={x:Type FrameworkElement}}}">
<Grid RenderOptions.ClearTypeHint="Enabled">
<Canvas Width="0"
Height="0"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<Rectangle x:Name="OpaqueRect"
Width="{Binding ActualWidth, ElementName=SubMenuBorder}"
Height="{Binding ActualHeight, ElementName=SubMenuBorder}"
Fill="{Binding Background, ElementName=SubMenuBorder}" />
</Canvas>
<ItemsPresenter x:Name="ItemsPresenter"
Grid.IsSharedSizeScope="True"
KeyboardNavigation.DirectionalNavigation="Cycle"
KeyboardNavigation.TabNavigation="Cycle"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Grid>
</ScrollViewer>
</Border>
<Rectangle Width="{TemplateBinding ActualWidth,
Converter={StaticResource SubtractingConverter},
ConverterParameter=1}"
Height="2"
Margin="1,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Fill="{Binding Background, ElementName=SubMenuBorder}" />
</Grid>
</Popup>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSuspendingPopupAnimation" Value="True">
<Setter TargetName="PART_Popup" Property="PopupAnimation" Value="None" />
</Trigger>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="templateRoot" Property="Background" Value="#3e3e40" />
<Setter TargetName="templateRoot" Property="BorderBrush" Value="#2C2C2C" />
</Trigger>
<Trigger SourceName="SubMenuScrollViewer" Property="CanContentScroll" Value="False">
<Setter TargetName="OpaqueRect" Property="Canvas.Top" Value="{Binding VerticalOffset, ElementName=SubMenuScrollViewer}" />
<Setter TargetName="OpaqueRect" Property="Canvas.Left" Value="{Binding HorizontalOffset, ElementName=SubMenuScrollViewer}" />
</Trigger>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter TargetName="templateRoot" Property="Background" Value="#1b1b1c" />
<Setter Property="Header" Value="Test" />
<Setter Property="BorderBrush" Value="#2C2C2C" />
<Setter Property="BorderThickness" Value="1" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="MenuItemControlTemplate2" TargetType="{x:Type MenuItem}">
<Border x:Name="templateRoot"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="True">
<Grid Margin="-1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"
MinWidth="22"
SharedSizeGroup="MenuItemIconColumnGroup" />
<ColumnDefinition Width="13" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="30" />
<ColumnDefinition Width="Auto" SharedSizeGroup="MenuItemIGTColumnGroup" />
<ColumnDefinition Width="20" />
</Grid.ColumnDefinitions>
<ContentPresenter x:Name="Icon"
Width="16"
Height="16"
Margin="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{TemplateBinding Icon}"
ContentSource="Icon"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
<Border x:Name="GlyphPanel"
Width="22"
Height="22"
Margin="-1,0,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="#3D26A0DA"
BorderBrush="#FF26A0DA"
BorderThickness="1"
ClipToBounds="False"
Visibility="Hidden">
<Path x:Name="Glyph"
Width="10"
Height="11"
Data="F1M10,1.2L4.7,9.1 4.5,9.1 0,5.2 1.3,3.5 4.3,6.1 8.3,0 10,1.2z"
Fill="#FF212121"
FlowDirection="LeftToRight" />
</Border>
<ContentPresenter x:Name="menuHeaderContainer"
Grid.Column="2"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{TemplateBinding Header}"
ContentSource="Header"
ContentStringFormat="{TemplateBinding HeaderStringFormat}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
<TextBlock x:Name="menuGestureText"
Grid.Column="4"
Margin="{TemplateBinding Padding}"
VerticalAlignment="Center"
Opacity="0.7"
Text="{TemplateBinding InputGestureText}" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="Icon" Value="{x:Null}">
<Setter TargetName="Icon" Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="GlyphPanel" Property="Visibility" Value="Visible" />
<Setter TargetName="Icon" Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="templateRoot" Property="BorderBrush" Value="Orange" />
<Setter TargetName="templateRoot" Property="Background" Value="Yellow" />
<Setter TargetName="menuHeaderContainer" Property="TextBlock.Foreground" Value="Black" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="templateRoot" Property="TextElement.Foreground" Value="#FF707070" />
<Setter TargetName="Glyph" Property="Fill" Value="#FF707070" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsHighlighted" Value="True" />
<Condition Property="IsEnabled" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="templateRoot" Property="Background" Value="#0A000000" />
<Setter TargetName="templateRoot" Property="BorderBrush" Value="#21000000" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Menu Background="#2d2d30">
<MenuItem Header="Tools" Template="{StaticResource MenuItemControlTemplate1}">
<MenuItem Padding="0"
Background="#2d2d30"
Header="Test"
Template="{StaticResource MenuItemControlTemplate2}" />
</MenuItem>
<MenuItem Header="Whatever" Template="{StaticResource MenuItemControlTemplate1}">
<MenuItem Padding="0"
Background="#2d2d30"
Header="Test"
Template="{StaticResource MenuItemControlTemplate2}" />
</MenuItem>
</Menu>
</Grid>
</Window>
这与您在上面提供的样式和模板基本相同,只是在 MenuItemControlTemplate1
ControlTemplate
和 Grid
中添加了 Rectangle
以便两者并且现有的 Border
可以包含在弹出窗口中。 SubtractingConverter
只是一个简单的 IValueConverter
,它从值中减去 ConverterParameter
...没什么特别的。我还继续将它放在 window 中进行测试。当我 运行 这个测试程序时,我现在得到这个:
因为我没有你所有的样式,显然不是所有的颜色都是正确的,但你会注意到你关心的菜单现在似乎是一个连续的选项卡,就像 Visual Studio.
现在这不是完整的解决方案。有明显的次要细节,例如父 "Tools" 和 "Whatever" 菜单周围缺少边框,但更关键的是,您仍然需要考虑 Popup
由于与监视器重叠而改变其位置放置 Rectangle
时的边缘。
如果将应用程序 window 移动到屏幕底部,Popup
class 将打开上方 的菜单实例"Tools" 菜单而不是下面,这显然会导致 Rectangle
放错地方。同样,当 window 再次位于屏幕右边缘时打开菜单将再次导致 Rectangle
由于弹出位置的变化而错位。即使 Visual Studio 2017 年也不能正确解释这种情况,如下所示:
现在,也许处理基本用例对您来说就足够了,在这种情况下,太棒了!如果您想进一步处理重新定位、调整大小、and/or hiding/showing 矩形,以便无论用户在什么奇怪的位置打开菜单,它看起来都很完美,那么我真的没有办法做到这一点完全没有一些实际的 C# 代码。我怀疑这至少是之前显示的 Visual Studio 的实时可视树中的 VSMenuItem
class 超出沼泽标准 MenuItem
[=87] 的事情之一=].实现该功能确实超出了原始问题的范围,但希望这至少能说明他们是如何实现的。
我正在为我的应用程序创建一个黑暗 ui,并且在使用 Visual Studio 作为参考点时遇到了一些有趣的事情。我注意到他们呈现他们的 MenuItems 几乎就像他们是 Tabcontrol 中的 Tabs 一样。这是一张照片:
这是我的样子:
我知道这可能很难看清,因为所有东西的颜色都差不多,所以我继续制作了另一张修改过的图片以更好地突出该区域。
如您所见,Visual studio 在 MenuItem 周围绘制边框,然后不在其正下方为下拉子项绘制边框。 Visual Studio 是怎么做到的呢?我怎样才能实现它?这是我的模板:
<Style x:Key="{x:Type Menu}" TargetType="Menu">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Foreground" Value="#f1f1f1" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Menu">
<Border x:Name="MainMenu" Background="#2d2d30">
<StackPanel
ClipToBounds="True"
IsItemsHost="True"
Orientation="Horizontal" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ControlTemplate x:Key="MenuItemControlTemplate1" TargetType="{x:Type MenuItem}">
<Border
x:Name="templateRoot"
Height="16"
Background="{TemplateBinding Background}"
BorderBrush="#535353"
SnapsToDevicePixels="True">
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter
Grid.Column="1"
Margin="{TemplateBinding Padding}"
Content="{TemplateBinding Header}"
ContentSource="Header"
ContentStringFormat="{TemplateBinding HeaderStringFormat}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
<Popup
x:Name="PART_Popup"
AllowsTransparency="True"
Focusable="False"
IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}"
Placement="Bottom"
PopupAnimation="{DynamicResource {x:Static SystemParameters.MenuPopupAnimationKey}}">
<Border
x:Name="SubMenuBorder"
Padding="2"
Background="#1b1b1c"
BorderBrush="#595959"
BorderThickness="1">
<ScrollViewer x:Name="SubMenuScrollViewer" Style="{DynamicResource {ComponentResourceKey ResourceId=MenuScrollViewer, TypeInTargetAssembly={x:Type FrameworkElement}}}">
<Grid RenderOptions.ClearTypeHint="Enabled">
<Canvas
Width="0"
Height="0"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<Rectangle
x:Name="OpaqueRect"
Width="{Binding ActualWidth, ElementName=SubMenuBorder}"
Height="{Binding ActualHeight, ElementName=SubMenuBorder}"
Fill="{Binding Background, ElementName=SubMenuBorder}" />
</Canvas>
<ItemsPresenter
x:Name="ItemsPresenter"
Grid.IsSharedSizeScope="True"
KeyboardNavigation.DirectionalNavigation="Cycle"
KeyboardNavigation.TabNavigation="Cycle"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Grid>
</ScrollViewer>
</Border>
</Popup>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSuspendingPopupAnimation" Value="True">
<Setter TargetName="PART_Popup" Property="PopupAnimation" Value="None" />
</Trigger>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="templateRoot" Property="Background" Value="#3e3e40" />
<Setter TargetName="templateRoot" Property="BorderBrush" Value="#2C2C2C" />
</Trigger>
<Trigger SourceName="SubMenuScrollViewer" Property="CanContentScroll" Value="False">
<Setter TargetName="OpaqueRect" Property="Canvas.Top" Value="{Binding VerticalOffset, ElementName=SubMenuScrollViewer}" />
<Setter TargetName="OpaqueRect" Property="Canvas.Left" Value="{Binding HorizontalOffset, ElementName=SubMenuScrollViewer}" />
</Trigger>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter TargetName="templateRoot" Property="Background" Value="#1b1b1c" />
<Setter Property="Header" Value="Test" />
<Setter Property="BorderBrush" Value="#2C2C2C" />
<Setter Property="BorderThickness" Value="1" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="MenuItemControlTemplate2" TargetType="{x:Type MenuItem}">
<Border
x:Name="templateRoot"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="True">
<Grid Margin="-1">
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="Auto"
MinWidth="22"
SharedSizeGroup="MenuItemIconColumnGroup" />
<ColumnDefinition Width="13" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="30" />
<ColumnDefinition Width="Auto" SharedSizeGroup="MenuItemIGTColumnGroup" />
<ColumnDefinition Width="20" />
</Grid.ColumnDefinitions>
<ContentPresenter
x:Name="Icon"
Width="16"
Height="16"
Margin="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{TemplateBinding Icon}"
ContentSource="Icon"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
<Border
x:Name="GlyphPanel"
Width="22"
Height="22"
Margin="-1,0,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="#3D26A0DA"
BorderBrush="#FF26A0DA"
BorderThickness="1"
ClipToBounds="False"
Visibility="Hidden">
<Path
x:Name="Glyph"
Width="10"
Height="11"
Data="F1M10,1.2L4.7,9.1 4.5,9.1 0,5.2 1.3,3.5 4.3,6.1 8.3,0 10,1.2z"
Fill="#FF212121"
FlowDirection="LeftToRight" />
</Border>
<ContentPresenter
x:Name="menuHeaderContainer"
Grid.Column="2"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{TemplateBinding Header}"
ContentSource="Header"
ContentStringFormat="{TemplateBinding HeaderStringFormat}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
<TextBlock
x:Name="menuGestureText"
Grid.Column="4"
Margin="{TemplateBinding Padding}"
VerticalAlignment="Center"
Opacity="0.7"
Text="{TemplateBinding InputGestureText}" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="Icon" Value="{x:Null}">
<Setter TargetName="Icon" Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="GlyphPanel" Property="Visibility" Value="Visible" />
<Setter TargetName="Icon" Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="templateRoot" Property="BorderBrush" Value="Orange" />
<Setter TargetName="templateRoot" Property="Background" Value="Yellow" />
<Setter TargetName="menuHeaderContainer" Property="TextBlock.Foreground" Value="Black" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="templateRoot" Property="TextElement.Foreground" Value="#FF707070" />
<Setter TargetName="Glyph" Property="Fill" Value="#FF707070" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsHighlighted" Value="True" />
<Condition Property="IsEnabled" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="templateRoot" Property="Background" Value="#0A000000" />
<Setter TargetName="templateRoot" Property="BorderBrush" Value="#21000000" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
将该菜单项的背景颜色设置为您想要的颜色,将前景色设置为白色。
为了弄清楚 Visual Studio 内部发生了什么,我启动了两个 Visual Studio 2017 实例并将一个附加到另一个进程,这使我可以使用实时可视化树工具来检查控件(大概您也可以为此使用 Snoop)。
事实证明,Visual Studio 中的菜单弹出窗口出现偏移,因此它覆盖了菜单栏,并绘制了某种小框以实现连续的选项卡外观。如果您使用属性 window 调整弹出窗口的 VerticalOffset
属性 使其与主菜单分开,这一点尤其明显。
在可视化树中找到 Popup
:
将 VerticalOffset
从原来的 -2 更改为正数:
以及生成的弹出窗口:
如果您查看现在分开的菜单弹出窗口的左上角,您应该能够看到弹出窗口的一个小扩展,当 VerticalOffset
最初为 -2 时,它与父 [=15] 重叠=] 边框创建一个类似选项卡的控件的错觉。
了解这一点,创建 Visual Studio 解决方案的基本版本就相当简单了:
<Window x:Class="MenuItemTest.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:local="clr-namespace:MenuItemTest"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="525"
Height="350"
mc:Ignorable="d">
<Window.Resources>
<local:SubtractingConverter x:Key="SubtractingConverter" />
<Style x:Key="{x:Type Menu}" TargetType="Menu">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Foreground" Value="#f1f1f1" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Menu">
<Border x:Name="MainMenu" Background="#2d2d30">
<StackPanel ClipToBounds="True"
IsItemsHost="True"
Orientation="Horizontal" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ControlTemplate x:Key="MenuItemControlTemplate1" TargetType="{x:Type MenuItem}">
<Border x:Name="templateRoot"
Height="16"
Background="{TemplateBinding Background}"
BorderBrush="#535353"
SnapsToDevicePixels="True">
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="1"
Margin="{TemplateBinding Padding}"
Content="{TemplateBinding Header}"
ContentSource="Header"
ContentStringFormat="{TemplateBinding HeaderStringFormat}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
<Popup x:Name="PART_Popup"
AllowsTransparency="True"
Focusable="False"
IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}"
Placement="Bottom"
PopupAnimation="{DynamicResource {x:Static SystemParameters.MenuPopupAnimationKey}}">
<Grid>
<Border x:Name="SubMenuBorder"
Padding="2"
Background="#1b1b1c"
BorderBrush="#595959"
BorderThickness="1">
<ScrollViewer x:Name="SubMenuScrollViewer" Style="{DynamicResource {ComponentResourceKey ResourceId=MenuScrollViewer, TypeInTargetAssembly={x:Type FrameworkElement}}}">
<Grid RenderOptions.ClearTypeHint="Enabled">
<Canvas Width="0"
Height="0"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<Rectangle x:Name="OpaqueRect"
Width="{Binding ActualWidth, ElementName=SubMenuBorder}"
Height="{Binding ActualHeight, ElementName=SubMenuBorder}"
Fill="{Binding Background, ElementName=SubMenuBorder}" />
</Canvas>
<ItemsPresenter x:Name="ItemsPresenter"
Grid.IsSharedSizeScope="True"
KeyboardNavigation.DirectionalNavigation="Cycle"
KeyboardNavigation.TabNavigation="Cycle"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Grid>
</ScrollViewer>
</Border>
<Rectangle Width="{TemplateBinding ActualWidth,
Converter={StaticResource SubtractingConverter},
ConverterParameter=1}"
Height="2"
Margin="1,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Fill="{Binding Background, ElementName=SubMenuBorder}" />
</Grid>
</Popup>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSuspendingPopupAnimation" Value="True">
<Setter TargetName="PART_Popup" Property="PopupAnimation" Value="None" />
</Trigger>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="templateRoot" Property="Background" Value="#3e3e40" />
<Setter TargetName="templateRoot" Property="BorderBrush" Value="#2C2C2C" />
</Trigger>
<Trigger SourceName="SubMenuScrollViewer" Property="CanContentScroll" Value="False">
<Setter TargetName="OpaqueRect" Property="Canvas.Top" Value="{Binding VerticalOffset, ElementName=SubMenuScrollViewer}" />
<Setter TargetName="OpaqueRect" Property="Canvas.Left" Value="{Binding HorizontalOffset, ElementName=SubMenuScrollViewer}" />
</Trigger>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter TargetName="templateRoot" Property="Background" Value="#1b1b1c" />
<Setter Property="Header" Value="Test" />
<Setter Property="BorderBrush" Value="#2C2C2C" />
<Setter Property="BorderThickness" Value="1" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="MenuItemControlTemplate2" TargetType="{x:Type MenuItem}">
<Border x:Name="templateRoot"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="True">
<Grid Margin="-1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"
MinWidth="22"
SharedSizeGroup="MenuItemIconColumnGroup" />
<ColumnDefinition Width="13" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="30" />
<ColumnDefinition Width="Auto" SharedSizeGroup="MenuItemIGTColumnGroup" />
<ColumnDefinition Width="20" />
</Grid.ColumnDefinitions>
<ContentPresenter x:Name="Icon"
Width="16"
Height="16"
Margin="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{TemplateBinding Icon}"
ContentSource="Icon"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
<Border x:Name="GlyphPanel"
Width="22"
Height="22"
Margin="-1,0,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="#3D26A0DA"
BorderBrush="#FF26A0DA"
BorderThickness="1"
ClipToBounds="False"
Visibility="Hidden">
<Path x:Name="Glyph"
Width="10"
Height="11"
Data="F1M10,1.2L4.7,9.1 4.5,9.1 0,5.2 1.3,3.5 4.3,6.1 8.3,0 10,1.2z"
Fill="#FF212121"
FlowDirection="LeftToRight" />
</Border>
<ContentPresenter x:Name="menuHeaderContainer"
Grid.Column="2"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{TemplateBinding Header}"
ContentSource="Header"
ContentStringFormat="{TemplateBinding HeaderStringFormat}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
<TextBlock x:Name="menuGestureText"
Grid.Column="4"
Margin="{TemplateBinding Padding}"
VerticalAlignment="Center"
Opacity="0.7"
Text="{TemplateBinding InputGestureText}" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="Icon" Value="{x:Null}">
<Setter TargetName="Icon" Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="GlyphPanel" Property="Visibility" Value="Visible" />
<Setter TargetName="Icon" Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="templateRoot" Property="BorderBrush" Value="Orange" />
<Setter TargetName="templateRoot" Property="Background" Value="Yellow" />
<Setter TargetName="menuHeaderContainer" Property="TextBlock.Foreground" Value="Black" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="templateRoot" Property="TextElement.Foreground" Value="#FF707070" />
<Setter TargetName="Glyph" Property="Fill" Value="#FF707070" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsHighlighted" Value="True" />
<Condition Property="IsEnabled" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="templateRoot" Property="Background" Value="#0A000000" />
<Setter TargetName="templateRoot" Property="BorderBrush" Value="#21000000" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Menu Background="#2d2d30">
<MenuItem Header="Tools" Template="{StaticResource MenuItemControlTemplate1}">
<MenuItem Padding="0"
Background="#2d2d30"
Header="Test"
Template="{StaticResource MenuItemControlTemplate2}" />
</MenuItem>
<MenuItem Header="Whatever" Template="{StaticResource MenuItemControlTemplate1}">
<MenuItem Padding="0"
Background="#2d2d30"
Header="Test"
Template="{StaticResource MenuItemControlTemplate2}" />
</MenuItem>
</Menu>
</Grid>
</Window>
这与您在上面提供的样式和模板基本相同,只是在 MenuItemControlTemplate1
ControlTemplate
和 Grid
中添加了 Rectangle
以便两者并且现有的 Border
可以包含在弹出窗口中。 SubtractingConverter
只是一个简单的 IValueConverter
,它从值中减去 ConverterParameter
...没什么特别的。我还继续将它放在 window 中进行测试。当我 运行 这个测试程序时,我现在得到这个:
因为我没有你所有的样式,显然不是所有的颜色都是正确的,但你会注意到你关心的菜单现在似乎是一个连续的选项卡,就像 Visual Studio.
现在这不是完整的解决方案。有明显的次要细节,例如父 "Tools" 和 "Whatever" 菜单周围缺少边框,但更关键的是,您仍然需要考虑 Popup
由于与监视器重叠而改变其位置放置 Rectangle
时的边缘。
如果将应用程序 window 移动到屏幕底部,Popup
class 将打开上方 的菜单实例"Tools" 菜单而不是下面,这显然会导致 Rectangle
放错地方。同样,当 window 再次位于屏幕右边缘时打开菜单将再次导致 Rectangle
由于弹出位置的变化而错位。即使 Visual Studio 2017 年也不能正确解释这种情况,如下所示:
现在,也许处理基本用例对您来说就足够了,在这种情况下,太棒了!如果您想进一步处理重新定位、调整大小、and/or hiding/showing 矩形,以便无论用户在什么奇怪的位置打开菜单,它看起来都很完美,那么我真的没有办法做到这一点完全没有一些实际的 C# 代码。我怀疑这至少是之前显示的 Visual Studio 的实时可视树中的 VSMenuItem
class 超出沼泽标准 MenuItem
[=87] 的事情之一=].实现该功能确实超出了原始问题的范围,但希望这至少能说明他们是如何实现的。