When I first read about WPF, I was very happy to hear that WPF supports themes. After a short reading, I had understand that a WPF theme is actually a XAML file which is automatically loaded as you run a WPF application on different operating systems such as Windows XP, Vista, etc, running with a different theme (luna, royale, etc). Actually, there is an option to create a new theme to work with WPF, but it is too burden and difficult to create.
It is very nice that WPF provides an option to choose a theme for your applications, when you replace the Windows Theme. But what if you want to choose different themes for the same application, for different customers, and without replacing their Windows Theme?
For example, you want different colors or different styles for your controls, depends on user configuration.
To overcome this problem, I have developed a simple mechanism for loading dynamic themes at runtime. This mechanism provides an easy way to replace one theme with another, only by replacing the theme file, and without compiling it, or the WPF application.
First, you have to create your theme file as a XAML resource-dictionary file, which defines colors, templates and styles as resources.
There are actually two ways for loading this theme file:
1. Static Theme - merge the theme file with the application resources using XAML.
2. Dynamic Theme - merge the theme file with the application resources using WPF API.
The former is simpler but also static since your theme file must always be located under the same directory, under the same name. The latter is more dynamic, it provides you an option to select the theme file name and location at runtime.
To implement the static-method, do as follows:
- Create a new WPF project
- Create a new project directory, call it themes
- Create a new XAML dictionary file under the themes directory, call it current.theme.xaml
- Select the file, and change its "Build Action" to Content, Copy if newer
- Paste one of the themes I have added bellow into the theme file you have just created
- Open the App.xaml file, and replace the <Application.Resources>...</Application.Resources> with this one:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="themes\current.theme.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
Add one or more buttons to the main window, build and run the code.
Now replace the theme file-content with the other one (drop the bin\debug\themes\current.theme.xaml file into visual studio, paste the second theme and save). Run the .exe file without compiling it again!!! You can see that the theme has change.
The problem with the static-method is that you should use the same file name for each different theme.
To implement the dynamic-method, do as follows:
- Create a new WPF project
- Create a new project directory, call it themes
- Add one or more XAML dictionary files under the themes directory
- Set each file "Build Action" to Content, Copy if newer
- Open the App.xaml.cs file, and override the method bellow:
protected override void OnStartup(StartupEventArgs e)
{ if (e.Args.Length == 1 && File.Exists(e.Args[0]))
{ Uri themeUri = new Uri(e.Args[0], UriKind.Relative);
ResourceDictionary theme = (ResourceDictionary)Application.LoadComponent(themeUri);
Resources.MergedDictionaries.Add(theme);
}
base.OnStartup(e);
}
Add one or more buttons to the main window, build and run the code with the following command line argument:
<your_app_name>.exe themes\<your_theme.xaml>
Now try to use other theme, or just change the theme file, and run the application again, but without compiling it!!!
So now you are able to create dynamic themes, which you can replace whenever you like to, and without compiling your application.
The problem with these two solutions is, somehow WPF searches for the resource names while merging dictionaries files. Hence, you must add each theme file as a project element, although you use it as a content rather than a resource (weird!!!). Still, the resource is dynamic, since you can always use the same files, and all you have to do is to replace their content without compiling the application again.
[bubble.theme.xaml]
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Default Color -->
<SolidColorBrush x:Key="DefaultBackgroundBrush" Color="#FF1F2A43"/>
<SolidColorBrush x:Key="DefaultBorderBrush" Color="#FF265184"/>
<SolidColorBrush x:Key="DefaultForegroundBrush" Color="#FFBFD1FF"/>
<LinearGradientBrush x:Key="DefaultBubbleBrush" EndPoint="0.545,2.847" StartPoint="0.545,-0.639">
<GradientStop Color="#FFCBCEFF" Offset="0.115"/>
<GradientStop Color="#FF2A3756" Offset="0.476"/>
</LinearGradientBrush>
<!-- Focus Color -->
<SolidColorBrush x:Key="FocusedBorderBrush" Color="#FF7482E5" />
<LinearGradientBrush x:Key="FocusedBubbleBrush" EndPoint="0.545,2.847" StartPoint="0.545,-0.639">
<GradientStop Color="#FFCBCEFF" Offset="0.115"/>
<GradientStop Color="#FF18183F" Offset="0.731"/>
</LinearGradientBrush>
<!-- Highlight Color -->
<SolidColorBrush x:Key="HighlightedBorderBrush" Color="#FF5F6CC8"/>
<LinearGradientBrush x:Key="HighlightedBackgroundBrush" EndPoint="0.505,2.832" StartPoint="0.505,-0.654">
<GradientStop Color="#FF2C3B5C" Offset="0"/>
<GradientStop Color="#FF1F2A43" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="HighlightedBubbleBrush" EndPoint="0.545,2.847" StartPoint="0.545,-0.639">
<GradientStop Color="#FFCBCEFF" Offset="0.052"/>
<GradientStop Color="#FF2C3B5C" Offset="0.457"/>
</LinearGradientBrush>
<SolidColorBrush x:Key="DefaultControlBrightBrush" Color="#FF293D66"/>
<!-- Press Color -->
<LinearGradientBrush x:Key="PressedBackgroundBrush" EndPoint="0.518,0.948" StartPoint="0.514,0.104">
<GradientStop Color="#FF14213F" Offset="0"/>
<GradientStop Color="#FF36476E" Offset="1"/>
<GradientStop Color="#FF0D0F1C" Offset="0.423"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="PressedBubbleBrush" EndPoint="0.545,2.847" StartPoint="0.545,-0.639">
<GradientStop Color="#FFCBCEFF" Offset="0"/>
<GradientStop Color="#FF1A1E3B" Offset="0.212"/>
</LinearGradientBrush>
<!-- Button Template -->
<Style BasedOn="{x:Null}" TargetType="{x:Type Button}"> <Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}"> <Grid>
<Grid.RowDefinitions>
<RowDefinition Height="0.5*"/>
<RowDefinition Height="0.5*"/>
</Grid.RowDefinitions>
<Rectangle x:Name="PART_Shape"
RadiusX="4" RadiusY="4"
Grid.RowSpan="2"
Margin="0,-0.333,0,0.333"
Fill="{StaticResource DefaultBackgroundBrush}" Stroke="{StaticResource DefaultBorderBrush}" />
<Rectangle x:Name="PART_Bubble"
Opacity="0.6"
RadiusX="3.333"
RadiusY="3.333"
Margin="5,3,5,0"
VerticalAlignment="Stretch"
Height="Auto"
Grid.RowSpan="1"
Fill="{StaticResource DefaultBubbleBrush}" />
<ContentPresenter x:Name="PART_Content"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="8,4,8,4"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Grid.RowSpan="2"
RecognizesAccessKey="True"
RenderTransformOrigin="0.5,0.5"
/>
</Grid>
<ControlTemplate.Triggers>
<!-- Enabled -->
<Trigger Property="IsEnabled" Value="True">
<Setter Property="Foreground" Value="{StaticResource DefaultForegroundBrush}"/> </Trigger>
<!-- Focused -->
<Trigger Property="IsFocused" Value="True">
<Setter Property="Stroke" TargetName="PART_Shape" Value="{StaticResource FocusedBorderBrush}" /> <Setter Property="Fill" TargetName="PART_Bubble" Value="{StaticResource FocusedBubbleBrush}" /> </Trigger>
<!-- Mouse Over -->
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Fill" TargetName="PART_Shape" Value="{StaticResource HighlightedBackgroundBrush}" /> <Setter Property="Stroke" TargetName="PART_Shape" Value="{StaticResource HighlightedBorderBrush}" /> <Setter Property="Fill" TargetName="PART_Bubble" Value="{StaticResource HighlightedBubbleBrush}" /> </Trigger>
<!-- Pressed -->
<Trigger Property="IsPressed" Value="True">
<Setter Property="Fill" TargetName="PART_Shape" Value="{StaticResource PressedBackgroundBrush}" /> <Setter Property="Fill" TargetName="PART_Bubble" Value="{StaticResource PressedBubbleBrush}"/> <Setter Property="RenderTransform" TargetName="PART_Content">
<Setter.Value>
<TransformGroup>
<TranslateTransform X="1" Y="1" />
</TransformGroup>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
[flat.theme.xaml]
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Default Color -->
<SolidColorBrush x:Key="DefaultBackgroundBrush" Color="#FF1F2A43"/>
<SolidColorBrush x:Key="DefaultBorderBrush" Color="#FF265184"/>
<SolidColorBrush x:Key="DefaultForegroundBrush" Color="#FFBFD1FF"/>
<LinearGradientBrush x:Key="DefaultBubbleBrush" EndPoint="0.545,2.847" StartPoint="0.545,-0.639">
<GradientStop Color="#FFCBCEFF" Offset="0.115"/>
<GradientStop Color="#FF2A3756" Offset="0.476"/>
</LinearGradientBrush>
<!-- Focus Color -->
<SolidColorBrush x:Key="FocusedBorderBrush" Color="#FF7482E5" />
<LinearGradientBrush x:Key="FocusedBubbleBrush" EndPoint="0.545,2.847" StartPoint="0.545,-0.639">
<GradientStop Color="#FFCBCEFF" Offset="0.115"/>
<GradientStop Color="#FF18183F" Offset="0.731"/>
</LinearGradientBrush>
<!-- Highlight Color -->
<SolidColorBrush x:Key="HighlightedBorderBrush" Color="#FF5F6CC8"/>
<LinearGradientBrush x:Key="HighlightedBackgroundBrush" EndPoint="0.505,2.832" StartPoint="0.505,-0.654">
<GradientStop Color="#FF2C3B5C" Offset="0"/>
<GradientStop Color="#FF1F2A43" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="HighlightedBubbleBrush" EndPoint="0.545,2.847" StartPoint="0.545,-0.639">
<GradientStop Color="#FFCBCEFF" Offset="0.052"/>
<GradientStop Color="#FF2C3B5C" Offset="0.457"/>
</LinearGradientBrush>
<SolidColorBrush x:Key="DefaultControlBrightBrush" Color="#FF293D66"/>
<!-- Press Color -->
<LinearGradientBrush x:Key="PressedBackgroundBrush" EndPoint="0.518,0.948" StartPoint="0.514,0.104">
<GradientStop Color="#FF14213F" Offset="0"/>
<GradientStop Color="#FF36476E" Offset="1"/>
<GradientStop Color="#FF0D0F1C" Offset="0.423"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="PressedBubbleBrush" EndPoint="0.545,2.847" StartPoint="0.545,-0.639">
<GradientStop Color="#FFCBCEFF" Offset="0"/>
<GradientStop Color="#FF1A1E3B" Offset="0.212"/>
</LinearGradientBrush>
<!-- Button Template -->
<Style BasedOn="{x:Null}" TargetType="{x:Type Button}"> <Setter Property="BorderBrush" Value="{StaticResource DefaultBorderBrush}"/> <Setter Property="Background" Value="{StaticResource DefaultBackgroundBrush}"/> <Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}"> <Grid>
<Rectangle x:Name="PART_Shape"
RadiusX="4"
RadiusY="4"
Fill="{TemplateBinding Background}" Stroke="{TemplateBinding BorderBrush}" StrokeThickness="1"
/>
<ContentPresenter x:Name="PART_Content"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" RecognizesAccessKey="True"
Margin="8,4,8,4"
/>
</Grid>
<ControlTemplate.Triggers>
<!-- Enabled -->
<Trigger Property="IsEnabled" Value="True">
<Setter Property="Foreground" Value="{StaticResource DefaultForegroundBrush}"/> </Trigger>
<!-- Focused -->
<Trigger Property="IsFocused" Value="True">
<Setter Property="Stroke" TargetName="PART_Shape" Value="{StaticResource FocusedBorderBrush}"/> </Trigger>
<!-- Mouse Over -->
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Fill" TargetName="PART_Shape" Value="{StaticResource HighlightedBackgroundBrush}" /> <Setter Property="Stroke" TargetName="PART_Shape" Value="{StaticResource HighlightedBorderBrush}"/> <Setter Property="StrokeThickness" TargetName="PART_Shape" Value="2"/>
</Trigger>
<!-- Pressed -->
<Trigger Property="IsPressed" Value="True">
<Setter Property="Fill" TargetName="PART_Shape" Value="{StaticResource PressedBackgroundBrush}" /> <Setter Property="RenderTransform" TargetName="PART_Content">
<Setter.Value>
<TransformGroup>
<TranslateTransform X="1" Y="1" />
</TransformGroup>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
If you find a bug, or an error, I will be glad to hear about.