DCSIMG
WPF Dynamic Theme - Essential XAML

WPF Dynamic Theme

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:

  1. Create a new WPF project
  2. Create a new project directory, call it themes
  3. Create a new XAML dictionary file under the themes directory, call it current.theme.xaml
  4. Select the file, and change its "Build Action" to Content, Copy if newer
  5. Paste one of the themes I have added bellow into the theme file you have just created
  6. 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:

  1. Create a new WPF project
  2. Create a new project directory, call it themes
  3. Add one or more XAML dictionary files under the themes directory
  4. Set each file "Build Action" to Content, Copy if newer
  5. 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.

Published Wednesday, May 02, 2007 2:58 PM by Tomer Shamam
תגים:,

Comments

# re: WPF Dynamic Theme

Friday, May 04, 2007 10:20 PM by Ran
Nice post, next time add a source code, it will halp.

# re: WPF Dynamic Theme

Wednesday, September 26, 2007 12:42 AM by cacompguy

Hi,

   Nice post. I am using Infragistics control. They do have "Theme" property. How do I style standard controls like button to match diplayed theme dynamically?

Thank you

# re: WPF Dynamic Theme

Friday, September 28, 2007 1:53 PM by Tomer Shamam

Hi cacompguy,

The Infragistics control Theme property is not part of the WPF, also I do not think that the Infragistics Theme property concept overlapped with my post.

You should refer the Infragistics SDK documentation.

# re: WPF Dynamic Theme

Monday, August 30, 2010 1:53 PM by Kris Adams

Hi there,

Any idea of a fix for the issue where loading themes dynamically in Windows XP with .Net 3.5 SP1 causes the app to crash (on the Resources.MergedDictionaries.Add method)?

It's very frustrating, and not a well publicized error. So hard to find an answer to.

# re: WPF Dynamic Theme

Friday, July 29, 2011 5:16 PM by Ilya Tsilikov

by the way i deed the same thing but I have a problem: the controls that are already loaded doesn't apply themes to them. Have you met such problem?

# re: WPF Dynamic Theme

Thursday, April 19, 2012 10:25 AM by kamlendra

can i update or change values of your

 ResourceDictionary theme =  object

like i have a resource dictionary of key value pairs, and bind it to a list view and Displaying the values.

<ListView  ItemsSource="{Binding theme , Mode=TwoWay}"

   ...

  <TextBox Grid.Column="0" Text="{Binding Path=Value, Mode=TwoWay}"/>

now i give flexibility to user to change the value. it is not updating back in the "theme" object.

# re: WPF Dynamic Theme

Monday, July 02, 2012 4:11 PM by Harsh

Hi, My requirement is when user change some value resource xaml file then application should pick new theme value withoud rebuilding this.

# re: WPF Dynamic Theme

Monday, November 26, 2012 2:17 PM by Dave Rose

Be careful when changing themes that have different control templates for the same control and you have other styles based off of them.

I had to move some styles that were the same for all themes and put them in a my custom controls and reload it as a 2nd resource directory after each theme change. I would just clear the resource directory and reload inside a begin init.

Leave a Comment

(required) 
(required) 
(optional)
(required) 

Enter the numbers above:
Powered by Community Server (Commercial Edition), by Telligent Systems