开发者

How do I set MenuItem's Icon using ItemContainerStyle

I'm following the example here of binding a MenuItem to a data object.

<Menu Grid.Row="0" KeyboardNavigation.TabNavigation="Cycle"
      ItemsSource="{Binding Path=MenuCom开发者_运维问答mands}">  
    <Menu.ItemContainerStyle>
        <Style>
            <Setter Property="MenuItem.Header" Value="{Binding Path=DisplayName}"/>
            <Setter Property="MenuItem.ItemsSource" Value="{Binding Path=Commands}"/>
            <Setter Property="MenuItem.Command" Value="{Binding Path=Command}"/>
            <Setter Property="MenuItem.Icon" Value="{Binding Path=Icon}"/>
        </Style>
    </Menu.ItemContainerStyle>                
</Menu>

It all works swimmingly except the MenuItem's icon shows up as the string System.Drawing.Bitmap. The bitmap in question is returned by the data object from a compiled resource.

internal static System.Drawing.Bitmap folder_page
{
    get
    {
        object obj = ResourceManager.GetObject("folder_page", resourceCulture);
        return ((System.Drawing.Bitmap)(obj));
    }
}

What am I doing wrong?


Kent (of course) has the right answer. But I thought I would go ahead and post the code for the converter that converts from the System.Drawing.Bitmap (Windows Forms) to a System.Windows.Windows.Media.BitmapSource (WPF) ... as this is a common problem/question.

This takes three steps:

  1. Use an image converter in your binding.
  2. Create the converter.
  3. Declare the converter in your resources.

Here is how you would use an image converter in your binding:

<Setter
    Property="MenuItem.Icon"
    Value="{Binding Path=Icon, Converter={StaticResource imageConverter}}"
/>

And, here is the code for the converter (put it into a file called ImageConverter.cs) and add it to your project:

[ValueConversion(typeof(Image), typeof(string))]
public class ImageConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        BitmapSource bitmapSource;

        IntPtr bitmap = ((Bitmap)value).GetHbitmap();
        try
        {
            bitmapSource = Imaging.CreateBitmapSourceFromHBitmap(bitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
        }
        finally
        {
            DeleteObject(bitmap);
        }

        return bitmapSource;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return null;
    }

    [DllImport("gdi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
    static extern int DeleteObject(IntPtr o);
}

Here is how you declare it in your resources section (note the local namespace that you will have to add):

<Window
    x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApplication2"
>
    <Window.Resources>
        <local:ImageConverter x:Key="imageConverter"/>
    </Window.Resources>
    <!-- some xaml snipped for clarity -->
</Window>

And that's it!


Update

After doing a quick search for similar questions, I noticed that Lars Truijens pointed out here that the previous converter implementation leaks. I have updated the converter code above ... so that it doesn't leak.

For more information on the cause of the leak, see the remarks section on this MSDN link.


WPF works with ImageSources, not System.Drawing classes. You'll need to bind to an ImageSource. You could use a converter to convert your Bitmap to an ImageSource, or you could ditch the resources and do things differently.


WPF's menuitems are somewhat weird in that they don't work with ImageSource objects like the rest of the WPF framework.

The easiest way, that will cause you the least amount of headache is to simply have a property in your viewmodel that returns a full Image control:

public Image MenuIcon
{
    get
    {
        return new Image()
               {
                   Source = CreateImageSource("myImage.png")
               };
    }
}

And then in your <Style> for menu items (which you can set in ItemContainerStyle for example) you simply bind the menu item's Icon property to the MenuIcon property in your viewmodel:

<Setter Property="Icon" Value="{Binding MenuIcon}" />

One could argue that this breaks the spirit of MVVM, but at some point you just have to be pragmatic and move on to more interesting problems.


Here's how I made a ViewModel for a menu item: AbstractMenuItem. Pay particular attention to the Icon region:

    #region " Icon "
    /// <summary>
    /// Optional icon that can be displayed in the menu item.
    /// </summary>
    public object Icon
    {
        get
        {
            if (IconFull != null)
            {
                System.Windows.Controls.Image img = new System.Windows.Controls.Image();
                if (EnableCondition.Condition)
                {
                    img.Source = IconFull;
                }
                else
                {
                    img.Source = IconGray;
                }
                return img;
            }
            else
            {
                return null;
            }
        }
    }
    private BitmapSource IconFull
    {
        get
        {
            return m_IconFull;
        }
        set
        {
            if (m_IconFull != value)
            {
                m_IconFull = value;
                if (m_IconFull != null)
                {
                    IconGray = ConvertFullToGray(m_IconFull);
                }
                else
                {
                    IconGray = null;
                }
                NotifyPropertyChanged(m_IconArgs);
            }
        }
    }
    private BitmapSource m_IconFull = null;
    static readonly PropertyChangedEventArgs m_IconArgs =
        NotifyPropertyChangedHelper.CreateArgs<AbstractMenuItem>(o => o.Icon);

    private BitmapSource IconGray { get; set; }

    private BitmapSource ConvertFullToGray(BitmapSource full)
    {
        FormatConvertedBitmap gray = new FormatConvertedBitmap();

        gray.BeginInit();
        gray.Source = full;
        gray.DestinationFormat = PixelFormats.Gray32Float;
        gray.EndInit();

        return gray;
    }

    /// <summary>
    /// This is a helper function so you can assign the Icon directly
    /// from a Bitmap, such as one from a resources file.
    /// </summary>
    /// <param name="value"></param>
    protected void SetIconFromBitmap(System.Drawing.Bitmap value)
    {
        BitmapSource b = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
            value.GetHbitmap(),
            IntPtr.Zero,
            Int32Rect.Empty,
            System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());
        IconFull = b;
    }

    #endregion

You just derive from this class and in the constructor you call SetIconFromBitmap and pass in a picture from your resx file.

Here's how I bound to those IMenuItems in the Workbench Window:

    <Menu DockPanel.Dock="Top" ItemsSource="{Binding Path=(local:Workbench.MainMenu)}">
        <Menu.ItemContainerStyle>
            <Style>
                <Setter Property="MenuItem.Header" Value="{Binding Path=(contracts:IMenuItem.Header)}"/>
                <Setter Property="MenuItem.ItemsSource" Value="{Binding Path=(contracts:IMenuItem.Items)}"/>
                <Setter Property="MenuItem.Icon" Value="{Binding Path=(contracts:IMenuItem.Icon)}"/>
                <Setter Property="MenuItem.IsCheckable" Value="{Binding Path=(contracts:IMenuItem.IsCheckable)}"/>
                <Setter Property="MenuItem.IsChecked" Value="{Binding Path=(contracts:IMenuItem.IsChecked)}"/>
                <Setter Property="MenuItem.Command" Value="{Binding}"/>
                <Setter Property="MenuItem.Visibility" Value="{Binding Path=(contracts:IControl.Visible), 
                    Converter={StaticResource BooleanToVisibilityConverter}}"/>
                <Setter Property="MenuItem.ToolTip" Value="{Binding Path=(contracts:IControl.ToolTip)}"/>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Path=(contracts:IMenuItem.IsSeparator)}" Value="true">
                        <Setter Property="MenuItem.Template">
                            <Setter.Value>
                                <ControlTemplate TargetType="{x:Type MenuItem}">
                                    <Separator Style="{DynamicResource {x:Static MenuItem.SeparatorStyleKey}}"/>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </Menu.ItemContainerStyle>
    </Menu>


For posterity: I've come up with this:

<Menu.ItemContainerStyle>
    <Style TargetType="MenuItem">
        <Setter Property="Icon" Value="{Binding IconUrl, Converter={ns:UrlToImageConverter Width=16, Height=16}}"/>
    </Style>
</Menu.ItemContainerStyle>

The converter is a combination of MarkupExtension and a IValueConverter, so you can specify it inline without having to make it a static resource.

It uses the System.Windows.Media.ImageSourceConverter to convert an uri to an ImageSource and then creates an Image control with that source. As bonus it uses the serviceProvider parameter as supplied to ProvideValue so it can resolve relative image urls as WPF would do it.

[ValueConversion(typeof(string), typeof(Image))]
[ValueConversion(typeof(Uri), typeof(Image))]
public class UrlToImageConverter : MarkupExtension, IValueConverter
{
    public int? MaxWidth { get; set; }

    public int? MaxHeight { get; set; }

    public int? MinWidth { get; set; }

    public int? MinHeight { get; set; }

    public Stretch? Stretch { get; set; }

    public StretchDirection? StretchDirection { get; set; }

    private static readonly ImageSourceConverter _converter = new System.Windows.Media.ImageSourceConverter();

    private readonly IServiceProvider _serviceProvider;

    public UrlToImageConverter()
    {
        _serviceProvider = new ServiceContainer();
    }

    /// <summary>  </summary>
    private UrlToImageConverter(UrlToImageConverter provider, IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider ?? new ServiceContainer();
        MaxWidth = provider.MaxWidth;
        MaxHeight = provider.MaxHeight;
        MinWidth = provider.MinWidth;
        MinHeight = provider.MinHeight;
        Stretch = provider.Stretch;
        StretchDirection = provider.StretchDirection;
    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null) return null;

        var context = GetTypeDescriptorContext();

        bool canConvert;
        if (context == null)
            canConvert = _converter.CanConvertFrom(value.GetType());
        else
            canConvert = _converter.CanConvertFrom(context, value.GetType());

        if (canConvert)
        {
            if (context == null)
                value = _converter.ConvertFrom(value);
            else
                value = _converter.ConvertFrom(context, CultureInfo.CurrentCulture, value);

            if (value is ImageSource source)
            {
                var img = new Image { Source = source };
                if (MaxWidth != null) img.MaxWidth = MaxWidth.Value;
                if (MaxHeight != null) img.MaxHeight = MaxHeight.Value;
                if (MinWidth != null) img.MinWidth = MinWidth.Value;
                if (MinHeight != null) img.MinHeight = MinHeight.Value;                    
                img.Stretch = Stretch ?? System.Windows.Media.Stretch.Uniform;
                img.StretchDirection = StretchDirection ?? System.Windows.Controls.StretchDirection.Both;
                return img;
            }
        }

        return null;
    }

    private ITypeDescriptorContext GetTypeDescriptorContext()
    {
        if (_serviceProvider is ITypeDescriptorContext context)
            return context;
        else
            return (ITypeDescriptorContext)_serviceProvider?.GetService(typeof(ITypeDescriptorContext));
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return new UrlToImageConverter(this, serviceProvider);
    }
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜