Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I define icon resources on a per-system theme basis?

I have a WPF 4.0 application that utilizes some custom 16x16 icons in things like menu commands and the like. I'd like to have (for now) two sets of icons, the default Vista/7-ish ones and some XP-ish ones. What I want is to have the current OS determine which icons to use.

Right now, I've got BitmapImage resources defined in theme resource dictionaries (i.e. Aero.NormalColor.xaml, etc.) that point to a specific PNG resource.

<!-- Aero.NormalColor.xaml -->
<BitmapImage x:Key="IconSave" UriSource="/MyWPFApp;component/Resources/Icons16/Aero/disk.png"/>

<!-- Luna.NormalColor.xaml -->
<BitmapImage x:Key="IconSave" UriSource="/MyWPFApp;component/Resources/Icons16/Luna/disk.png"/>

Anywhere in my app that wants to show an icon sets the Image/Icon's source property as a StaticResource to one of these BitmapImages.

<Image Source="{StaticResource IconSave}"/>

The idea is that since WPF loads a theme dictionary automatically based on the current OS and theme, only one set of BitmapImage resources would be loaded and the icons would magically be the appropriate ones.

This, however, doesn't work, and I get the dreaded "cannot find resource" exception at run-time. My hunch is that this is because theme files only get searched for custom controls, which Image is not.

Blend 4 has no problem with these, but it has defined its special DesignTimeResources.xaml file with a merge on Aero.NormalColor.xaml. VS2010 chokes, but it also fails to use things like DesignData files and such, too, so I'm not surprised. I currently have also a separate resource dictionary file (MainSkin.xaml) that is merged into the Application resources. Referencing styles and such from it work fine at run-time.

Am I on the right track and just have something slightly wrong? Do I need to do something completely different to get the desired effect, and if so, what?

like image 578
Sean Hanley Avatar asked Aug 24 '11 18:08

Sean Hanley


1 Answers

I've found that you can get this to work using the ComponentResourceKey. Within your theme resource dictionaries define the resources as follows

<!-- themes\aero.normalcolor.xaml -->
<BitmapImage x:Key="{ComponentResourceKey ResourceId=IconSave, TypeInTargetAssembly={x:Type local:CustomControl}}" UriSource="/MyWPFApp;component/Resources/Icons16/Aero/disk.png"/>

<!-- themes\luna.normalcolor.xaml -->
<BitmapImage x:Key="{ComponentResourceKey ResourceId=IconSave, TypeInTargetAssembly={x:Type local:CustomControl}}" UriSource="/MyWPFApp;component/Resources/Icons16/Luna/disk.png"/>

Here the local:CustomControl can either be your main window or a custom control within your assembly. Interestingly though, it doesn't actually matter as long as it's custom, so that it makes sure that you force it to load these resources.

You will also need to update you AssemblyInfo.cs to make sure that ThemeInfo looks at the source assembly for theme resource dictionaries with the following

[assembly:ThemeInfo(ResourceDictionaryLocation.SourceAssembly, ResourceDictionaryLocation.SourceAssembly )]

Now within your XAML (whatever control you like, doesn't have to be CustomControl) you can write the following to make use of the resource

<Image Source="{DynamicResource {ComponentResourceKey TypeInTargetAssembly={x:Type local:CustomControl}, ResourceId=IconSave}}"/>

By using DynamicResource you can also make the application dynamically update when the theme changes (rather than StaticResource which will require a restart).

I think can probably write a cleaner implementation of ComponentResourceKey to hide away the TypeInTargetAssembly (which I'll give a go at) but at least this should get you working.


To update, I've just implemented an improvement on ComponentResourceKey which will look at the currently executing assembly and find the first UIElement it can to use for the TypeInTargetAssembly.

    public class ThemeResourceKey : ComponentResourceKey
    {
        public ThemeResourceKey(String resourceId)
        {
            ResourceId = resourceId;
            var assembly = Assembly.GetExecutingAssembly();

            var types = assembly.GetTypes().Where(t => typeof (UIElement).IsAssignableFrom(t));
            var uiElementType = types.FirstOrDefault();
            if(uiElementType == default(Type))
                throw new ArgumentException("No custom UIElements defined within this XAML");

            TypeInTargetAssembly = uiElementType;
        }
    }

You can now define the resource dictionary with this

<!-- themes\aero.normalcolor.xaml -->
<BitmapImage x:Key="{local:ThemeResourceKey IconSave}" UriSource="/MyWPFApp;component/Resources/Icons16/Aero/disk.png"/>

and reference this in your controls as follows

<Image Source="{DynamicResource {local:ThemeResourceKey IconSave}}"/>

Which should prove a lot cleaner. Hope that helps and let me know if you have any issues with it.

like image 135
Iain Skett Avatar answered Nov 15 '22 09:11

Iain Skett