WPF 已知问题 Separator 无法应用 ContextMenu 定义的默认样式

2023-04-07 08:42:18 浏览数 (2)

本文记录一个 WPF 已知问题,在 ContextMenu 的 Resources 里定义 Separator 的默认样式,在 ContextMenu 里面的 Separator 将应用不上,或者说不会找到此默认的样式。需要明确给 Style 一个 Key 且在 Separator 写明此 Key 才能应用上

如下面的例子,在 ContextMenu 的 Resources 资源里面定义了 Separator 的默认样式

代码语言:javascript复制
            <ContextMenu>
                <ContextMenu.Resources>
                    <Style TargetType="Separator">
                        <Setter Property="Margin" Value="10,10,10,10"></Setter>
                    </Style>
                </ContextMenu.Resources>
            </ContextMenu>

接着在 ContextMenu 里面存放一个 Separator 元素,可以看到此 Separator 元素没有使用或者说找到定义的样式,视觉上就是 Margin 没有生效

代码语言:javascript复制
            <ContextMenu>
                <ContextMenu.Resources>
                    <Style TargetType="Separator">
                        <Setter Property="Margin" Value="10,10,10,10"></Setter>
                    </Style>
                </ContextMenu.Resources>
                <Separator></Separator>
            </ContextMenu>

而如果给 Separator 的 Style 加上 Key 且在 Separator 写明了此 Key 那么将可以成功应用上,如下面代码

代码语言:javascript复制
            <ContextMenu>
                <ContextMenu.Resources>
                    <Style x:Key="SeparatorStyle" TargetType="Separator">
                        <Setter Property="Margin" Value="10,10,10,10"></Setter>
                    </Style>
                </ContextMenu.Resources>
                <Separator Style="{StaticResource SeparatorStyle}"></Separator>
            </ContextMenu>

此问题是在 WPF 代码里面写了特殊判断逻辑,预计是有我没有理解的坑才如此做。感谢 少珺 工具人帮我找到了在 WPF 框架里面的问题

为了方便说明问题,我将给出可以运行的测试代码,此测试代码可以在本文末尾找到项目的下载

新建一个 WPF 项目,编辑主窗口,添加以下代码

代码语言:javascript复制
    <Grid Background="White" MouseDown="Grid_OnMouseDown">
        <Grid.ContextMenu>
            <ContextMenu>
                <ContextMenu.Resources>
                    <Style TargetType="Separator">
                        <Setter Property="Margin" Value="10,10,10,10"></Setter>
                    </Style>
                    <Style x:Key="SeparatorStyle" TargetType="Separator">
                        <Setter Property="Margin" Value="10,10,10,10"></Setter>
                    </Style>
                </ContextMenu.Resources>
                <Menu>1</Menu>
                <Menu>2</Menu>
                <Menu>3</Menu>
                <Separator></Separator>
                <Menu>1</Menu>
                <Menu>2</Menu>
                <Menu>3</Menu>
                <Separator Style="{StaticResource SeparatorStyle}"></Separator>
                <Menu>1</Menu>
                <Menu>2</Menu>
                <Menu>3</Menu>
            </ContextMenu>
        </Grid.ContextMenu>
    </Grid>

对应的后台代码如下

代码语言:javascript复制
    private void Grid_OnMouseDown(object sender, MouseButtonEventArgs e)
    {
        var grid = (Grid)sender;
        grid.ContextMenu.IsOpen = true;
    }

此时点击窗口内容,即可看到弹出了菜单

弹出的菜单的两条分割线的 Margin 是不相同的

根本原因是在 WPF 里面,对于在 Menu 里面的 Separator 采用的是如下逻辑,以下代码可以从 WPF 官方开源仓库 https://github.com/dotnet/wpf/blob/1aab9e3f42dbf550797bff97a32f2dbfb61a3198/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/MenuItem.cs#L1344-L1353 找到。我此问题报告给 WPF 官方,请看 https://github.com/dotnet/wpf/issues/7268

代码语言:javascript复制
                Separator separator = item as Separator;
                if (separator != null)
                {
                    bool hasModifiers;
                    BaseValueSourceInternal vs = separator.GetValueSource(StyleProperty, null, out hasModifiers);
                    if (vs <= BaseValueSourceInternal.ImplicitReference)
                        separator.SetResourceReference(StyleProperty, SeparatorStyleKey);

                    separator.DefaultStyleKey = SeparatorStyleKey;
                }

从上面代码可以看到,判断如果样式是小于等于 ImplicitReference 优先级的,那就采用默认的 SeparatorStyleKey 作为样式属性。如果没有在代码里面明确给定资源的 Key 内容,那以上代码的 vs 就是 ImplicitReference 优先级,于是样式就被修改为默认的主题样式

这是在 WPF 里面特别给定的代码,也许是大佬们为了修复某个我理解不了的坑

本文的代码放在github 和 gitee 欢迎访问

可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

代码语言:javascript复制
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin b820847a1af20370de28a1e73e32df9561a98ecc

以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码

代码语言:javascript复制
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin b820847a1af20370de28a1e73e32df9561a98ecc

获取代码之后,进入 HayhachujedaKikunayreefee 文件夹

更多 WPF 已知问题请参阅 博客导航


大佬 czdietrich 告诉我说,这是特意的。原因是 Separator 在很多个地方都会使用,如果在 Menu 里,也采用默认继承的关系,将会让 Separator 的样式打架。毕竟在 Menu 里面的,是期望比较特殊一些的。于是就限制了,如果是放在 Menu 里面,应该采用的是 MenuItem.SeparatorStyleKey 样式的才会作为默认的继承样式,如下面代码

代码语言:javascript复制
<Window x:Class="TestContextMenuSeparatorStyle.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    
    <Window.Resources>
        <Style x:Key="{x:Static MenuItem.SeparatorStyleKey}" TargetType="Separator">
            <Setter Property="Background" Value="Tomato" />
        </Style>
    </Window.Resources>
    
    <Grid Background="White">
        <Grid.ContextMenu>
            <ContextMenu>
                <MenuItem Header="Item 1" />
                <Separator />
                <MenuItem Header="Item 2" />
            </ContextMenu>
        </Grid.ContextMenu>
    </Grid>
</Window>

以上的代码也符合 WPF 的逻辑,详细请看 https://github.com/dotnet/wpf/issues/7268#issuecomment-1315303718

0 人点赞