如何在WPF应用程序中创建动态主题?



我刚刚开始从vb.net和winforms过渡到c#和WPF,并开始我的第一个应用程序。我已经做了很多教程,现在是时候尝试把一些东西付诸实践了。

我的第一个挑战是应用程序的主题。我想为我的用户提供一个主题的选择,它将为我的表单上的每个控件预设配色方案。例如明暗模式。我通过在app .xaml中放置样式来管理一些表单主题(应用程序将有多个表单,所以我理解它需要在这里?)。对于下拉主菜单,其中一些必须是复杂的(我取消了这段代码-在我的WPF知识的时候):

<Application.Resources>

<Style TargetType="Window">
<Setter Property="Background" Value="#303030"/>
</Style> 
<Style TargetType="Menu">
<Setter Property="Background" Value="#202020"/>
<Setter Property="Foreground" Value="Gainsboro"></Setter>
<Setter Property="Padding" Value="0,4,0,4"></Setter>
</Style>
<Style TargetType="{x:Type MenuItem}">
<Style.Triggers>
<Trigger Property="MenuItem.Role" Value="TopLevelHeader">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type MenuItem}">
<Border x:Name="templateRoot" SnapsToDevicePixels="true"
BorderThickness="{TemplateBinding Control.BorderThickness}"
Background="{TemplateBinding Control.Background}"
BorderBrush="{TemplateBinding Control.BorderBrush}">
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ContentPresenter x:Name="Icon" ContentSource="Icon" 
SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" VerticalAlignment="Center"
HorizontalAlignment="Center" Width="16" Height="16" Margin="3"/>
<Path x:Name="GlyphPanel" Data="F1 M 10.0,1.2 L 4.7,9.1 L 4.5,9.1 L 0,5.2 L 1.3,3.5 L 4.3,6.1L 8.3,0 L 10.0,1.2 Z" FlowDirection="LeftToRight" Margin="3"
Visibility="Collapsed" VerticalAlignment="Center" Fill="{TemplateBinding Control.Foreground}"/>
<ContentPresenter Grid.Column="1" ContentSource="Header" RecognizesAccessKey="true"
Margin="{TemplateBinding Control.Padding}"
SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}"/>
<Popup x:Name="PART_Popup" AllowsTransparency="true" Focusable="false"
PopupAnimation="{DynamicResource {x:Static SystemParameters.MenuPopupAnimationKey}}"
Placement="Bottom"
IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}"
PlacementTarget="{Binding ElementName=templateRoot}">
<Border x:Name="SubMenuBorder" Background="#202020" BorderBrush="#202020" BorderThickness="1" Padding="2">
<ScrollViewer x:Name="SubMenuScrollViewer"
Style="{DynamicResource {ComponentResourceKey ResourceId=MenuScrollViewer, TypeInTargetAssembly={x:Type FrameworkElement}}}">
<Grid RenderOptions.ClearTypeHint="Enabled">
<Canvas Height="0" Width="0" HorizontalAlignment="Left" VerticalAlignment="Top">
<Rectangle Name="OpaqueRect" Height="{Binding ElementName=SubMenuBorder, Path=ActualHeight}"
   Width="{Binding ElementName=SubMenuBorder, Path=ActualWidth}"
   Fill="{Binding ElementName=SubMenuBorder, Path=Background}"/>
</Canvas>
<!-- Icon separator color: -->
<Rectangle HorizontalAlignment="Left" Width="1" Margin="29,2,0,2" Fill="#202020"/>
<ItemsPresenter x:Name="ItemsPresenter" KeyboardNavigation.DirectionalNavigation="Cycle"
  KeyboardNavigation.TabNavigation="Cycle" Grid.IsSharedSizeScope="true"
  SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}"/>
</Grid>
</ScrollViewer>
</Border>
</Popup>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="MenuItem.IsSuspendingPopupAnimation" Value="true">
<Setter TargetName="PART_Popup" Property="Popup.PopupAnimation" Value="None"/>
</Trigger>
<Trigger Value="{x:Null}" Property="MenuItem.Icon">
<Setter TargetName="Icon" Property="UIElement.Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="MenuItem.IsChecked" Value="true">
<Setter TargetName="GlyphPanel" Property="UIElement.Visibility" Value="Visible"/>
<Setter TargetName="Icon" Property="UIElement.Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="MenuItem.IsHighlighted" Value="true">
<Setter TargetName="templateRoot" Value="#3D0767B8" Property="Border.Background"/>
<Setter TargetName="templateRoot" Value="#FF0767B8" Property="Border.BorderBrush"/>
</Trigger>
<Trigger Property="UIElement.IsEnabled" Value="false">
<Setter TargetName="templateRoot" Value="#FF707070" Property="TextElement.Foreground"/>
<Setter TargetName="GlyphPanel" Value="#FF707070" Property="Shape.Fill"/>
</Trigger>
<Trigger SourceName="SubMenuScrollViewer" Property="ScrollViewer.CanContentScroll" Value="false">
<Setter TargetName="OpaqueRect" Value="{Binding ElementName=SubMenuScrollViewer, Path=VerticalOffset}"
Property="Canvas.Top"/>
<Setter TargetName="OpaqueRect" Value="{Binding ElementName=SubMenuScrollViewer, Path=HorizontalOffset}"
Property="Canvas.Left"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>



</Application.Resources>

这当然是在编译时设置样式,但我需要在运行时动态地重复设置样式。

但是,我想让它使用户从设置菜单中的下拉菜单中选择一个主题,并且所有表单都显示新主题。最好是实时的,但"需要重启"。

实现这一目标的最佳方法是什么?从概念上讲,这几乎就像交换不同的CSS样式表。

关键是将所有相关资源引用为DynamicResource。这允许引擎在运行时监视和更新元素的资源。

要使其工作,您必须为每个主题属性定义一个资源,如边框厚度,字体大小或颜色/画笔。然后用定义为ComponentResourceKey的单个静态资源键注册每个资源键。在XAML资源中使用这个键,而不是通常的string字面值,例如

<SolidColorBrush x:Key="{x:Static ButtonBackgroundBrushKey}" />   

不是

<SolidColorBrush x:Key="ButtonBackgroundBrush" />. 

在其专用文件(ResourceDictionary)中定义每个主题。针对相同属性的不同主题的所有资源必须使用相同的静态键,以便您可以通过键交换实际资源。

下面的简单示例展示了两个主题。一个将使按钮呈现矩形和红色(默认主题),而另一个将使按钮呈现圆形和橙色。
点击按钮后,圆角主题将被加载以取代方形主题。
所有的资源文件都位于项目的根文件夹"Resources":

ThemingResources.cs
定义常量来代替string字面量,如资源文件名和资源键。
通过将资源键定义为静态字段,我们还允许控件的用户定义自己的资源,以便替换预定义的资源(控件自定义)。

public static class ThemingResources
{
public static readonly Uri SquaredThemeResourcesFileUri = new Uri("pack://application:,,,/MyProject;component/Resources/SquaredTheme.xaml");
public static readonly Uri RoundThemeResourcesFileUri = new Uri("pack://application:,,,/MyProject;component/Resources/RoundTheme.xaml");
public static ComponentResourceKey ButtonBackgroundBrushKey = new ComponentResourceKey(typeof(ThemingResources), "ButtonBackgroundBrushKey");
public static ComponentResourceKey ButtonBackgroundColorKey = new ComponentResourceKey(typeof(ThemingResources), "ButtonBackgroundColorKey");
public static ComponentResourceKey ButtonCornerRadiusKey = new ComponentResourceKey(typeof(ThemingResources), "ButtonCornerRadiusKey");
}

MainWindow.xaml

<!-- The button that switches to the round theme and which is actually themed -->
<Button Content="Switch To The Round Theme"
Click="LoadRoundTheme_OnClick" />

MainWindow.xaml.cs

private void LoadRoundTheme_OnClick(object sender, RoutedEventArgs e)
{
Collection<ResourceDictionary> mergedApplicationDictionaries = Application.Current.Resources.MergedDictionaries;
for (int resourceIndex = 0; resourceIndex < mergedApplicationDictionaries.Count; resourceIndex++)
{
ResourceDictionary resourceDictionary = mergedApplicationDictionaries[resourceIndex];
// Identify the current active theme file (resource dictionary)
if (resourceDictionary.Source == ThemingResources.SquaredThemeResourcesFileUri)
{
// Remove the current active theme file (resource dictionary)...
mergedApplicationDictionaries.RemoveAt(resourceIndex);
// ... to replace it with the one for the rounded theme
var roundThemeDictionary = new ResourceDictionary() { Source = ThemingResources.RoundThemeResourcesFileUri };
mergedApplicationDictionaries.Add(roundThemeDictionary);
return;
}
}
}

App.xaml
将所有主题资源定义为应用级资源(应用范围)。

<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MyProject;component/Resources/DefaultButtonStyle.xaml" />
<!-- Merge the default theme (the squared theme) -->
<ResourceDictionary Source="{x:Static resources:ThemingResources.SquaredThemeResourcesFileUri}" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

DefaultButtonStyle.xaml
Button的默认Style。它使用DynamicResourcestatic资源键引用所有与主题相关的资源。这实际上使Button能够被主题化。
如果您计划根据当前活动主题更改控件的ControlTemplate,您也可以将其引用为Dynamicresource,并将其定义为每个主题文件中的资源。

注意,Style不是主题的一部分。通过引用主题资源,实际上应用主题。

<ResourceDictionary xmlns:resources="clr-namespace:MyProject.Resources"> 
<Style TargetType="Button">
<Setter Property="Background"
Value="{DynamicResource {x:Static resources:ThemingResources.ButtonBackgroundBrushKey}}" />
<Setter Property="BorderBrush"
Value="{DynamicResource {x:Static resources:ThemingResources.ButtonBackgroundBrushKey}}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{DynamicResource {x:Static resources:ThemingResources.ButtonCornerRadiusKey}}">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

SquaredTheme.xaml

<ResourceDictionary xmlns:resources="clr-namespace:MyProject.Resources">
<Color x:Key="{x:Static resources:ThemingResources.ButtonBackgroundColorKey}">Red</Color>
<SolidColorBrush x:Key="{x:Static resources:ThemingResources.ButtonBackgroundBrushKey}"
Color="{DynamicResource {x:Static resources:ThemingResources.ButtonBackgroundColorKey}}" />
<CornerRadius x:Key="{x:Static resources:ThemingResources.ButtonCornerRadiusKey}">0</CornerRadius>  
</ResourceDictionary>

RoundTheme.xaml

<ResourceDictionary xmlns:resources="clr-namespace:MyProject.Resources">
<Color x:Key="{x:Static resources:ThemingResources.ButtonBackgroundColorKey}">Orange</Color>
<SolidColorBrush x:Key="{x:Static resources:ThemingResources.ButtonBackgroundBrushKey}"
Color="{DynamicResource {x:Static resources:ThemingResources.ButtonBackgroundColorKey}}" />
<CornerRadius x:Key="{x:Static resources:ThemingResources.ButtonCornerRadiusKey}">12</CornerRadius>
</ResourceDictionary>

相关内容

  • 没有找到相关文章

最新更新