How do I Change colors of the native WPF control theme?

4k Views Asked by At

How can I change the foundation colours used by the native WPF control theming under Windows 10? I know there are libraries like MahApps.Metro, and MUI, but all I want to do is make the elements in my application drawn with consistent colours (MenuItem and Toolbar, I'm looking at you and your not-so-harmonious colors). I would also like to provide a variety of colour themes.

How do I do that?

I have to admit I just don't understand whether what I am asking is possible or not. Some investigation indicate that the default WPF theme uses static resources for things like the button background:

<Setter Property="Background" Value="{StaticResource Button.Static.Background}"/>

If that resource is static, I guess I can't change it? Would it make sense to simply copy all the original WPF templates and replace static resources with dynamic resources somehow?

2

There are 2 best solutions below

2
On BEST ANSWER

To do this "properly" for all controls and colours is rather more involved than you might imagine.

Some of the brushes use windows theme colours, some use hard coded values.

You can over-ride windows theme colours.

For example, in a resource dictionary merged in app.xaml you could set your own preferences on all the colors and brushes.

Here's one:

 <SolidColorBrush Color="LimeGreen" x:Key="{x:Static SystemColors.HighlightBrushKey}"/>

https://learn.microsoft.com/en-us/dotnet/api/system.windows.systemcolors?view=netcore-3.1

You will find this only changes some things though.

You would have to re-template controls to replace the hard coded values.

What do I mean by that?

Take a look at the radiobutton template:

https://learn.microsoft.com/en-us/dotnet/desktop/wpf/controls/radiobutton-styles-and-templates?view=netframeworkdesktop-4.8

In there you'll see stuff like:

          <Ellipse x:Name="Border"
                   StrokeThickness="1">
            <Ellipse.Stroke>
              <LinearGradientBrush EndPoint="0.5,1"
                                   StartPoint="0.5,0">
                <GradientStop Color="{DynamicResource BorderLightColor}"
                              Offset="0" />
                <GradientStop Color="{DynamicResource BorderDarkColor}"
                              Offset="1" />
              </LinearGradientBrush>
            </Ellipse.Stroke>
            <Ellipse.Fill>
              <LinearGradientBrush StartPoint="0,0"
                                   EndPoint="0,1">
                <LinearGradientBrush.GradientStops>
                  <GradientStopCollection>
                    <GradientStop Color="{DynamicResource ControlLightColor}" />
                    <GradientStop Color="{DynamicResource ControlMediumColor}"
                                  Offset="1.0" />
                  </GradientStopCollection>
                </LinearGradientBrush.GradientStops>
              </LinearGradientBrush>
            </Ellipse.Fill>
          </Ellipse>

And those resources are defined in a big long list below the main template. You'd want to replace all these.

Whether you want to make your controls all use windows theming or your own theme, you've got a lot of work to do if you start from scratch.

You might want to take a look at the various pre-rolled themes which are available. Material design is quite popular seeing as a lot of people are familiar with android.

http://materialdesigninxaml.net/

3
On

I recently created some custom themes for an application I'm working on. The first thing to note is that controls in WPF are the class SolidColorBrush. These can be constructed from various methods that take RGBA in different constructors.

This is a large effort so if it's too much at least jump down to my Style xaml example in App.xaml and notice that you can bind to a variable in a ViewModel in your setters thereby making a StaticResource return Dynamic Color values. That's the beauty of Bindings. You still assign the style through {StaticResource} but the values it gives are determined by binding variables in the code.

I created three classes to handle options and theming. An OptionsMenu.xaml file as a dialog to let the user pick, an OptionsMenuVM.cs to set the bindings, and Options.cs to hold the data. These are the view, viewmodel, and model respectively.

Options.cs has

     public class Options
{
    //white theme color values
    public SolidColorBrush whiteThemeLightPanelColor = new SolidColorBrush(Color.FromRgb(255, 255, 255));
    public SolidColorBrush whiteThemeDarkPanelColor = new SolidColorBrush(Color.FromRgb(230, 230, 230));
    public SolidColorBrush whiteThemeTextBoxBackgroundColor = new SolidColorBrush(Color.FromRgb(255, 255, 255));
    public SolidColorBrush whiteThemeTextBoxForegroundColor = new SolidColorBrush(Color.FromRgb(0, 0, 0));

    //light theme color values
    public SolidColorBrush lightThemeLightPanelColor = new SolidColorBrush(Color.FromRgb(200, 200, 200));
    public SolidColorBrush lightThemeDarkPanelColor = new SolidColorBrush(Color.FromRgb(225, 225, 225));
    public SolidColorBrush lightThemeTextBoxBackgroundColor = new SolidColorBrush(Color.FromRgb(180, 180, 180));
    public SolidColorBrush lightThemeTextBoxForegroundColor = new SolidColorBrush(Color.FromRgb(0, 0, 0));

    //dark theme color values
    public SolidColorBrush darkThemeLightPanelColor = new SolidColorBrush(Color.FromRgb(100, 100, 100));
    public SolidColorBrush darkThemeDarkPanelColor = new SolidColorBrush(Color.FromRgb(70, 70, 70));
    public SolidColorBrush darkThemeTextBoxBackgroundColor = new SolidColorBrush(Color.FromRgb(50, 50, 50));
    public SolidColorBrush darkThemeTextBoxForegroundColor = new SolidColorBrush(Color.FromRgb(255, 255, 255));

    //blue theme color values
    public SolidColorBrush blueThemeLightPanelColor = new SolidColorBrush(Color.FromRgb(105, 175, 209));
    public SolidColorBrush blueThemeDarkPanelColor = new SolidColorBrush(Color.FromRgb(34, 103, 140));
    public SolidColorBrush blueThemeTextBoxBackgroundColor = new SolidColorBrush(Color.FromRgb(211, 211, 211));
    public SolidColorBrush blueThemeTextBoxForegroundColor = new SolidColorBrush(Color.FromRgb(0, 0, 0));

    //most recent custom color values
    public SolidColorBrush lastCustomLightPanelColor = new SolidColorBrush(Color.FromRgb(200, 200, 200));
    public SolidColorBrush lastCustomDarkPanelColor = new SolidColorBrush(Color.FromRgb(225, 225, 225));
    public SolidColorBrush lastCustomTextBoxBackgroundColor = new SolidColorBrush(Color.FromRgb(180, 180, 180));
    public SolidColorBrush lastCustomTextBoxForegroundColor = new SolidColorBrush(Color.FromRgb(0, 0, 0));

    //runtime color values
    public SolidColorBrush lightPanelColor;
    public SolidColorBrush darkPanelColor;
    public SolidColorBrush textBoxBackgroundColor;
    public SolidColorBrush textBoxForegroundColor;

    public string chosenTheme = "Light";
    }

The viewmodel has

    private Options userOptions;
    public Options UserOptions
    {
        get => userOptions;
        set
        {
            userOptions = value;

            OnPropertyChanged("UserOptions");

            OnPropertyChanged("ChosenTheme");

            UpdateColors();
        }
    }
    public SolidColorBrush LightPanelColor
    {
        get => userOptions.lightPanelColor;
        set
        {
            userOptions.lightPanelColor = value;
            OnPropertyChanged("LightPanelColor");
        }
    }

    public SolidColorBrush DarkPanelColor
    {
        get => userOptions.darkPanelColor;
        set
        {
            userOptions.darkPanelColor = value;
            OnPropertyChanged("DarkPanelColor");
        }
    }

    public SolidColorBrush TextBoxBackgroundColor
    {
        get => userOptions.textBoxBackgroundColor;
        set
        {
            userOptions.textBoxBackgroundColor = value;
            OnPropertyChanged("TextBoxBackgroundColor");
        }
    }
   public ObservableCollection<string> ThemeOptions { get; } = new ObservableCollection<string>() { "White", "Light", "Blue", "Dark", "Custom" };

    
    public string ChosenTheme
    {
        get => userOptions.chosenTheme;
        set
        {
            userOptions.chosenTheme = value;
            OnPropertyChanged("ChosenTheme");
            OnPropertyChanged("EnableColorSelection");
            UpdateColors();
        }
    }

    public void UpdateColors()
    {
        if(userOptions.chosenTheme.Equals("White"))
        {
            LightPanelColor = userOptions.whiteThemeLightPanelColor;
            DarkPanelColor = userOptions.whiteThemeDarkPanelColor;
            TextBoxBackgroundColor = userOptions.whiteThemeTextBoxBackgroundColor;
            TextBoxForegroundColor = userOptions.whiteThemeTextBoxForegroundColor;
        }
        else if (userOptions.chosenTheme.Equals("Light"))
        {
            LightPanelColor = userOptions.lightThemeLightPanelColor;
            DarkPanelColor = userOptions.lightThemeDarkPanelColor;
            TextBoxBackgroundColor = userOptions.lightThemeTextBoxBackgroundColor;
            TextBoxForegroundColor = userOptions.lightThemeTextBoxForegroundColor;
        }
        else if (userOptions.chosenTheme.Equals("Dark"))
        {
            LightPanelColor = userOptions.darkThemeLightPanelColor;
            DarkPanelColor = userOptions.darkThemeDarkPanelColor;
            TextBoxBackgroundColor = userOptions.darkThemeTextBoxBackgroundColor;
            TextBoxForegroundColor = userOptions.darkThemeTextBoxForegroundColor;
        }
        else if (userOptions.chosenTheme.Equals("Blue"))
        {
            LightPanelColor = userOptions.blueThemeLightPanelColor;
            DarkPanelColor = userOptions.blueThemeDarkPanelColor;
            TextBoxBackgroundColor = userOptions.blueThemeTextBoxBackgroundColor;
            TextBoxForegroundColor = userOptions.blueThemeTextBoxForegroundColor;
        }
        else if(userOptions.chosenTheme.Equals("Custom"))
        {
            LightPanelColor = userOptions.lastCustomLightPanelColor;
            DarkPanelColor = userOptions.lastCustomDarkPanelColor;
            TextBoxBackgroundColor = userOptions.lastCustomTextBoxBackgroundColor;
            TextBoxForegroundColor = userOptions.lastCustomTextBoxForegroundColor;
        }
    }

Options.xaml has

    <StackPanel Orientation="Horizontal">
                    <Label Content="Theme: " />
                    <ComboBox Width="150" ItemsSource="{Binding ThemeOptions}" SelectedItem="{Binding ChosenTheme}" />
                </StackPanel>
                <GroupBox Header="Custom Theme Color Selections" IsEnabled="{Binding EnableColorSelection}" Style="{StaticResource GroupBoxStyle}">
                    <StackPanel Orientation="Vertical">
                        <StackPanel Orientation="Horizontal">
                            <Label Content="Light Panel Background: " Style="{StaticResource OptionsLabel}"/>
                            <Button Foreground="{Binding Path=LightPanelColor, UpdateSourceTrigger=PropertyChanged}" ToolTip="Click to Change Color" Width="28" Height="18" Command="{Binding ChooseColorCmd}" CommandParameter="LightPanelColor">
                                <Rectangle Width="40" Height="15" Fill="{Binding Path=LightPanelColor, UpdateSourceTrigger=PropertyChanged}"/>
                            </Button>
                        </StackPanel>

                        <StackPanel Orientation="Horizontal">
                            <Label Content="Dark Panel Background: " Style="{StaticResource OptionsLabel}"/>
                            <Button Foreground="{Binding Path=DarkPanelColor, UpdateSourceTrigger=PropertyChanged}" ToolTip="Click to Change Color" Width="28" Height="18" Command="{Binding ChooseColorCmd}" CommandParameter="DarkPanelColor">
                                <Rectangle Width="40" Height="15" Fill="{Binding Path=DarkPanelColor, UpdateSourceTrigger=PropertyChanged}"/>
                            </Button>
                        </StackPanel>

                        <StackPanel Orientation="Horizontal">
                            <Label Content="Text Box Background: " Style="{StaticResource OptionsLabel}"/>
                            <Button Foreground="{Binding Path=TextBoxBackgroundColor, UpdateSourceTrigger=PropertyChanged}" ToolTip="Click to Change Color" Width="28" Height="18" Command="{Binding ChooseColorCmd}" CommandParameter="TextBoxBackgroundColor">
                                <Rectangle Width="40" Height="15" Fill="{Binding Path=TextBoxBackgroundColor, UpdateSourceTrigger=PropertyChanged}"/>
                            </Button>
                        </StackPanel>

                        <StackPanel Orientation="Horizontal">
                            <Label Content="Text Box Foreground: " Style="{StaticResource OptionsLabel}"/>
                            <Button Foreground="{Binding Path=TextBoxForegroundColor, UpdateSourceTrigger=PropertyChanged}" ToolTip="Click to Change Color" Width="28" Height="18" Command="{Binding ChooseColorCmd}" CommandParameter="TextBoxForegroundColor">
                                <Rectangle Width="40" Height="15" Fill="{Binding Path=TextBoxForegroundColor, UpdateSourceTrigger=PropertyChanged}"/>
                            </Button>
                        </StackPanel>
                    </StackPanel>
                </GroupBox>

This gives you a combo box to select the theme and if they choose custom I enable a few buttons to let them pick from a color picker. You can get the RGB from the color picker and set the colors that way. If they pick a normal theme, UpdateColors() will look at the chosenTheme and set the colors to the default ones in option.cs.

Here is the function in my viewModel that the buttons call to pick custom colors. Btw standard themes are easier adding the custom colors makes it more complicated.

    private void ChooseColor(object cp)
    {
        string caller = cp.ToString();
        System.Windows.Forms.ColorDialog cd = new System.Windows.Forms.ColorDialog();
        Color startColor = Color.FromRgb(0,0,0);
        if (caller.Equals("LightPanelColor"))
        {
            startColor = LightPanelColor.Color;
        }
        else if (caller.Equals("DarkPanelColor"))
        {
            startColor = DarkPanelColor.Color;
        }
        else if (caller.Equals("TextBoxBackgroundColor"))
        {
            startColor = TextBoxBackgroundColor.Color;
        }
        else if (caller.Equals("TextBoxForegroundColor"))
        {
            startColor = TextBoxForegroundColor.Color;
        }
        else
        {
            return;
        }

        cd.Color = System.Drawing.Color.FromArgb(startColor.A, startColor.R, startColor.G, startColor.B);
        if (cd.ShowDialog() == System.Windows.Forms.DialogResult.OK)
        {
            if(caller.Equals("LightPanelColor"))
            {
                LightPanelColor.Color = Color.FromArgb(cd.Color.A, cd.Color.R, cd.Color.G, cd.Color.B);
                OnPropertyChanged("LightPanelColor");
            }
            else if(caller.Equals("DarkPanelColor"))
            {
                DarkPanelColor.Color = Color.FromArgb(cd.Color.A, cd.Color.R, cd.Color.G, cd.Color.B);
                OnPropertyChanged("DarkPanelColor");
            }
            else if(caller.Equals("TextBoxBackgroundColor"))
            {
                TextBoxBackgroundColor.Color = Color.FromArgb(cd.Color.A, cd.Color.R, cd.Color.G, cd.Color.B);
                OnPropertyChanged("TextBoxBackgroundColor");
            }
            else if(caller.Equals("TextBoxForegroundColor"))
            {
                TextBoxForegroundColor.Color = Color.FromArgb(cd.Color.A, cd.Color.R, cd.Color.G, cd.Color.B);
                OnPropertyChanged("TextBoxForegroundColor");
            }
            else
            {
                return;
            }
        }
    }

To make these colors available across the application I put in App.xaml

    <vm:OptionsMenuVM x:Key="optionsMenuVM"/>

OptionsMenu.xaml gets it's data context through

    <Window Height="360" Width="564.225" DataContext="{StaticResource optionsMenuVM}" Background="{Binding DarkPanelColor}" Name="OptionsWindow">

Now anywhere in your application you can set the Background of controls by

    Background="{Binding Path=DarkPanelColor, Source={StaticResource optionsMenuVM}}">

Then you can pick whichever color theme you wanted. I just have four colors so you have to decide how many colors. Preferably a color for a type or set of controls. So every TextBox uses TextBoxColor or whatever you call it.

Here's an example that I put in my App.xaml of a TextBox style

    <Style x:Key="InputTextbox" TargetType="TextBox">
        <Style.Setters>
            <Setter Property="HorizontalAlignment" Value="Left"/>
            <Setter Property="Width" Value="90"/>
            <Setter Property="Margin" Value="0,2,2,2"/>
            <Setter Property="Background" Value="{Binding Path=TextBoxBackgroundColor, Source={StaticResource optionsMenuVM}}"/>
            <Setter Property="Foreground" Value="{Binding Path=TextBoxForegroundColor, Source={StaticResource optionsMenuVM}}"/>
            <EventSetter Event="Loaded" Handler="TextBox_Loaded"/>
        </Style.Setters>
    </Style>

So then for every TextBox like this you can write

    <TextBox  Style="{StaticResource InputTextbox}"/>

And you can do the same for other controls. As long as you call the PropertyChanged event when you set the colors, the whole application should immediately update. The background color bindings will all pull from your viewmodel.

I had to leave out parts because I have a large options menu and a lot of stuff going on but I hope the general idea of how I did this got across. One thing you want to do once you make something like this is save the options data.

Since the viewmodel just holds and instance of Options.cs, replacing this class instance with a loaded one will bring you back to where you left off. It's a good idea in my opinion to save this data into the AppData folder which you can get through

    Environment.SpecialFolder.ApplicationData

Then append a new folder for your program to that path (Path.Combine is your friend) and save a file there for your options. I tried serializing but Solid Color Brushes don't serialize so I used BinaryWriter to save the RGB values and read them back. Here's my save and load functions

    private void SaveUserOptionsFile()
    {
        
        try
        {
            using (BinaryWriter binaryWriter = new BinaryWriter(File.Open(optionsSavePath, FileMode.Create)))
            {
                Color lastCustomLightPanelColor = userOptions.lastCustomLightPanelColor.Color;
                Color lastCustomDarkPanelColor = userOptions.lastCustomDarkPanelColor.Color;
                Color lastCustomTextBoxBackgroundColor = userOptions.lastCustomTextBoxBackgroundColor.Color;
                Color lastCustomTextBoxForegroundColor = userOptions.lastCustomTextBoxForegroundColor.Color;

                binaryWriter.Write(lastCustomLightPanelColor.R);
                binaryWriter.Write(lastCustomLightPanelColor.G);
                binaryWriter.Write(lastCustomLightPanelColor.B);

                binaryWriter.Write(lastCustomDarkPanelColor.R);
                binaryWriter.Write(lastCustomDarkPanelColor.G);
                binaryWriter.Write(lastCustomDarkPanelColor.B);

                binaryWriter.Write(lastCustomTextBoxBackgroundColor.R);
                binaryWriter.Write(lastCustomTextBoxBackgroundColor.G);
                binaryWriter.Write(lastCustomTextBoxBackgroundColor.B);

                binaryWriter.Write(lastCustomTextBoxForegroundColor.R);
                binaryWriter.Write(lastCustomTextBoxForegroundColor.G);
                binaryWriter.Write(lastCustomTextBoxForegroundColor.B);

                binaryWriter.Write(userOptions.chosenTheme);
            }
        }
        catch(IOException e)
        {
            if(File.Exists(optionsSavePath))
            {
                File.Delete(optionsSavePath);
            }
        }
    }

    public void LoadUserOptionsFile()
    {
        if (File.Exists(optionsSavePath))
        { 
            try
            {
                using (BinaryReader binaryReader = new BinaryReader(File.Open(optionsSavePath, FileMode.Open)))
                {
                    UserOptions.lastCustomLightPanelColor.Color = Color.FromRgb(binaryReader.ReadByte(), binaryReader.ReadByte(), binaryReader.ReadByte());
                    UserOptions.lastCustomDarkPanelColor.Color = Color.FromRgb(binaryReader.ReadByte(), binaryReader.ReadByte(), binaryReader.ReadByte());
                    UserOptions.lastCustomTextBoxBackgroundColor.Color = Color.FromRgb(binaryReader.ReadByte(), binaryReader.ReadByte(), binaryReader.ReadByte());
                    UserOptions.lastCustomTextBoxForegroundColor.Color = Color.FromRgb(binaryReader.ReadByte(), binaryReader.ReadByte(), binaryReader.ReadByte());

                    ChosenTheme = binaryReader.ReadString();

                    originalUserOptions = new Options(UserOptions);
                }
            }
            catch (IOException e)
            {
                UserOptions = new Options();
                originalUserOptions = new Options();
                if (File.Exists(optionsSavePath))
                {
                    File.Delete(optionsSavePath);
                }
            }
        }
    }

I save this file by catching the close event on the main window and calling the save function. Load is called when the program opens and if nothing is there it goes to default starting values.

I have the variable originalUserOptions in there to let the user cancel. It is constructed as a copy of the options that exist when the user opens the menu. The actual options instance is edited by their input and they see the colors change. If they hit ok the options stay and if they hit cancel I set the options back to the original instance and it goes back to how it was when they started. In my setter for options I had to call PropertyChanged for all the relevant data to get it to update the view.

I know there's a ton here but this is an application wide data wrangling exercise and it is spread out over many files. If you try any of this, don't copy paste the whole blocks I had a lot going on and I had to try to pull out the relevant bits. Take it as an example of the idea and build a theme system yourself using some of the ideas. Sorry if it's too much going on to actually use. Good Luck.