适用于跨平台 Avalonia UI 框架的主题和本地化功能





5.00/5 (11投票s)
本文描述了一个简单灵活的、用于本地化跨平台 Avalonia 应用程序的新程序包,并提供了示例。
引言
请注意,本文和示例代码均已更新,以兼容最新版本的Avalonia - 11.0.6
本地化、国际化和主题化
多年前,我偶然发现了一个用于本地化/国际化 WPF 应用程序的出色程序包。我成功地使用它使我构建的一个 WPF 应用程序能够切换英语和德语版本。
这个 WPF 的程序包由 Tomer Shamam 开发,后来从他的旧博客中删除。他将代码发送给了我,并经他同意,我在 Github WPFLocalizationPackage 仓库中发布了它。
最近,我再次需要对一个应用程序进行国际化。起初,我想将那个 WPF 程序包移植到 Avalonia(WPF 的一个出色的跨平台开源后继者)。最终,我决定从头开始构建一个新的功能,借鉴了 Tomer 的 WPF 程序包的一些想法。
做出这个决定的主要原因是 Avalonia 的 DynamicResource
标记扩展和绑定比 WPF 的相应功能工作得更好,并且没有 WPF 伴随的那些怪癖。因此,我没有创建自定义的标记扩展,而是直接利用了 Avalonia 的 DynamicResource
。
我仍然使用了 Tomer 的想法,即创建一个包含不同本地化字典的 C# 对象,这些字典可以轻松地在不同的区域设置之间切换。同样重要的是,我创建了一个演示应用程序,它的外观与他的演示非常相似,以确保我的实现涵盖了他所有的功能,甚至更多。
这两个程序包——Tomer 的原始程序包和我的 Avalonia 程序包——都可以用来更改任何控件上的任何依赖项(或 Avalonia)属性,而不仅仅是与本地化相关的属性。因此,相同的功能可用于主题化或皮肤化——完全改变应用程序的外观。
我的本地化/国际化程序包的优点是:
- 易于使用——正如示例将演示的那样。
- 动态性——其他一些解决方案,包括大公司的解决方案,需要为每个区域设置进行不同的编译,而我的程序包(以及 Tomer 的 WPF 程序包)允许在应用程序运行时切换区域设置。
- 允许创建多个主题/本地化字典集,每个字典集只控制所需自定义的一个“坐标”。几个示例将展示如何动态切换多种语言和多种颜色主题——因此,任何语言和颜色主题的组合都是可能的。
运行高级演示
为了演示新的主题和 L10N(本地化)程序包的功能,我将从一个高级示例开始。此时请不要关注代码(稍后会进行解释)。请从 NP.Demos.ThemingAndLocalizationDemo 下载并运行演示。
您将看到以下内容:
右上角的两个 ComboBox
允许选择语言(“英语”、“希伯来语”或“俄语”)和颜色主题(“深色”或“浅色”)。上面的图片显示了应用程序处于英语语言和深色主题下。
这是应用程序在希伯来语语言和浅色主题下的视图(请注意文本和控件流的变化——从右到左而不是从左到右)
这是俄语/深色组合
什么是 Avalonia
Avalonia 是一个出色的跨平台开源 UI 框架,用于开发
- 可在Windows、Mac和Linux上运行的桌面解决方案
- 在浏览器中运行的Web应用程序(通过WebAssembly)
- 适用于Android、iOS和Tizen的移动应用程序。
Avalonia 的功能与 WPF 类似,但 Avalonia 已经比 WPF 更强大,而且 Bug 更少,并且是跨平台的。
Avalonia 比 UNO Platform 强大得多,也更干净、更快——它是跨平台 XAML/C# 世界中唯一的竞争对手。
可以在我的 codeproject.com 文章中找到 Avalonia 教程和有关此精彩程序包的更多信息
- 使用 AvaloniaUI 在简易示例中进行多平台 UI 编码。第一部分 - AvaloniaUI 构建块
- 多平台 Avalonia .NET 框架 XAML 基础知识轻松示例
- 使用简单的示例学习 Avalonia .NET 框架跨平台编程基础概念
- Avalonia .NET Framework 编程高级概念的简易示例
另外,不要错过我最近发布的强大 Avalonia 跨平台 UI 停靠程序包 UniDock,它在 UniDock - A New Multiplatform UI Docking Framework. UniDock Power Features. 中有介绍。
Avalonia 11 相关更改
Avalonia 11 中的主题化发生了重大变化。我提供了几个示例来解释下面内置的 Avania 主题化功能。
在多个平台上运行示例
本文中的所有示例均已在 Windows 10、Mac Catalina 和 Ubuntu 20.04 上进行了测试。
主题/本地化代码位置
新的主题/L10N 功能是 NP.Ava.Visuals 开源程序包的一部分。该程序包还可以有其他用途,我计划撰写另一篇文章来解释其最重要的功能。
还有一个名为 NP.ViewModelInterfaces 的项目,其中包含由某些 Visual 对象实现的非视觉接口。此代码的目的是在非视觉(例如,ViewModel)项目中使用,而无需任何对 Avalonia 代码的引用,以控制和查询某些视觉对象。到目前为止,NP.ViewModelInterfaces
只包含与主题化和本地化功能相关的接口:IThemeLoader.cs 和 ThemeInfo.cs
NuGet 包位置
NuGet 包可在 nuget.org 上的 NP.Ava.Visuals 中找到。它依赖于其他几个会自动安装的包。
您不需要单独引用 Avalonia 包,因为它们将通过安装 NP.Ava.Visuals
来安装。
如果您想从非视觉(ViewModel)项目控制主题/L10N 功能,您可以只安装 NP.ViewModelInterfaces,它也可以在 nuget.org 上找到。
主题/本地化代码示例
示例代码位置
所有主题/L10N 演示代码都可以在 NP.Demos.ThemingAndL10N 中找到。
代码是使用 Visual Studio 2022 构建和测试的。
内置主题示例
此示例项目位于 NP.Demos.BuiltInThemingSample 文件夹内。
在 VS2022 中打开解决方案,编译并运行项目。您将看到以下内容:
请注意,前景是蓝色。按粉色主题按钮——您将看到以下内容:
我们的粉色主题继承自浅色主题,因此背景被覆盖为粉色,但前景保持蓝色。
最后点击深色主题按钮
背景和前景都会改变。
现在,让我们看看代码。大部分代码位于 MainWindow.axaml XAML 文件中。
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:NP.Demos.BuiltInThemingSample"
x:Class="NP.Demos.BuiltInThemingSample.MainWindow"
Title="NP.Demos.BuiltInThemingSample"
Background="{DynamicResource ThemeBackgroundBrush}"
Foreground="{DynamicResource ThemeForegroundBrush}"
Width="300"
Height="200">
<!-- Default Avalonia Styles -->
<Window.Styles>
<SimpleTheme/>
</Window.Styles>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="ThemeForegroundBrush"
Color="Blue"/>
</ResourceDictionary>
<ResourceDictionary x:Key="{x:Static local:CustomThemes.Pink}">
<SolidColorBrush x:Key="ThemeBackgroundBrush"
Color="LightPink"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid RowDefinitions="Auto, *"
Margin="10">
<StackPanel Orientation="Horizontal">
<!-- this button switches to light theme -->
<Button x:Name="LightButton"
Content="Light Theme"
Margin="0,0,10,0"/>
<!-- this button switches to dark theme -->
<Button x:Name="DarkButton"
Content="Dark Theme"
Margin="0,0,10,0"/>
<!-- this button switches to pink theme -->
<Button x:Name="PinkButton"
Content="Pink Theme"
Margin="0,0,0,0"/>
</StackPanel>
<TextBlock Text="Hello World from Avalonia !!!"
Grid.Row="1"
VerticalAlignment="Center"
HorizontalAlignment="Center"/>
</Grid>
</Window>
现在,我将从文件顶部开始解释。第一个有趣的部分是默认主题设置:
<!-- Default Avalonia Styles -->
<Window.Styles>
<SimpleTheme/>
</Window.Styles>
我们将默认样式设置为由 Avalonia 的内置 SimpleTheme
定义,它有两个 ThemeVariant
:Light
(也称为 Default)和 Dark
。这两个 ThemeVariant
都在 ThemeVariant
Avalonia 类中定义为 static
属性。
public sealed record ThemeVariant
{
...
/// <summary>
/// Use the Light theme variant.
/// </summary>
public static ThemeVariant Light { get; } = new(nameof(Light));
/// <summary>
/// Use the Dark theme variant.
/// </summary>
public static ThemeVariant Dark { get; } = new(nameof(Dark));
...
}
在 XAML 中,可以通过使用 x:Static
引用 C# 代码中的定义来引用 ThemeVariant
。同样,通过使用它们的名称(因为每个 ThemeVariant
都应该有一个唯一的名称)来引用 Avalonia 自带的 ThemeVariant
。
然后(回到 MainWindow.xaml 文件),我们将 Windows.Resources
部分设置为一个包含 ThemeDictionaries
的 ResourceDictionary
。
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="ThemeForegroundBrush"
Color="Blue"/>
</ResourceDictionary>
<ResourceDictionary x:Key="{x:Static local:CustomThemes.Pink}">
<SolidColorBrush x:Key="ThemeBackgroundBrush"
Color="LightPink"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Window.Resources>
ThemeDictionaries
中的第一个字典通过将 ThemeForegroundBrush
设置为 Blue
(而不是 Black
)来修改浅色 ThemeVariant
。
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="ThemeForegroundBrush"
Color="Blue"/>
</ResourceDictionary>
它知道它需要修改哪个 ThemeVariant
,这是因为在 ResourceDictionary
上设置了 x:Key="Light"
参数。请记住:每个 ThemeVariant
都有一个唯一的名称,由于 Light ThemeVariant
来自 Avalonia,Avalonia 可以通过设置为 ThemeVariant
名称的 x:Key
来确定要修改哪个 ThemeVariant
。
第二个 ResourceDictionary
:
<ResourceDictionary x:Key="{x:Static local:CustomThemes.Pink}">
<SolidColorBrush x:Key="ThemeBackgroundBrush"
Color="LightPink"/>
</ResourceDictionary>
将画笔与示例项目中 CustomThemes
静态类中定义的 Pink ThemeVariant
相关联。
public static class CustomThemes
{
public static ThemeVariant Pink { get; } = new ThemeVariant("Pink", ThemeVariant.Light);
}
请注意,Pink ThemeVariant
继承自 ThemeVariant.Light
(其构造函数的第二个参数)。因此,Pink ThemeVariant
获得来自 Light ThemeVariant
的所有默认画笔,而只有一个画笔被覆盖——ThemeBackgroundBrush
被设置为 LightPink
颜色。
请注意,Pink ResourceDictionary
的 x:Key
使用 x:Static
标记扩展设置——因为它是一个自定义 ThemeVariant
,我们不能将其名称用作 ResourceDictionary
的 x:Key
参数。
新的主题化功能有什么作用?
新的主题化功能作为 NP.Ava.Visuals 包的一部分,可从 nuget 获取。源代码位于 NP.Ava.Visuals github 仓库中。
ThemeVariant
无疑极大地改进了 Avalonia 的主题化功能,但仍然不足以创建多个独立的 thene 范围——每个范围负责某个功能。
例如,想象一下您想创建多个 Color
主题(例如,上面提到的 Light、Dark 和 Pink),然后是两个 CornerRadius
主题——一个用于使按钮和其他控件的 CornerRadius 为 0,我们称之为 NoCornerRadius
主题——另一个用于使所有控件的 CornerRadius
为 3 ——CornerRadius3
主题。
现在我们想拥有 Color
主题和 CornerRadius
主题的所有可能组合——例如,我们想让用户选择带有 CornerRadius3
的粉色主题。实际上,Color
和 CornerRadius
主题提供了两个独立的主题变化坐标。我们还可以想到 3 个或更多独立坐标的例子,例如 Color
、CornerRadius
和 Locale
。
由于 Avalonia 内置的 ThemeVariant
仅支持单继承,因此无法用于此目的——需要重复代码才能创建沿多个坐标的各种主题组合。
然而,我的 NP.Ava.Visuals
包中的 ThemeLoader
功能解决了这个问题,如下面的示例所示。
简易主题示例
此示例位于 NP.Demos.SimpleThemingSample 下。它仅通过使用 NP.Ava.Visuals
功能来表示单坐标(颜色)主题更改。
下载、编译并运行。您将看到以下内容:
按顶部的深色主题按钮。您会看到背景变为黑色,而文本颜色(前景)变为白色。
只有顶部的两个按钮没有改变。
查看主项目中的文件。
ColorThemes 文件夹下有两个 XAML 文件,名为 DarkResources.axaml 和 LightResources.axaml。让我们来看看它们。这是 DarkResources.axaml 的内容:
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Defing Background and Foreground brushes for dark theme -->
<SolidColorBrush x:Key="BackgroundBrush"
Color="Black"/>
<SolidColorBrush x:Key="ForegroundBrush"
Color="White"/>
</ResourceDictionary>
这是 LightResources.axaml:
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Defing Background and Foreground brushes for light theme -->
<SolidColorBrush x:Key="BackgroundBrush"
Color="White"/>
<SolidColorBrush x:Key="ForegroundBrush"
Color="Black"/>
</ResourceDictionary>
它们包含具有相同键和相反颜色的 Avalonia 资源——在 DarkResources.axaml 中,BackgroundBrush
是 'Black
',而 ForegroundBrush
是白色——而在 LightResources.axaml 中——正好相反。
在通常的主解决方案文件之外,只有 App.axaml、MainWindow.axaml 和 MainWindow.axaml.cs 有一些非平凡的更改。
查看 App.axaml 的内容:
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:np="https://np.com/visuals"
x:Class="NP.Demos.SimpleThemingSample.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- define the Theme loader with two themes - Dark and Light -->
<np:ThemeLoader Name="ColorThemeLoader"
SelectedThemeId="Light"> <!-- Set original theme to Light -->
<np:ThemeInfo Id="Dark"
ResourceUrl="/ColorThemes/DarkResources.axaml"/>
<np:ThemeInfo Id="Light"
ResourceUrl="/ColorThemes/LightResources.axaml"/>
</np:ThemeLoader>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
<!-- Default Avalonia Styles -->
<Application.Styles>
<StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseDark.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
</Application.Styles>
</Application>
实现 ThemeLoader
的重要代码包含在 ResourceDictionary.MergeDictionaries
标签内。
<!-- define the Theme loader with two themes - Dark and Light -->
<np:ThemeLoader Name="ColorThemeLoader"
SelectedThemeId="Light"> <!-- Set original theme to Light -->
<np:ThemeInfo Id="Dark"
ResourceUrl="/ColorThemes/DarkResources.axaml"/>
<np:ThemeInfo Id="Light"
ResourceUrl="/ColorThemes/LightResources.axaml"/>
</np:ThemeLoader>
ThemeLoader
本质上是一个智能 ResourceDictionary
,可以交换其内容。
我们的 ThemeLoader
通过使用 ThemeInfo
对象定义了两个主题。第一个 ThemeInfo
对象指定了深色主题——它的 Id
是 "Dark"
,它的 ResourceUrl="/ColorThemes/DarkResources.axaml"
设置为指向上面描述的 DarkResources.axaml 文件。第二个 ThemeInfo
对象通过将其 ResourceUrl
指向 LightResource.axaml 文件来指定浅色主题。它的 Id
是 "Light"
。
通过更改其 SelectedThemeId
,主题加载器可以交换其资源内容。最初,它设置为 "Light"
,这是第二个 ThemeInfo
对象的 Id
,因此它在应用程序启动时加载,我们得到了在浅色主题下运行的应用程序。
我们将 ThemeLoader
的 Name
属性设置为 "ColorThemeLoader
"。通过这个名称,我们将在 MainWindow.axaml.cs 代码隐藏中找到这个加载器。
按下“深色主题”按钮将导致代码隐藏(在 MainWindow.axaml.cs 文件中)将我们的 ThemeLoader
的 SelectedThemeId
更改为“Dark
”,以便应用程序更改其颜色。
MainWindow.axaml 文件非常简单:
<Window ...
Background="{DynamicResource BackgroundBrush}"
Width="300"
Height="200">
<Grid RowDefinitions="Auto, *"
Margin="10">
<StackPanel Orientation="Horizontal">
<!-- this button switches to light theme -->
<Button x:Name="LightButton"
Content="Light Theme"
Margin="0,0,10,0"/>
<!-- this button switches to dark theme -->
<Button x:Name="DarkButton"
Content="Dark Theme"
Margin="0,0,0,0"/>
</StackPanel>
<TextBlock Text="Hello World from Avalonia !!!"
Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="20"
Foreground="{DynamicResource ForegroundBrush}"/>
</Grid>
</Window>
MainWindow
的 Background
属性使用 DynamicResource
标记扩展连接到 BackgroundBrush
资源(在深色主题下指向黑色,在浅色主题下指向白色):Background="{DynamicResource BackgroundBrush}"
。
TextBlock
的 Foreground
使用相同的方法获取 ForegroundBrush
资源的值:Foreground="{DynamicResource ForegroundBrush}"
。
以下是 MainWindow.axaml.cs 文件内容的关键点:
public partial class MainWindow : Window
{
// reference to ThemeLoader object defined
// in XAML
private ThemeLoader _themeLoader;
public MainWindow()
{
InitializeComponent();
...
// find the theme loader by its name
_themeLoader =
Application.Current.Resources.GetThemeLoader("ColorThemeLoader")!;
// set the handler for lightButton's click event
Button lightButton = this.FindControl<Button>("LightButton");
lightButton.Click += LightButton_Click;
// set the handler for darkButton's click event
Button darkButton = this.FindControl<Button>("DarkButton");
darkButton.Click += DarkButton_Click;
}
private void LightButton_Click(object? sender,
global::Avalonia.Interactivity.RoutedEventArgs e)
{
// set the theme to Light
_themeLoader.SelectedThemeId = "Light";
}
private void DarkButton_Click(object? sender,
global::Avalonia.Interactivity.RoutedEventArgs e)
{
// set the theme to Dark
_themeLoader.SelectedThemeId = "Dark";
}
...
}
首先,我们通过将名称传递给 GetThemeLoader
扩展方法来获取 XAML 中定义的 ThemeLoader
对象。
// find the theme loader by its name
_themeLoader =
Application.Current.Resources.GetThemeLoader("ColorThemeLoader")!;
接下来,我们获取 XAML 中定义的 LightButton
和 DarkButton
的引用,并为它们的 Click
事件设置处理程序。
// set the handler for lightButton's click event
Button lightButton = this.FindControl<Button>("LightButton");
lightButton.Click += LightButton_Click;
// set the handler for darkButton's click event
Button darkButton = this.FindControl<Button>("DarkButton");
darkButton.Click += DarkButton_Click;
在每个处理程序中,我们相应地将 SelectedThemeId
设置为字符串 "Light"
或 "Dark"
,分别对应浅色和深色按钮。
private void LightButton_Click(object? sender, RoutedEventArgs e)
{
// set the theme to Light
_themeLoader.SelectedThemeId = "Light";
}
private void DarkButton_Click(object? sender, RoutedEventArgs e)
{
// set the theme to Dark
_themeLoader.SelectedThemeId = "Dark";
}
带样式更改的简易主题
下一个示例演示了如何沿单个坐标(颜色)更改样式和资源。它还展示了如何利用内置的 ThemeVariant
来获取其中定义的资源。
上一个示例显示了如何更改背景和文本颜色。然而,顶部的浅色主题和深色主题按钮没有改变——它们仍然是浅色,因为 Light 是默认的 ThemeVariant
。
此演示的代码可在 NP.Demos.SimpleThemingSampleWithStyleChange 中找到。
编译并运行演示——您将看到以下内容:
请注意,按钮是浅色的。然后按深色主题按钮——主题改变,按钮也随之改变。
注意与上一个示例的两个区别:
- 顶部的按钮改变了颜色。这些按钮不受我们 LightResources.axaml 和 DarkResources.axaml 文件中定义的资源影响,但受到
ThemeVariant
更改的影响。 - 窗口中央的深色主题按钮具有圆角——这实际上来自于
Styles
的更改——浅色和深色样式分别定义在ColorThemes
项目文件夹中的 LightStyles.axaml 文件和 DarkStyles.axaml 文件中。
请注意,在此示例中,我们仍然沿单个坐标更改主题,因为我们不能将所有深色和浅色主题与 NoCornerRadious
和 CornerRadius3
选项混合,而是将浅色主题硬编码为始终不带 CornerRadious
,而深色主题——具有圆角半径 3。
与上一个示例相比,主要的代码更改(位于 App.axaml 文件中)并以粗体显示。
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:np="https://np.com/visuals"
x:Class="NP.Demos.SimpleThemingSample.App"
np:ThemeVariantBehavior.ThemeReference=
"{DynamicResource BuiltInThemeReference}">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- define the Theme loader with two themes - Dark and Light -->
<np:ThemeLoader Name="ColorThemeLoader"
SelectedThemeId="Light"
StyleResourceName="ColorLoaderStyles">
<!-- to refer to the style by StyleReference-->
<np:ThemeInfo Id="Dark"
ResourceUrl="/ColorThemes/DarkResources.axaml"
StyleUrl="/ColorThemes/DarkStyles.axaml"/>
<!-- refers to dark styles -->
<np:ThemeInfo Id="Light"
ResourceUrl="/ColorThemes/LightResources.axaml"
StyleUrl="/ColorThemes/LightStyles.axaml"/>
<!-- refers to light styles -->
</np:ThemeLoader>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
<Application.Styles>
<!-- reference to the theme dependent style defined within the ThemeLoader-->
<np:StyleReference TheStyle="{StaticResource ColorLoaderStyles}"/>
<!-- Default Avalonia Style -->
<SimpleTheme/>
</Application.Styles>
</Application>
行 np:ThemeVariantBehavior.ThemeReference="{DynamicResource BuiltInThemeReference}"
负责使用预定义的 ThemeVariant
。它将当前 LightResource.axaml 或 DarkResource.axaml 文件中定义的 ThemeVariant
的引用应用于整个应用程序。
LightResources.axaml 文件包含以下额外 XAML 代码:
<np:ThemeVariantReference x:Key="BuiltInThemeReference"
TheThemeVariant="{x:Static ThemeVariant.Light}"/>
以及 DarkResources.axaml 文件:
<np:ThemeVariantReference x:Key="BuiltInThemeReference"
TheThemeVariant="{x:Static ThemeVariant.Dark}"/>
现在我将解释如何切换按钮 Styles
。
请注意,ColorThemes 文件夹下有两个新文件——DarkStyles.axml 和 LightStyles.axaml。它们都只包含一个 Style
选择器——Selector="Button.MyButton"
——的样式定义。这是 LightStyles.axaml 的内容,将 CornerRadious
设置为 0
:
<!-- Add Styles Here -->
<Style Selector="Button.MyButton">
<Setter Property="CornerRadius"
Value="0"/>
</Style>
这是 DarkStyles.axaml 的内容,将 CornerRadius
设置为 3
:
<!-- Add Styles Here -->
<Style Selector="Button.MyButton">
<Setter Property="CornerRadius"
Value="3"/>
</Style>
现在,在 App.axaml 文件中,我们通过设置 ThemeInfo.StyleUrl
到相应的文件 URL 来引用这些文件:
<np:ThemeInfo Id="Dark"
ResourceUrl="/ColorThemes/DarkResources.axaml"
StyleUrl="/ColorThemes/DarkStyles.axaml"/> <!-- refers to dark styles -->
<np:ThemeInfo Id="Light"
ResourceUrl="/ColorThemes/LightResources.axaml"
StyleUrl="/ColorThemes/LightStyles.axaml"/> <!-- refers to light styles -->
请注意,每个 ThemeInfo
对象都在 Id
和 ResourceUrl
属性(在上一个示例中已解释)的顶部定义了 StyleUrl
。
<np:ThemeInfo Id="Dark"
ResourceUrl="/ColorThemes/DarkResources.axaml"
StyleUrl="avares://Avalonia.Themes.Default/Accents/BaseDark.xaml"/>
<!-- refers to dark styles -->
<np:ThemeInfo Id="Light"
ResourceUrl="/ColorThemes/LightResources.axaml"
StyleUrl="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
<!-- refers to light styles -->
ThemeLoader
现在定义了一个属性 StyleResourceName
:StyleResourceName="ColorLoaderStyles">
。此属性值应选择为不与 ThemeLoader
中包含的资源的任何资源键冲突。StyleReference
对象使用此值来引用由我们的 ThemeLoader
选择的主题相关的 Style
。
<Application.Styles>
<!-- reference to the styles defined within the ThemeLoader-->
<np:StyleReference TheStyle="{StaticResource ColorLoaderStyles}"/>
...
</Application.Styles>
更改主题和语言示例
现在我们准备处理跨多个坐标的更改——一个坐标是 Color
,另一个是 Language
。
本示例的目的是演示一个应用程序,该应用程序支持独立更改其颜色主题和语言,以便支持颜色主题和语言的每种组合。
此演示的代码可在以下 URL 找到:NP.Demos.SimpleThemingAndL10NSample。
下载、编译并运行示例后,您将看到以下内容:
按深色主题按钮将颜色主题更改为深色。
按希伯来语按钮将文本更改为希伯来语。
按浅色主题按钮将颜色主题再次更改为浅色。
查看演示应用程序中的项目文件。
此示例有两组字典:
- ColorThemes 文件夹下的 DarkResources.axaml 和 LightResources.axaml。
- LanguageDictionaries 文件夹下的 EnglishDictionary.axaml 和 HebrewDictionary.axaml。
颜色主题文件——DarkResources.axaml 和 LightResources.axaml 与之前的示例完全相同。
查看 EnglishDictionary.axaml。
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<x:String x:Key="WindowTitle">Theming Demo</x:String>
<x:String x:Key="WelcomeText">Hello World from Avalonia !!!</x:String>
<x:String x:Key="WindowTitleText">Window Title is '{0}'</x:String>
</ResourceDictionary>
这是一个非常简单的字典文件,它定义了三个 string
资源——WindowTitle
、WelcomeText
和 WindowTitleText
。
HebrewDictionary.axaml 定义了相同的资源,但其值被翻译成希伯来语。
WindowTitle
控制 Window
的标题,WelcomeText
是显示在窗口内的文本,WindowTitleText
也显示在窗口内作为第二行。我添加它以显示文本的动态替换,使用 Avalonia 绑定。请注意,WindowTitleText
的文本 "Window Title is '{0}'
" 中包含 {0}
部分,它将被窗口标题的任何内容替换。这将在下面进行解释。
打开 App.axaml 文件。它定义了两个 ThemeLoaders
——一个用于颜色主题,另一个用于语言。
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- define the Theme loader with two themes - Dark and Light -->
<np:ThemeLoader Name="ColorThemeLoader"
SelectedThemeId="Light"
StyleResourceName="ColorLoaderStyles">
<!-- to refer to the style by StyleReference-->
<np:ThemeInfo Id="Dark"
ResourceUrl="/ColorThemes/DarkResources.axaml"
StyleUrl="avares://Avalonia.Themes.Default/Accents/BaseDark.xaml"/>
<!-- refers to dark styles -->
<np:ThemeInfo Id="Light"
ResourceUrl="/ColorThemes/LightResources.axaml"
StyleUrl="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
<!-- refers to light styles -->
</np:ThemeLoader>
<np:ThemeLoader Name="LanguageLoader"
SelectedThemeId="English">
<!-- to refer to the style by StyleReference-->
<np:ThemeInfo Id="English"
ResourceUrl="/LanguageDictionaries/EnglishDictionary.axaml"/>
<!-- refers to dark styles -->
<np:ThemeInfo Id="Hebrew"
ResourceUrl="/LanguageDictionaries/HebrewDictionary.axaml"/>
<!-- refers to light styles -->
</np:ThemeLoader>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
ColorThemeLoader
默认选择“Light
”,LanguageLoader
选择“English
”(SelectedThemeId="English"
)。
现在打开 MainWindow.axaml 文件。
<Window ...
Title="{DynamicResource WindowTitle}"
Background="{DynamicResource BackgroundBrush}"
...
>
<Grid RowDefinitions="Auto, *"
Margin="10">
<StackPanel Orientation="Horizontal">
<!-- this button switches to light theme -->
<Button x:Name="LightButton"
Content="Light Theme"
Margin="0,0,10,0"/>
<!-- this button switches to dark theme -->
<Button x:Name="DarkButton"
Content="Dark Theme"
Margin="0,0,10,0"/>
<!-- this button switches to English language-->
<Button x:Name="EnglishButton"
Content="English"
Margin="0,0,10,0"/>
<!-- this button switches to Hebrew language-->
<Button x:Name="HebrewButton"
Content="Hebrew"
Margin="0,0,10,0"/>
</StackPanel>
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Grid.Row="1">
<TextBlock Text="{DynamicResource WelcomeText}"
...
Foreground="{DynamicResource ForegroundBrush}"
Margin="0,0,0,10"/>
<TextBlock HorizontalAlignment="Center"
...
Foreground="{DynamicResource ForegroundBrush}"
Margin="0,0,0,10">
<TextBlock.Text> <!-- Use multibinding to format the string -->
<MultiBinding Converter="{x:Static np:StringFormatConverter.Instance}">
<!-- pass the main string from a language dictionary -->
<DynamicResourceExtension ResourceKey="WindowTitleText"/>
<!-- pass window title as a string parameter -->
<Binding Path="Title"
RelativeSource="{RelativeSource AncestorType=Window}"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</Grid>
</Window>
Window
标签将其 Title
设置为 WindowTitle
,将其 Background
设置为来自主题/本地化字典的 BackgroundBrush
。
Title="{DynamicResource WindowTitle}"
Background="{DynamicResource BackgroundBrush}"
顶部定义了四个按钮:两个用于深色和浅色主题,两个用于英语和希伯来语。
TextBlock
将它们的 Text
和 Foreground
属性动态设置为语言和颜色字典中定义的资源,例如:
Text="{DynamicResource WelcomeText}"
Foreground="{DynamicResource ForegroundBrush}"
第二个 TextBlock
有一种智能的方式来绑定其文本,使用 MultiBinding
——将单个目标绑定到多个源。这是为了扩展 string
,自动插入 Window
的 Title
属性值。
<TextBlock.Text> <!-- Use multibinding to format the string -->
<MultiBinding Converter="{x:Static np:StringFormatConverter.Instance}">
<!-- pass the main string from a language dictionary -->
<DynamicResourceExtension ResourceKey="WindowTitleText"/>
<!-- pass window title as a string parameter -->
<Binding Path="Title"
RelativeSource="{RelativeSource AncestorType=Window}"/>
</MultiBinding>
</TextBlock.Text>
我们使用 NP.Avalonia.Visuals
包中定义的 StringFormatConverter
。它将第一个 string
作为格式,其余作为参数,并对它们调用 string.Format(string format, params object[] args)
方法。多重绑定中的第一个绑定由 DynamicResourceExtension
提供(是的,在 Avalonia 中——DynamicResource
只是一个绑定,因此它可以作为其 Binding
子项之一插入到 MultiBinding
中)。此绑定返回格式 string
(对于英语,将是 "Window Title is '{0}'"
)。
第二个绑定返回要插入到第一个 string
中的窗口标题(代替“{0}
”)。
现在看看 MainWindow.axaml.cs 文件,它与上一个示例的同名文件非常相似,只是在这里,我们定义了两个 ThemeLoader
对象(_colorThemeLoader
和 _languageThemeLoader
),并将处理程序分配给 4 个按钮而不是 2 个按钮的 Click
事件。
public partial class MainWindow : Window
{
private ThemeLoader _colorThemeLoader;
private ThemeLoader _languageThemeLoader;
public MainWindow()
{
InitializeComponent();
...
// find the color theme loader by name
_colorThemeLoader =
Application.Current.Resources.GetThemeLoader("ColorThemeLoader")!;
// find the language theme loader by name
_languageThemeLoader =
Application.Current.Resources.GetThemeLoader("LanguageLoader")!;
Button lightButton = this.FindControl<Button>("LightButton");
lightButton.Click += LightButton_Click;
Button darkButton = this.FindControl<Button>("DarkButton");
darkButton.Click += DarkButton_Click;
Button englishButton = this.FindControl<Button>("EnglishButton");
englishButton.Click += EnglishButton_Click;
Button hebrewButton = this.FindControl<Button>("HebrewButton");
hebrewButton.Click += HebrewButton_Click;
}
private void LightButton_Click(object? sender, RoutedEventArgs e)
{
// set the theme to Light
_colorThemeLoader.SelectedThemeId = "Light";
}
private void DarkButton_Click(object? sender, RoutedEventArgs e)
{
// set the theme to Dark
_colorThemeLoader.SelectedThemeId = "Dark";
}
private void EnglishButton_Click(object? sender, RoutedEventArgs e)
{
// set language to English
_languageThemeLoader.SelectedThemeId = "English";
}
private void HebrewButton_Click(object? sender, RoutedEventArgs e)
{
// set language to Hebrew
_languageThemeLoader.SelectedThemeId = "Hebrew";
}
...
}
高级演示的代码
我们在本文的介绍部分已经展示了高级演示,所以在这里,我们将只讨论代码(位于 NP.Demos.ThemingAndLocalizationDemo)。
从概念上讲,高级演示的代码在上面讨论的简单示例之上并没有太多新内容。许多属性都进行了本地化,包括窗口和控件的大小、应用程序的流程(希伯来语是从右到左书写和查看的)、水平对齐方式等。此外,还在 LanguageDictionaries
中添加了 RussianResources.axaml 文件。
此演示中使用的一个功能值得特别讨论。该演示使用 DynamicResourceBinding
对象,例如:
<TextBlock.Text>
<MultiBinding Converter="{x:Static np:StringFormatConverter.Instance}">
<np:DynamicResourceBinding Path="Uid"/>
<Binding Path="ID"/>
</MultiBinding>
</TextBlock.Text>
DynamicResourceBinding
结合了 Binding
和 DynamicResource
的功能。它绑定到一个 string
或对象,该对象随后用作 DynamicResource
的资源键。当提供资源键的属性或资源键指向的动态资源更改其值时,DynamicResourceBinding
的返回值将发生变化。
我创建了 DynamicResourceBinding
以匹配 Tomer 的自定义标记扩展提供的一些功能,但它对于自定义应用程序也非常有用。
我将在未来专门介绍 NP.Avalonia.Visuals
包功能的文章中详细介绍 DynamicResourceBinding
。
结论
本文解释并提供了我在跨平台桌面应用程序中构建和使用的基于 Avalonia 的主题化/本地化功能的详细示例。
这项功能的灵感来自于 Tomer Shamam 编写的旧 WPF 程序包。
此功能作为 NP.Avalonia.Visuals
nuget 包和 Github 仓库的一部分发布,采用最简单、最宽松的 MIT 许可证——这基本上意味着您可以在任何应用程序中使用它,无论商业与否,只要您不将作者(我)归咎于可能存在的错误,并提供简短的归属。
本文描述的所有示例均在 Windows 10、Mac Catalina 和 Ubuntu 20.04 机器上进行了测试。
历史
- 2021 年 11 月 18 日:初始版本
- 2023 年 12 月 26 日:升级以支持 Avalonia 11