Create and apply custom themes
Introduction
With WPF, Microsoft introduces one of the most famous mechanism until now reserved almost exclusively for web applications, available for any types of application: UI composed by themes. Indeed, all visual elements can be defined by style and all styles can be regrouped into a specific theme. With extrapolations several themes can also be applied on the same application.
The sample
The sample I provide with this post try to explain how to create several external themes for an application. Why themes are externals ? Because I could integrate my themes in the same assembly but today many profiles participate in the creation of an application. Therefor I would like you to keep in mind that an application is often designed following an industrial process (this is nevertheless the way where we try to go). A development team might create the logic of the application whereas designers imagine the UI by creating several themes to be applied.
Because I didn't want to imagine a complex application to explain the theme concept, I reused the logon application provided by Microsoft with the WinFX SDK. So the sample is composed with 3 parts: an assembly with custom controls and the default theme, another containing the Window application and another one with custom themes.
Figure1. Custom themes
Create a generic theme
In the first assembly, I define custom controls for the application. It's not a constraint but it facilitates the design and the theme to be applied. The only thing to do is to override the eventual initial visual styles defined for the base controls:
public class LogonButton : Button
{
public LogonButton()
{
}
static LogonButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(LogonButton), new FrameworkPropertyMetadata(typeof(LogonButton)));
}
}
Assuming a WPF application considers a ResourceDictionary to be the default theme, I add a ResourceDictionary named generic.xaml. With this name, the dictionary will be automatically applied whether no specific dictionary is set. However considering that the theme is designed in an independent manner, I choose to use the type of the styled object as key instead of a string:
<
ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:RD.CustomThemes.Controls"
>
<Style x:Key="{x:Type cc:BackgroundControl}" TargetType="{x:Type cc:BackgroundControl}">
<Setter Property="Grid.ColumnSpan" Value="4" />
<Setter Property="Grid.RowSpan" Value="3" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type cc:BackgroundControl}">
<Grid>
<Rectangle />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
...
<Style x:Key="{x:Type cc:LogonButton}" BasedOn="{StaticResource {x:Type Button}}" TargetType="{x:Type cc:LogonButton}">
<Setter Property="Width" Value="150" />
<Setter Property="Grid.Row" Value="2" />
</Style>
</ResourceDictionary>
A last thing to do is to verify that a generic theme can be properly applied. Add the ThemeInfo attribute into the AssemblyInfo.cs. Note that with the february CTP, this attribute is added for you in the file.
[assembly:
ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)]
Create other specific themes
In another assembly I just create a 'themes' folder, add a reference to the first assembly and define 3 other themes with no constraint naming convention. In each xaml file, I just define the style of each control. The code below comes from one of the 3 resource dictionaries:
<
ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:RD.CustomThemes.Controls;assembly=RD.CustomThemes.Controls"
>
<Style x:Key="{x:Type cc:BackgroundControl}" TargetType="{x:Type cc:BackgroundControl}">
<Setter Property="Grid.ColumnSpan" Value="4" />
<Setter Property="Grid.RowSpan" Value="3" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type cc:BackgroundControl}">
<Grid>
<Rectangle>
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0" Color="White" />
<GradientStop Offset="1" Color="#cc99ccff" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
...
</ResourceDictionary>
The Windows application
To apply themes from the external assembly, I just deploy it into the windows application directory and I choose to load the dictionaries when the application starts.
Because I do not want to reference this assembly, I use an Uri to load dictionaries. Why I don't want to use a reference ? The reason is very simple, I just suppose that during the life of my application, the design team could create another theme that could be applied in deploying this new assembly into the application directory. I just want the themes to be applied dynamically.
The code below doesn't fully implement this concept but the base mechanisms are here:
public
partial class MyApp : Application
{
private List<ResourceDictionary> _themeList;
public MyApp()
{
_themeList = new List<ResourceDictionary>(4);
}
internal List<ResourceDictionary> AvailableThemes
{
get { return _themeList; }
}
private void ApplicationStartUp(object sender, StartupEventArgs e)
{
_themeList.Add((ResourceDictionary)Application.LoadComponent(new Uri(@"RD.CustomThemes;;;component\themes/customthemes.luna.baml", UriKind.Relative)));
_themeList.Add((ResourceDictionary)Application.LoadComponent(new Uri(@"RD.CustomThemes;;;component\themes/customthemes.xbox.baml", UriKind.Relative)));
_themeList.Add((ResourceDictionary)Application.LoadComponent(new Uri(@"RD.CustomThemes;;;component\themes/customthemes.toon.baml", UriKind.Relative)));
}
}
The Uri must use a particular format: AssemblyName;vVersion;PublicKeyToken;component\ThemeResourceName
The code below enables to switch from one theme to another without forgetting the default one:
protected
override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.Key == Key.F5)
{
++_themeIndex;
if (_themeIndex > this.MyApplication.AvailableThemes.Count - 1)
{
_themeIndex = -1;
Application.Current.Resources = null;
}
else
Application.Current.Resources = this.MyApplication.AvailableThemes[_themeIndex];
}
}
Conclusion
As you was able to see, with WPF, Microsoft introduces a new business for windows applications e.g. in the industrial application design process, the UI can be externalized and can be in charge by a different team from the development. I think that in a near future, we will see companies propose skins and themes for many types of application. Furthermore the rioterdeckers are integrating these concepts in an evolved UI engine. Stay tuned...
Download the sources
Download the binaries
(WinFX February CTP)