How do you select the right size icon from a multi-resolution .ico file in WPF?
I use simple Markup Extension for that:
/// <summary>/// Simple extension for icon, to let you choose icon with specific size./// Usage sample:/// Image Stretch="None" Source="{common:Icon /Controls;component/icons/custom.ico, 16}"/// Or:/// Image Source="{common:Icon Source={Binding IconResource}, Size=16}"/// </summary> public class IconExtension : MarkupExtension{ private string _source; public string Source { get { return _source; } set { // Have to make full pack URI from short form, so System.Uri recognizes it. _source = "pack://application:,,," + value; } } public int Size { get; set; } public override object ProvideValue(IServiceProvider serviceProvider) { var decoder = BitmapDecoder.Create(new Uri(Source), BitmapCreateOptions.DelayCreation, BitmapCacheOption.OnDemand); var result = decoder.Frames.SingleOrDefault(f => f.Width == Size); if (result == default(BitmapFrame)) { result = decoder.Frames.OrderBy(f => f.Width).First(); } return result; } public IconExtension(string source, int size) { Source = source; Size = size; } public IconExtension() { }}
Xaml usage:
<Image Stretch="None" Source="{common:Icon Source={Binding IconResource},Size=16}"/>
or
<Image Stretch="None" Source="{common:Icon /ControlsTester;component/icons/custom-reports.ico, 16}" />
(based on @Nikolay great answer and follow-up comment about binding)
You probably will be better off creating a Converter
instead of a MarkupExtension
so that you can leverage Binding
. Using the same logic as provided by @Nikolay
/// <summary> /// Forces the selection of a given size from the ICO file/resource. /// If the exact size does not exists, selects the closest smaller if possible otherwise closest higher resolution. /// If no parameter is given, the smallest frame available will be selected /// </summary> public class IcoFileSizeSelectorConverter : IValueConverter { public virtual object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { var size = string.IsNullOrWhiteSpace(parameter?.ToString()) ? 0 : System.Convert.ToInt32(parameter); var uri = value?.ToString()?.Trim(); if (string.IsNullOrWhiteSpace(uri)) return null; if (!uri.StartsWith("pack:")) uri = $"pack://application:,,,{uri}"; var decoder = BitmapDecoder.Create(new Uri(uri), BitmapCreateOptions.DelayCreation, BitmapCacheOption.OnDemand); var result = decoder.Frames.Where(f => f.Width <= size).OrderByDescending(f => f.Width).FirstOrDefault() ?? decoder.Frames.OrderBy(f => f.Width).FirstOrDefault(); return result; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); }
You must then create a resource from your converter class somewhere in a ResourceDictionary as usual:
<localConverters:IcoFileSizeSelectorConverter x:Key="IcoFileSizeSelector" />
And then you can use Binding
:
<Image Source="{Binding Path=IconResource, Converter={StaticResource IcoFileSizeSelector}, ConverterParameter=16}" />
PS: in the converter code, we accept all inputs for parameter, even missing or invalid ones. That behaviour is more convenient if like me you like to play with live XAML edit.