【问题标题】:How do I Change colors of the native WPF control theme?如何更改本机 WPF 控件主题的颜色?
【发布时间】:2020-10-22 22:58:05
【问题描述】:

如何更改 Windows 10 下原生 WPF 控件主题使用的基础颜色?我知道有 MahApps.Metro 和 MUI 之类的库,但我想做的只是让我的应用程序中的元素使用一致的颜色绘制(MenuItem 和工具栏,我在看着你和你不那么和谐的颜色) .我也想提供各种颜色主题。

我该怎么做?

我不得不承认我只是不明白我所问的是否可能。一些调查表明,默认的 WPF 主题使用静态资源来处理按钮背景等内容:

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

如果该资源是静态的,我想我无法更改它?简单地复制所有原始 WPF 模板并以某种方式将静态资源替换为动态资源是否有意义?

【问题讨论】:

  • 安迪的回答很好!我想补充一点——主题控制是一个大项目。有很多控件,每个控件都有许多特定的控件模板和可能的动画。您还必须考虑更改前景色以更好地与背景形成对比。 @Steven,我也推荐 MaterialThemes - 不要试图重新发明轮子,除非你真的打算开始你自己的主题项目。无论如何,材料主题也是开始学习如何主题化的好地方。你可以查看他们的源代码,学习并做出改变。您甚至可以为该项目做出贡献。

标签: wpf styles skinning theming


【解决方案1】:

为所有控件和颜色“正确”执行此操作比您想象的要复杂得多。

有些画笔使用 Windows 主题颜色,有些使用硬编码值。

您可以覆盖 Windows 主题颜色。

例如,在 app.xaml 中合并的资源字典中,您可以对所有颜色和画笔设置自己的偏好。

这是一个:

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

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

你会发现这只会改变一些事情。

您必须重新模板控件以替换硬编码值。

这是什么意思?

看看单选按钮模板:

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

在那里你会看到类似的东西:

          <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>

这些资源在主模板下方的一个长长的列表中定义。你会想要替换所有这些。

无论您想让您的控件全部使用 Windows 主题还是您自己的主题,如果您从头开始,您还有很多工作要做。

您可能想看看可用的各种预卷主题。 Material design 很受欢迎,因为很多人都熟悉 android。

http://materialdesigninxaml.net/

【讨论】:

  • 谢谢@Andy,这几乎证实了我的怀疑。我正在开发一个数据密集型 IDE 风格的工具,我发现所有现有的主题都有太多的空白或大字体。我将看到从一个现有的开源项目开始,然后调低字体大小和间距。
  • 听起来像是一个计划。如果我是你,我绝对不会从头开始@Steven
【解决方案2】:

我最近为我正在开发的应用程序创建了一些自定义主题。首先要注意的是 WPF 中的控件是 SolidColorBrush 类。这些可以通过在不同构造函数中采用 RGBA 的各种方法来构造。

这是一项很大的工作,所以如果它太多,至少跳到我在 App.xaml 中的样式 xaml 示例,并注意您可以在设置器中绑定到 ViewModel 中的变量,从而使静态资源返回动态颜色值。这就是绑定的美妙之处。您仍然通过 {StaticResource} 分配样式,但它给出的值由代码中的绑定变量确定。

我创建了三个类来处理选项和主题。一个 OptionsMenu.xaml 文件作为一个对话框让用户选择,一个 OptionsMenuVM.cs 来设置绑定,Options.cs 来保存数据。它们分别是视图、视图模型和模型。

Options.cs 有

     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";
    }

视图模型有

    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 有

    <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>

这为您提供了一个组合框来选择主题,如果他们选择自定义,我启用几个按钮让他们从颜色选择器中选择。您可以从颜色选择器中获取 RGB 并以这种方式设置颜色。如果他们选择了一个普通的主题,UpdateColors() 将查看 selectedTheme 并将颜色设置为 option.cs 中的默认颜色。

这是我的 viewModel 中按钮调用以选择自定义颜色的函数。顺便说一句,标准主题更容易添加自定义颜色使其更复杂。

    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;
            }
        }
    }

为了使这些颜色在我放入 App.xaml 的应用程序中可用

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

OptionsMenu.xaml 通过

获取它的数据上下文
    <Window Height="360" Width="564.225" DataContext="{StaticResource optionsMenuVM}" Background="{Binding DarkPanelColor}" Name="OptionsWindow">

现在您可以在应用程序的任何位置设置控件的背景

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

然后您可以选择您想要的任何颜色主题。我只有四种颜色,所以你必须决定有多少种颜色。最好是一种或一组控件的颜色。所以每个 TextBox 都使用 TextBoxColor 或任何你叫它的东西。

这是我在 App.xaml 中放入 TextBox 样式的示例

    <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>

那么对于每个这样的文本框你都可以写

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

您可以对其他控件执行相同操作。只要在设置颜色时调用 PropertyChanged 事件,整个应用程序应该会立即更新。背景颜色绑定将全部从您的视图模型中提取。

我不得不省略部分内容,因为我有一个很大的选项菜单和很多事情要做,但我希望我如何做到这一点的总体思路能够得到理解。一旦你做了这样的事情,你想做的一件事就是保存选项数据。

由于视图模型仅包含 Options.cs 的实例,因此将此类实例替换为已加载的实例将使您回到上次中断的位置。在我看来,将这些数据保存到您可以通过的 AppData 文件夹中是个好主意

    Environment.SpecialFolder.ApplicationData

然后将您的程序的新文件夹附加到该路径(Path.Combine 是您的朋友)并在那里保存一个文件供您选择。我尝试了序列化,但纯色画笔没有序列化,所以我使用 BinaryWriter 保存 RGB 值并将它们读回。这是我的保存和加载功能

    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);
                }
            }
        }
    }

我通过捕获主窗口上的关闭事件并调用保存函数来保存此文件。当程序打开时调用加载,如果没有任何内容,它会转到默认起始值​​。

我有变量 originalUserOptions 让用户取消。它被构造为用户打开菜单时存在的选项的副本。实际的选项实例由他们的输入编辑,他们看到颜色发生了变化。如果他们点击确定,则选项保持不变,如果他们点击取消,我将选项设置回原始实例,它会回到他们开始时的状态。在我的选项设置器中,我必须为所有相关数据调用 PropertyChanged 才能更新视图。

我知道这里有很多内容,但这是一个应用程序范围的数据整理练习,它分布在许多文件中。如果您尝试其中任何一个,请不要复制粘贴我进行了很多工作的整个块,我不得不尝试提取相关部分。以它作为想法的一个例子,并使用一些想法自己构建一个主题系统。抱歉,如果实际使用太多。祝你好运。

【讨论】:

  • 这是一个有趣的解决方案,应该可以工作(我还没有尝试过,但它看起来很可靠)。我只是想知道,这也可以只用 xaml 中的样式来完成吗?我一直在尝试使用带有颜色定义的 xaml 文件和带有我的实际样式(都作为合并字典)的 xaml 文件,然后更新第一个字典中的颜色。然而,这只是部分更新了我期望它适用于所有人的控制颜色。
  • 回答我自己的问题(这不是我第一次在 SO 上发布后不久找到答案......):是的,这可以做到。我的样式设置不完全正确,所以一开始我无法让它完全工作,但我找到了解决方案。
  • 您绝对可以在 xaml 中定义颜色。我选择了一个更加分裂的结构,因为它是针对整个选项菜单的,并且与颜色只有一点关系。我需要将数据保存到用户 AppData 文件夹中的文件中,以便不同的用户有自己的选择。真正的目标是将所有内容封装起来,以便读取和读取文件,并为选项中的数据留出空间,以便稍后使用更多子菜单进行膨胀。
猜你喜欢
  • 2012-04-29
  • 2018-10-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-22
  • 2022-08-18
  • 1970-01-01
相关资源
最近更新 更多