Search code examples
c#wpfxamldata-bindingwpf-style

Set WPF MenuItem Icon in Style from attached property


I'm trying to move some XAML defining MenuItem into a Style.

I've got the following working 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>

where the resource looks like this:

<?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>

As a sidenote, i also don't know how to get rid of the explicit scaling. It seems to be a minor problem but i would appreciate if this could be solved as well.

Anyway, to move as much as possible of this somewhat too expressive definition into a Style, I created code for an attached property of type 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);
        }
    }
}

redefined the menu item

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

and tried to create a Style which reads the VisualIcon property to set the Icon content

<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>

but failed. Referencing the resource with a DynamicResource and a static key works, but i cannot get any binding using the attached property to work.

Since I'm a WPF beginner, I'm not sure if this is a good approach anyway.

[EDIT1]

I tested all the provided solutions. Grek40's answer didn't display any icons for me, neither in design view nor during runtime; maybe I did something completely wrong.

The second approach in grx70's second answer it the most promising for me as it reliably displays the icon in the design view as well as during runtime. However, it seems like there's a difference between Win7 and Win10 i don't understand. I tested the identical source (on a external drive) on Win7 and Win10. For Win10, it looks nice, but Win7 draws the icons way too big.

(Note: The reason is give in this comment)

Difference Win7/Win10

Here's the window testcode:

<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>

and here are the global resources:

<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>

and

<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>

which are merged in 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>

The reason the icons are offset is that i took them more or less 1:1 from github.com/Templarian/WindowsIcons and was hoping that because they're provided in this format (i also tried to save them as brushes, first with Inkscape and then with Expression Design) it would be common to use them like this.


Solution

  • Since it somewhat looks like an XY problem, I'll give you alternative approach to accomplishing your goal.

    First of all, your appbar.folder.open resource is overly complicated. The Canvas is completely redundant (you can offset your Path by setting its Margin). The figures are offset within the Path by 19,24, which combined with the Path being offset in Canvas leads to the necessity of using Viewbox together with ScaleTransform. Moreover, your resource is not reusable, since it can only be loaded into the visual tree once, so it would only be visible in the last place it was referenced (it would be unloaded in all the previous places). My advice would be to create a Style for a Path instead - not only is it reusable, but also extensible in the sense that you can modify other properties on the target Path after applying the style. Here's a minimal style to do the job (I've translated your data so that it is no longer offset):

    <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>
    

    Secondly, I don't quite understand the purpose of your AttachedProperties.VisualIcon attached property. In my approach it is completely redundant. Also, I believe that this is the part that makes the designer not display your icon properly with project code disabled. The only problem is that if we set MenuItem.Icon="{DynamicResource appbar.folder.open}" we'll get System.Windows.Style text displayed instead of the icon. And DynamicResourceExtension does not support converting the referenced resource out-of-the-box. But, there's a clever trick we can use to make things work - simply provide an implicit DataTemplate with DataType="{x:Type Style}" which will be automatically applied:

    <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>
    

    We set some additional properties on the Path so that it fits well into the icon area.

    All we need to do now is to reference the style and the icon to have it displayed on a MenuItem:

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

    This approach has the additional advantage of that it works in the designer even with project code disabled (at least it does for me).

    Edit

    If you want to completely separate and automate things, you could subclass Style:

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

    And provide implicit DataTemplate in your resources dictionary:

    <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>
    

    I moved all the properties from the DataTemplate to the Style so that it can be overridden in other styles. Note though that you need to fully qualify property names, i.e. use Path.Data instead of simply Data.

    Now all you need to is to reference the resource in your view:

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

    Or even:

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

    And all the magic is done by the framework. The beauty of this approach is that you can replace your ResourceDictionary with one containing for example:

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

    And everything still works without the need to modify the views.