从附件 属性 中设置样式中的 WPF MenuItem 图标

Set WPF MenuItem Icon in Style from attached property

我正在尝试将一些 XAML 定义 MenuItem 移动到 Style.

我有以下工作 XAML:

<Menu x:Name="menu" Height="19" Margin="10,10,10.333,0" VerticalAlignment="Top">
        <MenuItem Header="_Open">
            <MenuItem.Icon>
                <Viewbox>
                    <ContentControl Content="{DynamicResource appbar.folder.open}" RenderTransformOrigin="0.5,0.5">
                        <ContentControl.RenderTransform>
                            <TransformGroup>
                                <ScaleTransform ScaleX="2" ScaleY="2"/>
                            </TransformGroup>
                        </ContentControl.RenderTransform>
                    </ContentControl>
                </Viewbox>
            </MenuItem.Icon>
        </MenuItem>
</Menu>

资源如下所示:

<?xml version="1.0" encoding="utf-8"?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
            x:Key="appbar.folder.open" Width="76" Height="76" Clip="F1 M 0,0L 76,0L 76,76L 0,76L 0,0">
        <Path Width="44" Height="26" Canvas.Left="19" Canvas.Top="24" Stretch="Fill" Fill="#FF000000" Data="F1 M 19,50L 28,34L 63,34L 54,50L 19,50 Z M 19,28.0001L 35,28C 36,25 37.4999,24.0001 37.4999,24.0001L 48.75,24C 49.3023,24 50,24.6977 50,25.25L 50,28L 53.9999,28.0001L 53.9999,32L 27,32L 19,46.4L 19,28.0001 Z "/>
    </Canvas>
</ResourceDictionary>

作为旁注,我也不知道如何摆脱显式缩放。这似乎是一个小问题,但如果能解决这个问题,我将不胜感激。

无论如何,为了尽可能多地将这种过于表达的定义移动到样式中,我为附加的 属性 类型 Visual

创建了代码
namespace extensions
{
    public class AttachedProperties
    {
        public static readonly DependencyProperty VisualIconProperty =
            DependencyProperty.RegisterAttached("VisualIcon",
                typeof(System.Windows.Media.Visual), typeof(AttachedProperties),
                new PropertyMetadata(default(System.Windows.Media.Visual)));

        public static void SetVisualIcon(UIElement element, System.Windows.Media.Visual value)
        {
            element.SetValue(VisualIconProperty, value);
        }
        public static System.Windows.Media.Visual GetVisualIcon(UIElement element)
        {
            return (System.Windows.Media.Visual)element.GetValue(VisualIconProperty);
        }
    }
}

重新定义菜单项

<MenuItem Header="_Open"
          Style="{StaticResource MenuItemStyle}"
          extensions:AttachedProperties.VisualIcon="{DynamicResource appbar.folder.open}" />

并尝试创建一个读取 VisualIcon 属性 的样式来设置图标内容

<Style x:Key="MenuItemStyle" TargetType="MenuItem">
    <Setter Property="MenuItem.Icon">
        <Setter.Value>
            <Viewbox>
                <ContentControl RenderTransformOrigin="0.5,0.5">
                    <ContentControl.Content>
                        <!-- this would work but doesn't reference the VisualIcon property.. <DynamicResource ResourceKey="appbar.folder.open"/> -->
                        <!-- these do not -->
                        <Binding Path="(extensions:AttachedProperties.VisualIcon)" RelativeSource="{RelativeSource FindAncestor, AncestorType=MenuItem}"/>-->
                        <!--<Binding Path="(extensions:AttachedProperties.VisualIcon)" RelativeSource="{RelativeSource TemplatedParent}"/>-->
                        <!--<Binding Path="(extensions:AttachedProperties.VisualIcon)" RelativeSource="{RelativeSource Self}"/>-->
                    </ContentControl.Content>
                    <ContentControl.RenderTransform>
                        <TransformGroup>
                            <ScaleTransform ScaleX="2" ScaleY="2"/>
                        </TransformGroup>
                    </ContentControl.RenderTransform>
                </ContentControl>
            </Viewbox>
        </Setter.Value>
    </Setter>
</Style>

但失败了。使用 DynamicResource 和静态键引用资源有效,但我无法使用附加的 属性 获得任何绑定。

由于我是 WPF 初学者,我不确定这是否是一个好方法。

[EDIT1]

我测试了所有提供的解决方案。 Grek40's 没有为我显示任何图标,无论是在设计视图中还是在运行时;也许我做错了什么。

grx70's 中的第二种方法对我来说是最有前途的,因为它可以在设计视图和运行时可靠地显示图标。 但是,我不明白Win7和Win10之间似乎存在差异。我在 Win7 和 Win10 上测试了相同的源(在外部驱动器上)。对于Win10来说,它看起来不错,但是Win7绘制的图标太大了。

(注:原因在中给出)

这是 window 测试代码:

<Window.Resources>
    <Style x:Key="MenuItemStyle" TargetType="controls:MenuItemEx">
        <Setter Property="MenuItem.Icon">
            <Setter.Value>
                <Viewbox>
                    <ContentControl RenderTransformOrigin="0.5,0.5">
                        <ContentControl.Content>
                            <Binding Path="(extensions:AttachedProperties.VisualIcon)" RelativeSource="{RelativeSource FindAncestor, AncestorType=MenuItem}"/>
                        </ContentControl.Content>
                        <ContentControl.RenderTransform>
                            <TransformGroup>
                                <ScaleTransform ScaleX="2" ScaleY="2"/>
                            </TransformGroup>
                        </ContentControl.RenderTransform>
                    </ContentControl>
                </Viewbox>
            </Setter.Value>
        </Setter>
    </Style>
    <Style x:Key="MenuItemStyle2" TargetType="MenuItem">
        <Setter Property="uihelpers:MenuItemHelper.IsEnabled" Value="True" />
        <Setter Property="MenuItem.Icon">
            <Setter.Value>
                <Viewbox>
                    <ContentControl RenderTransformOrigin="0.5,0.5"
                        Content="{Binding
                        Path=(uihelpers:MenuItemHelper.MenuItem).(extensions:AttachedProperties.VisualIcon),
                        RelativeSource={RelativeSource AncestorType=Viewbox}}">
                        <ContentControl.RenderTransform>
                            <TransformGroup>
                                <ScaleTransform ScaleX="2" ScaleY="2"/>
                            </TransformGroup>
                        </ContentControl.RenderTransform>
                    </ContentControl>
                </Viewbox>
            </Setter.Value>
        </Setter>
    </Style>

    <Style x:Key="MenuItemStyle3" TargetType="{x:Type MenuItem}">
        <Style.Resources>
            <DataTemplate x:Key="MenuItemStyle3dt" DataType="{x:Type Style}">
                <Path Style="{Binding}"
                      Stretch="Uniform"
                      HorizontalAlignment="Center"
                      VerticalAlignment="Center" />
            </DataTemplate>
        </Style.Resources>
    </Style>
    <Style x:Key="appbar.folder.open.style3.local" TargetType="{x:Type Path}">
        <Setter Property="Fill" Value="Black" />
        <Setter Property="Data" Value="M0,26 L9,10 L44,10 L35,26 Z M0,4 L16,4 C17,1 18.5,0 18.5,0 L29.75,0 C30.3,0 31,0.7 31,1.25 L31,4 L34,4 L34,8 L8,8 L0,22.4 Z" />
    </Style>
    <extensions:PathStyle x:Key="appbar.folder.open.style3b">
        <Setter Property="Path.HorizontalAlignment" Value="Center" />
        <Setter Property="Path.VerticalAlignment" Value="Center" />
        <Setter Property="Path.Stretch" Value="Uniform" />
        <Setter Property="Path.Fill" Value="Black" />
        <Setter Property="Path.Data" Value="M0,26 L9,10 L44,10 L35,26 Z M0,4 L16,4 C17,1 18.5,0 18.5,0 L29.75,0 C30.3,0 31,0.7 31,1.25 L31,4 L34,4 L34,8 L8,8 L0,22.4 Z" />
    </extensions:PathStyle>
    <DataTemplate DataType="{x:Type extensions:PathStyle}">
        <Path Style="{Binding}" />
    </DataTemplate>

    <Canvas x:Key="appbar.folder.open.style4.local" x:Shared="False" Width="76" Height="76" Clip="F1 M 0,0L 76,0L 76,76L 0,76L 0,0">
        <Path Width="44" Height="26" Canvas.Left="19" Canvas.Top="24" Stretch="Fill" Fill="#FF000000" Data="F1 M 19,50L 28,34L 63,34L 54,50L 19,50 Z M 19,28.0001L 35,28C 36,25 37.4999,24.0001 37.4999,24.0001L 48.75,24C 49.3023,24 50,24.6977 50,25.25L 50,28L 53.9999,28.0001L 53.9999,32L 27,32L 19,46.4L 19,28.0001 Z "/>
    </Canvas>
    <Viewbox x:Key="MenuItemStyle4.Icon" x:Shared="False">
        <ContentControl Content="{Binding Path=Tag,RelativeSource={RelativeSource AncestorType=MenuItem}}" RenderTransformOrigin="0.5,0.5">
            <ContentControl.RenderTransform>
                <TransformGroup>
                    <ScaleTransform ScaleX="2" ScaleY="2"/>
                </TransformGroup>
            </ContentControl.RenderTransform>
        </ContentControl>
    </Viewbox>
    <Style x:Key="MenuItemStyle4" TargetType="MenuItem">
        <Setter Property="Icon" Value="{StaticResource MenuItemStyle4.Icon}"/>
    </Style>
</Window.Resources>
<Grid>
    <Menu x:Name="menu" Height="19" Margin="10,10,10.333,0" VerticalAlignment="Top">
        <MenuItem Header="_File">
            <controls:MenuItemEx Header="_Open"
                      Style="{StaticResource MenuItemStyle}"
                      extensions:AttachedProperties.VisualIcon="{DynamicResource appbar.folder.open}" />
            <MenuItem Header="_Open"
                      Style="{StaticResource MenuItemStyle2}"
                      extensions:AttachedProperties.VisualIcon="{DynamicResource appbar.folder.open}" />
            <MenuItem Header="_Open"
                      Style="{StaticResource MenuItemStyle3}"
                      Icon="{DynamicResource appbar.folder.open.style3}" />
            <MenuItem Header="_Open"
                      Style="{StaticResource MenuItemStyle3}"
                      Icon="{DynamicResource appbar.folder.open.style3.local}" />
            <MenuItem Header="_Open" Icon="{DynamicResource appbar.folder.open.style3b}" />
            <MenuItem Header="_Open"
                      Style="{StaticResource MenuItemStyle4}"
                      Tag="{DynamicResource appbar.folder.open}" />
            <MenuItem Header="_Open"
                      Style="{StaticResource MenuItemStyle4}"
                      Tag="{DynamicResource appbar.folder.open.style4.local}" />
            <MenuItem Header="_Save">
                <MenuItem.Icon>
                    <Viewbox>
                        <ContentControl Content="{DynamicResource appbar.save}" RenderTransformOrigin="0.5,0.5">
                            <ContentControl.RenderTransform>
                                <TransformGroup>
                                    <ScaleTransform ScaleX="2" ScaleY="2"/>
                                </TransformGroup>
                            </ContentControl.RenderTransform>
                        </ContentControl>
                    </Viewbox>
                </MenuItem.Icon>
            </MenuItem>
        </MenuItem>
    </Menu>
    <Menu x:Name="menu2" Height="19" Margin="9,32,11.333,0" VerticalAlignment="Top" ItemContainerStyle="{StaticResource MenuItemStyle4}">
        <MenuItem Header="_File">
            <MenuItem Header="_Open"
                       Tag="{DynamicResource appbar.folder.open.style4.local}" />
            <MenuItem Header="_Open"
                       Tag="{DynamicResource appbar.folder.open}" />
        </MenuItem>
    </Menu>
</Grid>

这里是全球资源:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
            x:Key="appbar.folder.open" x:Shared="False" Width="76" Height="76" Clip="F1 M 0,0L 76,0L 76,76L 0,76L 0,0">
        <Path Width="44" Height="26" Canvas.Left="19" Canvas.Top="24" Stretch="Fill" Fill="#FF000000" Data="F1 M 19,50L 28,34L 63,34L 54,50L 19,50 Z M 19,28.0001L 35,28C 36,25 37.4999,24.0001 37.4999,24.0001L 48.75,24C 49.3023,24 50,24.6977 50,25.25L 50,28L 53.9999,28.0001L 53.9999,32L 27,32L 19,46.4L 19,28.0001 Z "/>
    </Canvas>
    <Style x:Key="appbar.folder.open.style3" TargetType="{x:Type Path}">
        <Setter Property="Fill" Value="Black" />
        <Setter Property="Data" Value="M0,26 L9,10 L44,10 L35,26 Z M0,4 L16,4 C17,1 18.5,0 18.5,0 L29.75,0 C30.3,0 31,0.7 31,1.25 L31,4 L34,4 L34,8 L8,8 L0,22.4 Z" />
    </Style>
</ResourceDictionary>

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
            x:Key="appbar.save" Width="76" Height="76" Clip="F1 M 0,0L 76,0L 76,76L 0,76L 0,0">
        <Path Width="34.8333" Height="34.8333" Canvas.Left="20.5833" Canvas.Top="20.5833" Stretch="Fill" Fill="#FF000000" Data="F1 M 20.5833,20.5833L 55.4167,20.5833L 55.4167,55.4167L 45.9167,55.4167L 45.9167,44.3333L 30.0833,44.3333L 30.0833,55.4167L 20.5833,55.4167L 20.5833,20.5833 Z M 33.25,55.4167L 33.25,50.6667L 39.5833,50.6667L 39.5833,55.4167L 33.25,55.4167 Z M 26.9167,23.75L 26.9167,33.25L 49.0833,33.25L 49.0833,23.75L 26.9167,23.75 Z "/>
    </Canvas>
</ResourceDictionary>

合并到app.xaml:

<Application.Resources>
    <ResourceDictionary >
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="icons/appbar.folder.open.xaml"/>
            <ResourceDictionary Source="icons/appbar.save.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

图标偏移的原因是我或多或少地从 github.com/Templarian/WindowsIcons 中提取了它们 1:1 并且希望如此,因为它们是以这种格式提供的(我也尝试将它们保存为画笔,首先使用 Inkscape,然后使用 Expression Design)像这样使用它们是很常见的。

诊断

您的绑定不起作用的原因如下:

  1. 对于 RelativeSourceMode.Self - 因为 VisualIcon 附加的 属性 没有在 ContentControl 上明确设置并且其默认值为 null.
  2. 对于 RelativeSourceMode.TemplatedParent - 因为 ContentControl 不是模板的一部分(如果是,从您提供的代码中看不出来,可能是 VisualIcon 对于模板化父级是 null)
  3. 对于 RelativeSourceMode.FindAncestor - 因为 MenuItem 没有将 MenuItem.Icon 的值设置为 属性 作为其逻辑子项,所以直到 ContentControl 被加载进入可视化树,没有办法建立两者之间的关系。而且,显然,在这种情况下,绑定是在此之前解决的,并且由于某种原因在最终加载 ContentControl 时没有重新解决。

解决方案

您可以采用多种方法来解决此问题,以下是其中的一些方法。

我。仅在加载 ContentControl 时设置绑定

您可以为 ContentControl.Loaded 事件订阅以下处理程序:

private void ContentControl_Loaded(object sender, RoutedEventArgs e)
{
    var control = (ContentControl)sender;
    control.SetBinding(ContentControl.ContentProperty, new Binding
    {
        Path = new PropertyPath(AttachedProperties.VisualIconProperty),
        RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor)
        {
            AncestorType = typeof(MenuItem),
        },
    });
}

然后在你的XAML:

<ContentControl
    Content="{Binding
        Path=(extensions:AttachedProperties.VisualIcon),
        RelativeSource={RelativeSource AncestorType=MenuItem}}"
    Loaded="ContentControl_Loaded" (...)>
    (...)
</ContentControl>

请注意,如果样式放在单独文件的资源字典中,您将必须遵循 these instructions 才能正常工作。

您还可以创建自定义 MarkupExtension(例如命名为 DeferredBinding)来完成这项工作。

二. Subclass MenuItem 并设置图标为逻辑子

没有多少代码可以写了:

public class MyMenuItem : MenuItem
{
    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        base.OnPropertyChanged(e);
        if (e.Property == IconProperty)
        {
            if (e.OldValue != null)
                RemoveLogicalChild(e.OldValue);
            if (e.NewValue != null)
                AddLogicalChild(e.NewValue);
        }
    }
}

但缺点是你总是要记住使用MyMenuItem而不是MenuItem:

<local:MyMenuItem Header="_Open"
      Style="{StaticResource MenuItemStyle}"
      extensions:AttachedProperties.VisualIcon="{DynamicResource appbar.folder.open}" />

和:

<Style x:Key="MenuItemStyle" TargetType="local:MyMenuItem">
    (...)
</Style>

在这种情况下,您还可以在 FindAncestor 模式下进行绑定。

三.创建一个助手 class,允许通过附加的 属性

ContentControl 访问相关的 MenuItem

下面的助手 class 包含两个依赖属性 - IsEnabled(我相信它是不言自明的)和 MenuItem,它是只读的并保存实际的 MenuItem,上面设置了目标图标:

public static class MenuItemHelper
{
    /**** Here are the important parts: ****/

    //When IsEnabled changes we need to either hook things up or do the cleanup
    private static void HandleIsEnabledChanged(
        DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        var item = (MenuItem)d;
        if ((bool)e.NewValue)
            //We set MenuItem attached property for current Icon
            HandleIconChanged(null, item, EventArgs.Empty);
        else
            //We clear the value of MenuItem attached property
            HandleIconChanged(item.Icon, item, EventArgs.Empty);
    }

    //By using an extension method we get hold of the old value without the need
    //to maintain any kind of dictionary, so we don't need to worry about memory leaks
    private static void HandleIconChanged(
        this object oldValue,
        object sender,
        EventArgs e)
    {
        var item = (MenuItem)sender;
        if (oldValue is DependencyObject oldIcon)
            SetMenuItem(oldIcon, null);
        if (item.Icon is DependencyObject newIcon)
            SetMenuItem(newIcon, item);
        //We need to remove the old handler, because it relates to the old icon
        DependencyPropertyDescriptor
            .FromProperty(MenuItem.IconProperty, item.GetType())
            .RemoveValueChanged(item, item.Icon.HandleIconChanged);
        //We add new handler, so that when the icon changes we get correct old icon
        DependencyPropertyDescriptor
            .FromProperty(MenuItem.IconProperty, item.GetType())
            .AddValueChanged(item, item.Icon.HandleIconChanged);
    }

    /**** The rest is just DP boilerplate code ****/

    private static readonly DependencyPropertyKey MenuItemPropertyKey =
        DependencyProperty.RegisterAttachedReadOnly(
            name: "MenuItem",
            propertyType: typeof(MenuItem),
            ownerType: typeof(MenuItemHelper),
            defaultMetadata: new PropertyMetadata(null));

    public static readonly DependencyProperty MenuItemProperty =
        MenuItemPropertyKey.DependencyProperty;

    public static MenuItem GetMenuItem(DependencyObject d)
        => (MenuItem)d.GetValue(MenuItemProperty);

    private static void SetMenuItem(DependencyObject d, MenuItem value)
        => d.SetValue(MenuItemPropertyKey, value);

    public static readonly DependencyProperty IsEnabledProperty =
        DependencyProperty.RegisterAttached(
            name: "IsEnabled",
            propertyType: typeof(bool),
            ownerType: typeof(MenuItemHelper),
            defaultMetadata: new PropertyMetadata(false, HandleIsEnabledChanged));

    public static bool GetIsEnabled(MenuItem item)
        => (bool)item.GetValue(IsEnabledProperty);

    public static void SetIsEnabled(MenuItem item, bool value)
        => item.SetValue(IsEnabledProperty, value);
}

那么你只需要在MenuItem上设置MenuItemHelper.IsEnabled="True"就可以使用MenuItemHelper.MenuItem进行绑定(记住它会设置在图标的根元素上- Viewbox 在你的情况下):

<Style x:Key="MenuItemStyle" TargetType="MenuItem">
    <Setter Property="extensions:MenuItemHelper.IsEnabled" Value="True" />
    <Setter Property="MenuItem.Icon">
        <Setter.Value>
            <Viewbox>
                <ContentControl
                    Content="{Binding
                        Path=(extensions:MenuItemHelper.MenuItem).(extensions:AttachedProperties.VisualIcon),
                        RelativeSource={RelativeSource AncestorType=Viewbox}}" (...)>
                    (...)
                </ContentControl>
            </Viewbox>
        </Setter.Value>
    </Setter>
</Style>

我个人最喜欢的是#III,因为它是三者中最通用的,但也许在您的特定情况下,其他解决方案可能更适用。

因为它看起来有点像 XY problem,我会为您提供实现目标的替代方法。

首先,您的 appbar.folder.open 资源过于复杂。 Canvas 是完全多余的(您可以通过设置其 Margin 来抵消您的 Path)。这些数字在 Path 中偏移了 19,24,这与 PathCanvas 中的偏移相结合导致需要将 Viewbox 与 [= 一起使用27=]。此外,您的资源不可重用,因为它只能加载到可视化树中一次,因此它只会在最后一次被引用的地方可见(它将在之前的所有地方卸载)。我的建议是为 Path 创建一个 Style - 它不仅可重用,而且在应用后可以修改目标 Path 上的其他属性的意义上也是可扩展的风格。这是完成这项工作的最小样式(我已经翻译了您的数据,因此它不再偏移):

<Style x:Key="appbar.folder.open" TargetType="{x:Type Path}">
    <Setter Property="Fill" Value="Black" />
    <Setter Property="Data" Value="M0,26 L9,10 L44,10 L35,26 Z M0,4 L16,4 C17,1 18.5,0 18.5,0 L29.75,0 C30.3,0 31,0.7 31,1.25 L31,4 L34,4 L34,8 L8,8 L0,22.4 Z" />
</Style>

其次,我不太明白你AttachedProperties.VisualIcon附上属性的目的。在我的方法中,它是完全多余的。另外,我相信这是使设计器在禁用项目代码的情况下无法正确显示图标的部分。唯一的问题是,如果我们设置 MenuItem.Icon="{DynamicResource appbar.folder.open}",我们将显示 System.Windows.Style 文本而不是图标。并且 DynamicResourceExtension 不支持开箱即用地转换引用资源。但是,我们可以使用一个巧妙的技巧来使事情正常进行——只需提供一个隐含的 DataTemplateDataType="{x:Type Style}",它将自动应用:

<Style x:Key="MenuItemStyle" TargetType="{x:Type MenuItem}">
    <Style.Resources>
        <DataTemplate DataType="{x:Type Style}">
            <Path Style="{Binding}"
                  Stretch="Uniform"
                  HorizontalAlignment="Center"
                  VerticalAlignment="Center" />
        </DataTemplate>
    </Style.Resources>
</Style>

我们在 Path 上设置了一些额外的属性,以便它很好地适合图标区域。

我们现在需要做的就是引用样式和图标,使其显示在 MenuItem:

<Menu (...)>
    <MenuItem Header="_Open"
              Style="{StaticResource MenuItemStyle}"
              Icon="{DynamicResource appbar.folder.open}" />
</Menu>

这种方法还有一个额外的优势,即它在设计器中工作,即使项目代码被禁用(至少对我来说是这样)。

编辑

如果你想完全分离和自动化,你可以继承 Style:

public class PathStyle : Style
{
    public PathStyle()
    {
        TargetType = typeof(Path);
    }
}

并在您的资源字典中提供隐式 DataTemplate

<local:PathStyle x:Key="appbar.folder.open">
    <Setter Property="Path.HorizontalAlignment" Value="Center" />
    <Setter Property="Path.VerticalAlignment" Value="Center" />
    <Setter Property="Path.Stretch" Value="Uniform" />
    <Setter Property="Path.Fill" Value="Black" />
    <Setter Property="Path.Data" Value="M0,26 L9,10 L44,10 L35,26 Z M0,4 L16,4 C17,1 18.5,0 18.5,0 L29.75,0 C30.3,0 31,0.7 31,1.25 L31,4 L34,4 L34,8 L8,8 L0,22.4 Z" />
</local:PathStyle>
<DataTemplate DataType="{x:Type local:PathStyle}">
    <Path Style="{Binding}" />
</DataTemplate>

我将所有属性从 DataTemplate 移动到 Style 以便它可以被其他样式覆盖。请注意,您需要完全限定 属性 名称,即使用 Path.Data 而不是简单地 Data.

现在您只需在您的视图中引用资源即可:

<MenuItem Icon="{DynamicResource appbar.folder.open}" (...) />

甚至:

<ContentPresenter Content="{DynamicResource appbar.folder.open}" />

所有的魔法都是由框架完成的。这种方法的美妙之处在于,您可以将 ResourceDictionary 替换为包含以下内容的一个:

<Border x:Key="appbar.folder.open" x:Shared="False" Background="Red" />

无需修改视图,一切仍然有效。

您的样式方法存在问题,当应用相同样式时,Viewbox 将在多个项目之间共享。这可以通过将 Viewbox 创建为带有 x:Shared="False" 的单独资源来避免。如果你想坚持你目前的 Canvas-Path 方法,你也应该把它变成一个非共享资源,这样它就可以重复使用了

出于演示目的,我设置了 MenuItem.Tag 属性,但您所附的 属性 想法应该也是如此。

资源:

<!--Notice the added x:Shared-->
<Canvas x:Key="appbar.folder.open" x:Shared="False" Width="76" Height="76" Clip="F1 M 0,0L 76,0L 76,76L 0,76L 0,0">
    <Path Width="44" Height="26" Canvas.Left="19" Canvas.Top="24" Stretch="Fill" Fill="#FF000000" Data="F1 M 19,50L 28,34L 63,34L 54,50L 19,50 Z M 19,28.0001L 35,28C 36,25 37.4999,24.0001 37.4999,24.0001L 48.75,24C 49.3023,24 50,24.6977 50,25.25L 50,28L 53.9999,28.0001L 53.9999,32L 27,32L 19,46.4L 19,28.0001 Z "/>
</Canvas>

<!--Another icon for purpose of demonstration-->
<Canvas x:Key="appbar.folder.close" x:Shared="False" Width="76" Height="76" Clip="F1 M 0,0L 76,0L 76,76L 0,76L 0,0">
    <Path Width="44" Height="26" Canvas.Left="19" Canvas.Top="24" Stretch="Fill" Fill="#FFFF0000" Data="F1 M 19,50L 28,34L 63,34L 54,50L 19,50 Z M 19,28.0001L 35,28C 36,25 37.4999,24.0001 37.4999,24.0001L 48.75,24C 49.3023,24 50,24.6977 50,25.25L 50,28L 53.9999,28.0001L 53.9999,32L 27,32L 19,46.4L 19,28.0001 Z "/>
</Canvas>

<Viewbox x:Key="MenuItemStyle.Icon" x:Shared="False">
    <ContentControl Content="{Binding Path=Tag,RelativeSource={RelativeSource AncestorType=MenuItem}}" RenderTransformOrigin="0.5,0.5">
        <ContentControl.RenderTransform>
            <TransformGroup>
                <ScaleTransform ScaleX="2" ScaleY="2"/>
            </TransformGroup>
        </ContentControl.RenderTransform>
    </ContentControl>
</Viewbox>

<Style x:Key="MenuItemStyle" TargetType="MenuItem">
    <Setter Property="Icon" Value="{StaticResource MenuItemStyle.Icon}"/>
</Style>

用法:

<Menu x:Name="menu" Height="19" Margin="10,10,10.333,0" VerticalAlignment="Top" ItemContainerStyle="{StaticResource MenuItemStyle}">
    <MenuItem Header="_Open" Tag="{DynamicResource appbar.folder.open}"/>
    <MenuItem Header="_Open 2" Tag="{DynamicResource appbar.folder.open}"/>
    <MenuItem Header="_Close" Tag="{DynamicResource appbar.folder.close}"/>
</Menu>