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 ControlTemplateGrid 中添加了 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] 的事情之一=].实现该功能确实超出了原始问题的范围,但希望这至少能说明他们是如何实现的。