Bootstrap

跨平台WPF框架Avalonia教程 九

样式选择器

Avalonia UI 使用自定义的 XAML 语法来匹配控件,采用样式选择器。

信息

如果您熟悉 CSS(层叠样式表)技术,您会发现这个语法非常相似。

以下是一些样式选择器的示例列表:

样式选择器描述
Button选择所有 Button(类)控件。
Button.red选择所有带有 red 样式类的 Button 控件。
Button.red.large选择所有同时带有 red 和 large 样式类的 Button 控件。
Button:focus选择所有带有 :focus 伪类激活的 Button 控件。
Button.red:focus选择所有同时带有 red 样式类和 :focus 伪类激活的 Button 控件。
Button#myButton选择 Name(属性)为 "myButton" 的 Button 控件。
StackPanel Button.xl选择所有带有 xl 样式类的 Button(类)控件,同时它们是 StackPanel(类)控件的后代,可以位于任何级别。
StackPanel > Button.xl选择所有带有 xl 样式类的 Button(类)控件,同时它们是 StackPanel(类)控件的直接后代。
Button /template/ ContentPresenter选择所有在 Button(类)控件的模板内的 ContentPresenter(类)控件。

属性设置器

样式中的设置器定义了在 Avalonia UI 中通过选择器匹配控件并确定使用哪种样式后,将更改哪些属性。设置器是在XAML中的简单属性和值对,格式如下:

<Setter Property="propertyName" Value="newValueString"/>

例如:

<Setter Property="FontSize" Value="24"/>
<Setter Property="Padding" Value="4 2 0 4"/>

您也可以使用长格式语法将控件属性设置为具有多个设置的对象,如下所示:

<Setter Property="MyProperty">
   <MyObject Property1="My Value" Property2="999"/>
</Setter>

样式还可以使用绑定来设置属性。在常规选择过程之后,这将使 Avalonia UI 使用目标控件的数据上下文中的值。例如,可以这样定义设置器:

<Setter Property="FontSize" Value="{Binding SelectedFontSize}"/>

样式优先级

当选择器匹配多个样式时,有两个规则来决定哪个属性设置器具有优先权:

  • 应用程序中包含样式集合的位置 - closest 优先级高。
  • 样式在定位的样式集合中的位置 - latest 优先级高。

例如,首先意味着在窗口级别定义的样式将覆盖在应用程序级别定义的样式。其次,这意味着如果所选样式集位于同一级别,则后面的定义(按照文件中的书写顺序)优先。

注意

如果将样式类比为CSS,请注意:与CSS不同,在 Avalonia UI 中,Classes 属性中类名的列表顺序对设置器优先级没有影响。也就是说,如果这两个样式类都设置了颜色,那么这两种类名的列举方式结果是相同的:

<Button Classes="h1 blue"/>
<Button Classes="blue h1"/>

值还原

当样式与控件匹配时,所有设置器都将应用于控件。如果样式选择器导致样式不再与控件匹配,属性值将恢复为下一个优先级更高的值。

可变值

请注意,Setter 创建了一个将应用于所有与样式匹配的控件的 Value 的单个实例:如果对象是可变的,则更改将反映在所有控件上。

还要注意,在设置器值中定义的对象上的绑定将无法访问目标控件的数据上下文。这是因为可能有多个目标控件。这种情况可能在像这样定义的样式中出现:

<Style Selector="local|MyControl">
  <Setter Property="MyProperty">
     <MyObject Property1="{Binding MyViewModelProperty}"/>
  </Setter>
</Style>

这意味着在上面的示例中,设置器的绑定源将是 MyObject.DataContext,而不是 MyControl.DataContext。此外,如果 MyObject 没有数据上下文,则绑定将无法生成值。

注意:如果您使用编译后的绑定,需要在 <Style> 元素中显式设置绑定源的数据类型:

<Style Selector="MyControl" x:DataType="MyViewModelClass">
  <Setter Property="ControlProperty" Value="{Binding MyViewModelProperty}" />
</Style>

信息

For more information about compiled bindings, see here. --> TO DO

设置器数据模板

如前面所述,当使用没有数据模板的设置器时,将创建一个设置器值的单个实例,并在所有匹配的控件之间共享。要根据数据模板更改值,请将目标控件放置在模板元素内,如下所示:

<Style Selector="Border.empty">
  <Setter Property="Child">
    <Template>
      <TextBlock>No content available.</TextBlock>
    </Template>
  </Setter>
</Style>

 

设置器优先级

Avalonia 的 Setter 按照 BindingPriority、视觉树局部性和样式集合顺序依次应用。每个 StyledProperty 都单独应用优先级,从而使得样式可以受益于组合。DirectProperty 和 CLR 属性不能被样式化,因此不参与此优先级。

绑定优先级

Animation = -1, // 最高优先级
LocalValue = 0,
StyleTrigger,
Template,
Style,
Inherited,
Unset = int.MaxValue, // 最低优先级

如何在XAML中分配绑定优先级?

BindingPriority 不能在 XAML 中显式设置。以下示例展示了如何在每个场景中隐式分配 绑定优先级。这对于设计和排除样式故障至关重要,以确保样式按预期工作。

Animation

Animation 具有最高的 绑定优先级,并应用于 Keyframe 中的 Setter,通常在整个过渡系统中应用。

<Button Background="Green" Content="Bounces from Red to Blue">
    <Button.Styles>
        <Style Selector="Button">
            <Style.Animations>
                <Animation IterationCount="Infinite" Duration="0:0:2">
                    <KeyFrame Cue="0%">
                        <Setter Property="Background" Value="Red" />
                    </KeyFrame>
                    <KeyFrame Cue="100%">
                        <Setter Property="Background" Value="Blue" />
                    </KeyFrame>
                </Animation>
            </Style.Animations>
        </Style>
    </Button.Styles>
</Button>

LocalValue

当 XAML 属性在 ControlTemplate 之外直接设置时分配。下面的两个 Background Setter 都将具有 LocalValue 优先级。

<Button Background="Orange" />
<Button Background="{DynamicResource ButtonBrush}" />

提示

资源标记扩展对优先级没有任何影响。

StyleTrigger

当 Selector 具有条件激活时,Setter 的 绑定优先级 从 Style 提升到 StyleTrigger。任何具有条件激活的两个选择器将具有相同的优先级,无论存在多少激活器以及激活器在选择器语法中的位置。Avalonia 没有 CSS 的 Specificity 概念。

<Style Selector="Button:pointerover /template/ ContentPresenter#PART_ContentPresenter">
    <Setter Property="Background" Value="Orange" />
</Style>

提示

样式类、伪类、子位置和属性匹配选择器是条件性的。控件名称选择器不是条件性的。

Template

当属性在 ControlTemplate 中直接设置时。下面的 BorderThicknessBackground 和 Padding 具有 Template 优先级。

<ControlTemplate>
    <Border BorderThickness="2">
        <Button Background="{DynamicResource ButtonBrush}" Padding="{TemplateBinding Padding}" />
    </Border>
</ControlTemplate>

Style

当 Setter 在 Style 中定义且没有条件激活时。

<Style Selector="Button /template/ ContentPresenter#PART_ContentPresenter">
    <Setter Property="Background" Value="Orange" />
</Style>

提示

特别值得注意的是,其优先级低于 Template。因此,这些选择器不能用于覆盖上述 Template 示例中提到的属性。

Inherited

当属性未设置时,它可能会从其父级继承属性值。这必须在属性注册期间或在 OverrideMetadata 中指定。

public static readonly StyledProperty<bool> UseLayoutRoundingProperty =
    AvaloniaProperty.Register<Layoutable, bool>(
        nameof(UseLayoutRounding),
        defaultValue: true,
        inherits: true);

视觉树局部性

具有相同 BindingPriority 的 Setter 将根据它们在视觉树中相对于 Control 的位置来选择。需要向上遍历节点最少的 Setter 将具有优先权。内联样式 Setter 在此步骤中具有最高优先权。

<Window>
    <Window.Styles>
        <Style Selector="Button">
            <Setter Property="FontSize" Value="16" />
            <Setter Property="Foreground" Value="Red" />
        </Style>
    </Window.Styles>
    <StackPanel>
        <StackPanel.Styles>
            <Style Selector="Button">
                <Setter Property="FontSize" Value="24" />
            </Style>
        </StackPanel.Styles>

        <Button Content="This Has FontSize=24 with Foreground=Red" />
    </StackPanel>
</Window>

Styles集合顺序

当 绑定优先级 和视觉树局部性都相等时,最终的决定因素是 Styles 集合中的顺序。最后一个适用的 Setter 将具有优先级。

<StackPanel>
    <StackPanel.Styles>
        <Style Selector="Button.small">
            <Setter Property="FontSize" Value="12" />
        </Style>
        <Style Selector="Button.big">
            <Setter Property="FontSize" Value="24" />
        </Style>
    </StackPanel.Styles>

    <Button Classes="small big" Content="This Has FontSize=24" />
    <Button Classes="big small" Content="This Also Has FontSize=24" />
</StackPanel>

信息

这些按钮以不同的顺序指定它们的类,但这不会影响 Setter 优先级。

绑定优先级不会传递

回想一下上面的 Animation 示例。即使 BindingPriority.Animation 具有最高优先级,当你悬停时,动画背景仍会被静态背景替换。这是因为 Selector 目标了错误的 Control。检查 ControlTheme 是诊断原因的必要步骤。

ControlTheme for Button, Trimmed

<ControlTheme x:Key="{x:Type Button}" TargetType="Button">
    <Setter Property="Background" Value="{DynamicResource ButtonBackground}"/>
    <Setter Property="Template">
        <ControlTemplate>
            <ContentPresenter x:Name="PART_ContentPresenter"
                              Background="{TemplateBinding Background}"/>
        </ControlTemplate>
    </Setter>

    <Style Selector="^:pointerover /template/ ContentPresenter#PART_ContentPresenter">
        <Setter Property="Background" Value="{DynamicResource ButtonBackgroundPointerOver}"/>
    </Style>
</ControlTheme>

顶部的 Setter 以 Style 优先级将 ButtonBackground 应用到 ButtonBackground 的渲染由具有 Template 优先级的 ContentPresenter 处理,它获取已应用于 Button 的 ButtonBackground

但是当 Button 被悬停时,:pointerover Selector 会被激活并具有 StyleTrigger 优先级,这会覆盖 TemplateBinding 并获取 ButtonBackgroundPointerOver 而不是 Button 的 Background。这绕过了我们最初的 Animation Selector 所针对的 Button 的 Background。 以下表格总结了这一点:

悬停时的背景 Setter 和样式优先级位置
Background="Green"LocalValueButton
Background="Red"Animation (覆盖 LocalValue)Keyframe
<ContentPresenter Background="{TemplateBinding Background}"/>TemplateControlTemplate
^:pointerover /template/ ContentPresenter#PART_ContentPresenterStyleTrigger (覆盖 Template)ControlTheme

相反,我们应该使用优先级至少为 StyleTrigger 的 Setter 来针对 ContentPresenterBindingPriority.Animation 满足这个要求。这一观察结果只有在检查原始 ControlTemplate 时才能得出,并强调仅依赖优先级是不足以有效样式化应用程序的。

Corrected to override :pointerover priority

<Button Background="Green" Content="Bounces from Red to Blue">
    <Button.Styles>
        <Style Selector="Button /template/ ContentPresenter#PART_ContentPresenter">
            <Style.Animations>
                <Animation IterationCount="Infinite" Duration="0:0:2">
                    <KeyFrame Cue="0%">
                        <Setter Property="Background" Value="Red" />
                    </KeyFrame>
                    <KeyFrame Cue="100%">
                        <Setter Property="Background" Value="Blue" />
                    </KeyFrame>
                </Animation>
            </Style.Animations>
        </Style>
    </Button.Styles>
</Button>

如何使用包含的样式

本指南向您展示了如何从一个单独的样式文件中共享样式(该文件已包含在您的应用程序中)。这种方法允许您在多个应用程序之间共享样式。

要实现这一点,您需要在一个新的XAML文件中定义样式。在这里,根元素必须是StyleStyles元素之一。例如:

<Styles xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style Selector="TextBlock.h1">
        <Setter Property="FontSize" Value="24"/>
        <Setter Property="FontWeight" Value="Bold"/>
    </Style>
</Styles>

Avalonia UI 解决方案模板提供了一种快速添加样式文件到您的项目的方法。按照以下步骤进行操作:

  • 解决方案资源管理器中,右键单击您的项目。
  • 点击添加新建项
  • 在Avalonia项目中,点击 Styles (Avalonia)
  • 为您的样式文件输入一个名称

要使用在一个单独文件中定义的样式,您需要通过StyleInclude元素来引用它。source属性定义了样式文件的位置。您可以选择在哪个级别添加这个元素。

例如,要使用在名为AppStyles.axaml的文件中定义的样式(保存在/Styles文件夹中),您可以在窗口中添加如下的StyleInclude元素:

<Window ... >
    <Window.Styles>
        <StyleInclude Source="/Styles/AppStyles.axaml" />
    </Window.Styles>

    <StackPanel>
       <TextBlock Classes="h1">Heading 1</TextBlock>
       <TextBlock>This is not a heading and will not be changed.</TextBlock>
    </StackPanel>
</Window>

然而,更常见的是在App.axaml文件中引用样式文件,如下所示:

<Application... > 
    <Application.Styles>
        <FluentTheme Mode="Light"/>
        <StyleInclude Source="/AppStyles.axaml"/>
    </Application.Styles>
</Application>

这样将允许您在整个应用程序中使用来自单独文件的样式。

您还可以通过使用 avares:// 前缀来包含另一个程序集的样式:

<Application... > 
    <Application.Styles>
        <FluentTheme Mode="Light"/>
        <StyleInclude Source="avares://MyApp.Shared/Styles/CommonAppStyles.axaml"/>
    </Application.Styles>
</Application>

将引用 MyApp.Shared 项目中的 /Styles/CommonAppStyles.axaml 文件。

 

👉 如何使用资源

应用程序中的基础图形元素通常需要统一的呈现风格,例如画刷(Brush)、颜色等。您可以将这些定义为资源,放在 Avalonia UI 应用程序的不同层级中,也可以将它们放在需要的文件中引用。

资源(Resources)总是定义在资源字典内。这意味着每个资源都有一个Key属性。

定义资源字典的位置决定了资源的有效范围:资源在定义它们的文件及其子级中都可用。因此,您可以通过选择资源字典的位置来定制资源的范围。

声明资源

例如,您可能希望在整个应用程序中标准化笔刷颜色。在这种情况下,您可以在应用程序的 XAML 文件App.axaml中声明一个资源字典,如下所示:

App.axaml

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="MyApp.App">
  <Application.Resources>
    <SolidColorBrush x:Key="Warning">Yellow</SolidColorBrush>
  </Application.Resources>
</Application>

或者,您可能希望一组资源仅适用于特定的窗口或用户控件。在这种情况下,您将在窗口或用户控件文件中定义一个资源字典。例如:

MyUserControl.axaml

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="MyApp.MyUserControl">
  <UserControl.Resources>
    <SolidColorBrush x:Key="Warning">LightYellow</SolidColorBrush>
  </UserControl.Resources>
</UserControl>

实际上,您可以在控件级别定义资源,如果需要:

MainWindow.axaml

<Window xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="MyApp.MainWindow">
  <StackPanel>
    <StackPanel.Resources>
      <SolidColorBrush x:Key="Warning">PaleGoldenRod</SolidColorBrush>
    </StackPanel.Resources>
  </StackPanel>
</Window>

您还可以声明特定样式使用的资源。

MyStyle.axaml

<Style Selector="TextBlock.warning">
  <Style.Resources>
    <SolidColorBrush x:Key="Warning">Yellow</SolidColorBrush>
  </Style.Resources>
  <Setter ... />
</Style>

备注

请注意,此资源在特定样式块之外是不可见的,这意味着它不会在样式块之外具有“warning”类的TextBlock中生效。

还可以为特定主题变体定义资源:深色(Dark)、浅色(Light) 或自定义主题。下面的示例中,BackgroundBrush 和 ForegroundBrush 将根据系统或应用程序设置的当前主题变体而具有不同的值。有关主题变体的更多信息,请阅读 主题变体 页面。

<ResourceDictionary>
    <ResourceDictionary.ThemeDictionaries>
        <ResourceDictionary x:Key='Light'>
            <SolidColorBrush x:Key='BackgroundBrush'>White</SolidColorBrush>
            <SolidColorBrush x:Key='ForegroundBrush'>Black</SolidColorBrush>
        </ResourceDictionary>
        <ResourceDictionary x:Key='Dark'>
            <SolidColorBrush x:Key='BackgroundBrush'>Black</SolidColorBrush>
            <SolidColorBrush x:Key='ForegroundBrush'>White</SolidColorBrush>
        </ResourceDictionary>
    </ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>

资源字典文件

通过在单独的文件中定义资源字典,您可以提高 Avalonia UI 应用程序项目的组织性,使资源定义易于查找和维护。

位于资源字典文件中的资源可在整个应用程序中访问。

要添加资源字典文件,请按照以下步骤操作:

  • 在您要创建新文件的位置,右键单击项目。
  • 单击 添加,然后选择 新建项
  • 在左侧的列表中单击 Avalonia

  • 选择 Resource Dictionary (Avalonia)
  • 输入要使用的文件名。
  • 单击 添加

现在,您可以在指定的位置添加要定义的资源。如下所示:

<ResourceDictionary xmlns="https://github.com/avaloniaui"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <!-- 在此添加资源 -->
</ResourceDictionary>

使用资源

您可以使用 DynamicResource 标记扩展使用在资源字典的有效范围内定义的资源。

例如,要直接在边框元素的背景属性上使用资源,可以使用以下 XAML:

<Border Background="{DynamicResource Warning}">
  Look out!
</Border>

静态资源

或者,您可以选择使用 StaticResource 标记扩展。例如:

<Border Background="{StaticResource Warning}">
  Look out!
</Border>

静态资源不同之处在于它不会响应代码中(运行时)对资源的更改。一旦加载,静态资源将不可更改。

使用静态资源的好处是它的计算量较小,因此加载速度更快,并且使用的内存更少。

资源优先级

Avalonia UI 通过从 DynamicResource 或 StaticResource 标记扩展所在的逻辑控件树级别向上搜索来解析要使用的资源,直到找到具有所请求的关键字的资源。

这意味着具有相同关键字的资源的优先级是基于它们与正在解析的资源标记扩展之间的距离来确定的。因此,逻辑控件树中较高级别的资源定义实际上会被处于较低级别的资源定义“覆盖”。例如,请考虑以下 XAML:

<UserControl ... >
  <UserControl.Resources>
    <SolidColorBrush x:Key="Warning">Yellow</SolidColorBrush>
  </UserControl.Resources>

  <StackPanel>
    <StackPanel.Resources>
      <SolidColorBrush x:Key="Warning">Orange</SolidColorBrush>
    </StackPanel.Resources>

    <Border Background="{DynamicResource Warning}">
      Look out!
    </Border>
  </StackPanel>
</UserControl>

在这里,边框控件使用了关键字为Warning的资源。这个资源在父控件(StackPanel)级别上定义了两次,并且在UserControl级别上也定义了一次。Avalonia UI 最终确定的边框背景应该是橙色,因为沿着逻辑控件树向上搜索的第一个拥有此关键字的资源字典位于StackPanel中。

包含和合并资源

可以从资源字典文件中包含资源,并将其与另一个文件中定义的资源合并(即使没有任何资源)。

这意味着您可以在一个独立的文件中设计样式,并在另一个文件中引入其中的资源。这样更容易实现样式一致,也便于组织和维护应用程序解决方案。

要在样式文件中包含来自文件的资源字典,请添加以下 XAML:

<Styles.Resources>
    <ResourceDictionary>
      <ResourceDictionary.MergedDictionaries>
        <ResourceInclude Source="/Assets/AppResources.axaml"/>
      </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
  </Styles.Resources>

在上面的示例中,资源文件 AppResources.axaml 位于 /Assets 项目文件夹中。然后,您可以使用这些资源定义样式,例如:

<Style Selector="Button.btn-info">
    <Setter Property="Background" Value="{StaticResource InfoColor}"/>
</Style>

其中,资源 InfoColor 在导入的文件中被定义为 SolidColorBrush

信息

请注意,这里使用 StaticResource 引用,因为它不能更改——这里的需求是保持样式一致。

合并资源的优先级

如前所述,资源通过从标记扩展所在的点开始向上搜索逻辑控件树来解析。

然而,由于可以在应用程序的不同位置中定义样式和合并字典,优先级规则将更加复杂,具体规则如下所示:

  • Control resources -> Merged dictionaries
  • Style resources -> Merged dictionaries
  • App resources -> Merged dictionaries

例如,在下面的“应用程序”中,底部Border控件上使用的资源将遵循[]中指示的顺序进行搜索:

Application
 |- Resources [11]
     |- Merged dictionary [12]
     |- Merged dictionary [13]
 |- Styles
     |- Resources [14]
         |- Merged dictionary [15]
         |- Merged dictionary [16]

Window
 |- Resources [6]
     |- Merged dictionary [7]
 |- Styles
     |- Resources [8]
         |- Merged dictionary [9]
         |- Merged dictionary [10]
 |- StackPanel
     |- Resources [1]
         |- Merged dictionary [2]
         |- Merged dictionary [3]
     |- Styles
         |- Resources [4]
             |- Merged dictionary [5]
     |- Border

从Border控件开始,首先搜索父级(StackPanel)控件中定义的资源。然后再考虑同一级别的合并字典 - 按照 XAML 中它们出现的顺序。

然后,搜索父级(StackPanel)控件中定义的样式,接着再搜索同一级别的合并字典。

在逻辑控件树中向上搜索,每个级别都以类似的方式进行。应用程序级别的资源和样式是搜索的最后一级。

从代码中使用资源

Avalonia 提供了不同的方法来实现代码中访问资源。

备注

在下面的示例中,ResourceNode 可以是支持 Resource 的任何节点,比如 Appliction.CurrentWindowUserControl 等等。

  • ResourceNode.Resources["TheKey"]:
    这将直接访问当前的 Dictionary。请注意:合并的字典和父级将不会被扫描。
  • ResourceNode.TryGetResource:
    此函数将尝试获取特定资源,并在成功时返回 true,否则返回 false。将扫描合并的字典,但不会遵循逻辑树。
  • ResourceNode.TryFindResource:
    此扩展方法将尝试获取特定资源,并在成功时返回 true,否则返回 false。将扫描合并字典和逻辑树。
  • ResourceNode.GetResourceObservable:
    这将返回一个可用于观察资源更改的 IObservable。例如,您可以绑定到它。
// 在此示例中,我们在 App.axaml 中定义了资源,并且希望在 MainWindow 构造函数中查找该值。
//
//    </Application.Resources>
//         <x:String x:Key="TheKey">HelloWorld</x:String>
//    </Application.Resources>

public MainWindow()
{
    InitializeComponent();

    // found1 = false | result1 = null
    var found1 = this.TryGetResource("TheKey", this.ActualThemeVariant, out var result1);

    // found2 = true | result2 = "Hello World" 
    var found2 = this.TryFindResource("TheKey", this.ActualThemeVariant, out var result2);

    // 从代码中找到资源并将其绑定到 TextBlock
    myTextBlock.Bind(TextBlock.TextProperty, Resources.GetResourceObservable("TheKey"));

    // 通过绑定的 observable 更新 myTextBlock.Text
    this.Resources["TheKey"] = "Hello from code behind"; 
}

如何使用主题变体

提示

由于主题变体与资源系统深度集成,了解 Avalonia 的 resources 首先是很重要的。

介绍

在 Avalonia 中,主题变体(theme variant)指的是基于选择的主题而具有的特定视觉外观的控件。

通过使用主题变体,开发人员可以创建视觉吸引力强、一致性的用户界面,以适应不同的用户偏好或系统设置。例如,一个应用程序可以提供一个带有白色背景和黑色文本的浅色主题变体,以及一个带有黑色背景和白色文本的深色主题变体。用户可以选择他们喜欢的主题,应用程序将相应地调整其外观。

Avalonia 内置的主题 SimpleTheme 和 FluentTheme 无需额外代码即可无缝支持 Dark 和 Light 变体。这使得应用程序可以根据系统偏好动态适应,并使用内置控件。对于高级定制,本文档解释了如何定义与变体相关的自定义资源及其引用方式。

切换当前主题变体

默认情况下,Avalonia 继承用户在全系统范围内设置的主题变体。 应用程序通过两个重要的属性来控制主题变体:ActualThemeVariant 和 RequestedThemeVariant。这些属性允许您在应用程序的不同层级中管理和切换主题变体。

ActualThemeVariant 属性

ActualThemeVariant 只读属性用于检索控件、窗口或应用程序当前使用的 UI 主题。它表示元素上实际应用的主题变体。 该属性在每个控件上都可用,并在树中向下继承。在访问 主题字典(theme dictionaries) 时,其值也被样式系统使用。

RequestedThemeVariant 属性

RequestedThemeVariant 属性允许覆盖主题变体并为 ApplicationWindow(TopLevel) 或 ThemeVariantScope 指定所需的变体。

要覆盖全局应用程序变体,而不使用系统默认值:

App.axaml

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="AvaloniaApplication.App"
             RequestedThemeVariant="Dark">
  <Application.Styles>
    <FluentTheme />
  </Application.Styles>
</Application>

或者可以在特定子树中重新定义主题变体,使用 ThemeVariantScope 控件。在下面的示例中,Window 使用 Dark 变体,而 ThemeVariantScope 内部将其重新定义为 Light 变体:

MainWindow.axaml

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
        x:Class="AvaloniaApplication.MainWindow"
        RequestedThemeVariant="Dark"
        Background="Gray">
  <StackPanel Spacing="5" Margin="5">
    <Button Content="Dark button" />
    <ThemeVariantScope RequestedThemeVariant="Light">
      <Button Content="Light button" />
    </ThemeVariantScope>
  </StackPanel>
</Window>

如果需要使用重置 RequestedThemeVariant 的值,可以将 RequestedThemeVariant="Default" 设置在其中。

提示

更改 Window 的 RequestedThemeVariant 也会影响支持该功能的平台上的窗口装饰变体。

定义和引用自定义的变体特定资源

在 Avalonia 中,主题变体特定的资源可以在 ResourceDictionary 中使用 ThemeDictionaries 属性进行定义。

通常,开发人员使用 Light 或 Dark 作为主题变体的键。使用 Default 作为键标记这个特定的主题字典作为后备,以防在其他主题字典中找不到主题变体或资源键。

继续上面的例子,让我们为每个主题变体添加 BackgroundBrush 和 ForegroundBrush 并赋予不同的值:

MainWindow.axaml

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
        x:Class="Sandbox.MainWindow"
        RequestedThemeVariant="Dark"
        Background="Gray">
  <Window.Resources>
    <ResourceDictionary>
      <ResourceDictionary.ThemeDictionaries>
        <ResourceDictionary x:Key='Light'>
          <SolidColorBrush x:Key='BackgroundBrush'>SpringGreen</SolidColorBrush>
          <SolidColorBrush x:Key='ForegroundBrush'>Black</SolidColorBrush>
        </ResourceDictionary>
        <ResourceDictionary x:Key='Dark'>
          <SolidColorBrush x:Key='BackgroundBrush'>DodgerBlue</SolidColorBrush>
          <SolidColorBrush x:Key='ForegroundBrush'>White</SolidColorBrush>
        </ResourceDictionary>
      </ResourceDictionary.ThemeDictionaries>
    </ResourceDictionary>
  </Window.Resources>
  
  <Window.Styles>
    <Style Selector="Button">
      <Setter Property="Background" Value="{DynamicResource BackgroundBrush}" />
      <Setter Property="Foreground" Value="{DynamicResource ForegroundBrush}" />
    </Style>
  </Window.Styles>

  <StackPanel Spacing="5" Margin="5">
    <Button Content="Dark button"
            Background="{DynamicResource BackgroundBrush}"
            Foreground="{DynamicResource ForegroundBrush}" />
    <ThemeVariantScope RequestedThemeVariant="Light">
      <Button Content="Light button"
              Background="{DynamicResource BackgroundBrush}"
              Foreground="{DynamicResource ForegroundBrush}" />
    </ThemeVariantScope>
  </StackPanel>
</Window>

有关使用资源的更多详细信息,请参阅 如何使用资源 页面。

如何使用自定义字体

使用独特的字体来定制您的 Avalonia 应用程序,可以增加独特的外观和感觉。本指南将引导您了解如何将自定义字体集成到您的 Avalonia 应用程序中。

信息

如果您想查看这些概念在实际中的完整运行示例,请查看 示例应用程序

将自定义字体添加到项目中

在使用自定义字体之前,您需要将其包含在项目中。

在本指南中,我们将使用一个名为 Nunito 的字体,该字体已经存储在我们的应用程序资源中,路径为 avares://GoogleFonts/Assets/Fonts

确保字体的构建属性设置为 AvaloniaResource

在应用程序资源中声明您的字体

在您的Avalonia应用程序中,打开App.xaml文件,并在<Application.Resources>标签内包含您的自定义字体。为它指定一个键,您将使用该键在应用程序中引用它。在本例中,我们使用了键NunitoFont

App.axaml

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="GoogleFonts.App"
             RequestedThemeVariant="Default">
    <Application.Styles>
        <FluentTheme />
    </Application.Styles>
    <Application.Resources>
        <FontFamily x:Key="NunitoFont">avares://GoogleFonts/Assets/Fonts#Nunito</FontFamily>
    </Application.Resources>
</Application>

使用您的自定义字体

一旦您的字体在应用程序资源中声明,您可以在应用程序中使用它。

要引用您的自定义字体,请使用FontFamily属性与StaticResource标记扩展。您需要将已声明字体的键作为参数传递。在本例中,NunitoFont是我们自定义字体的键。

以下是如何将我们的自定义Nunito字体应用于TextBlock的示例:

<TextBlock Text="{Binding Greeting}" 
           FontSize="70" 
           FontFamily="{StaticResource NunitoFont}" 
           HorizontalAlignment="Center" VerticalAlignment="Center"/>

在上面的示例中,TextBlock控件将使用我们在应用程序资源中声明的Nunito字体。绑定到TextBlock的文本现在将以指定的字体大小70呈现为Nunito字体。

请记住,FontFamily属性可以应用于任何具有FontFamily属性的控件,这意味着您可以在整个应用程序中使用您的自定义字体。

就是这样!您已成功将自定义字体集成到您的Avalonia应用程序中。现在,您可以通过选择适合您的字体为应用程序的用户界面增加独特的风格。

👉 如何解决样式问题

大部分 Avalonia UI 的样式系统与 CSS 样式方法相对应。如果您对这项技术不了解,您可能会发现以下提示有所帮助。

选择器没有目标

Avalonia UI 的选择器,就像 CSS 选择器一样,当没有匹配的控件时不会引发错误或警告。样式将静默失败。

信息

检查是否使用了不存在的名称或类。

信息

检查是否使用了子选择器,而没有匹配的子控件。

包含文件的顺序

样式按照声明的顺序应用。如果有多个包含了针对相同控件属性的样式文件,则最后一个包含的样式将覆盖之前的样式。例如:

<Style Selector="TextBlock.header">
    <Style Property="Foreground" Value="Green" />
</Style>

<Style Selector="TextBlock.header">
    <Style Property="Foreground" Value="Blue" />
    <Style Property="FontSize" Value="16" />
</Style>

<StyleInclude Source="Style1.axaml" />
<StyleInclude Source="Style2.axaml" />

在这个例子中,首先应用了来自文件 Styles1.axaml 的样式,所以文件 Styles2.axaml 中的样式设置会覆盖之前的样式。最终的 TextBlock 将具有 FontSize="16" 和 Foreground="Green"。在样式文件内部也会发生相同的优先级排序。

本地设置的属性具有优先级

直接在控件上定义的本地值通常比任何样式值具有更高的优先级。因此,在这个例子中,文本块的前景色将是红色的:

<Style Selector="TextBlock.header">
    <Setter Property="Foreground" Value="Green" />
</Style>
...
<TextBlock Classes="header" Foreground="Red" />

您可以在 BindingPriority 枚举中看到完整的值优先级列表,较低的枚举值具有较高的优先级。

绑定优先级说明
Animation-1最高优先级——甚至可以覆盖本地值
LocalValue0在控件的属性上设置了本地值。
StyleTrigger1当伪类变为活动状态时触发。
TemplatedParent2
Style3
Unset2147483647

注意

例外情况是 Animation 值具有最高优先级,甚至可以覆盖本地值。

信息

一些默认的 Avalonia UI 样式在其模板中使用本地值而不是模板绑定或样式设置。这使得在不替换整个模板的情况下无法更新模板属性。

缺失的样式伪类(触发器)选择器

假设有一种情况,您希望第二个样式覆盖前一个样式,但实际上并没有覆盖:

<Style Selector="Border:pointerover">
    <Setter Property="Background" Value="Blue" />
</Style>
<Style Selector="Border">
    <Setter Property="Background" Value="Red" />
</Style>
...
<Border Width="100" Height="100" Margin="100" />

在这个代码示例中,Border 正常情况下具有红色背景,当鼠标指针悬停在上面时具有蓝色背景。这是因为与 CSS 一样,更具体的选择器具有优先权。当您希望使用一个单独的样式覆盖任何状态(例如 pointerover、pressed 或其他状态)的默认样式时,这可能会成为问题。为了实现这一点,您需要为这些状态创建新的样式。

信息

访问 Avalonia 源代码以找到当出现这种情况时的原始模板,并将带有伪类的样式复制粘贴到您的代码中。

具有伪类的选择器不覆盖默认样式

以下代码示例中的样式应该在默认样式之上起作用:

<Style Selector="Button">
    <Setter Property="Background" Value="Red" />
</Style>
<Style Selector="Button:pointerover">
    <Setter Property="Background" Value="Blue" />
</Style>

您可能期望 Button 在默认情况下是红色的,当鼠标指针悬停在上面时是蓝色的。实际上,只有第一个样式的 setter 将被应用,第二个将被忽略。

原因在于 Button 的模板中。您可以在 Avalonia 源代码中找到默认模板(旧版 Default 主题和新版 Fluent 主题),但为了方便起见,我们在此处简化了来自 Fluent 主题的模板:

<Style Selector="Button">
    <Setter Property="Background" Value="{DynamicResource ButtonBackground}"/>
    <Setter Property="Template">
        <ControlTemplate>
            <ContentPresenter Name="PART_ContentPresenter"
                              Background="{TemplateBinding Background}"
                              Content="{TemplateBinding Content}"/>
        </ControlTemplate>
    </Setter>
</Style>
<Style Selector="Button:pointerover /template/ ContentPresenter#PART_ContentPresenter">
    <Setter Property="Background" Value="{DynamicResource ButtonBackgroundPointerOver}" />
</Style>

实际背景是由 ContentPresenter 渲染的,在默认情况下它与按钮的 Background 属性绑定。然而,在 pointerover 状态下,选择器直接将背景应用于 ContentPresenter (Button:pointerover /template/ ContentPresenter#PART_ContentPresenter)。这就是为什么在前一个代码示例中我们的 setter 被忽略的原因。修正后的代码应该直接针对 content presenter:

<!-- 这里的 #PART_ContentPresenter 名称选择器不是必需的,但为了具有更具体的样式而添加 -->
<Style Selector="Button:pointerover /template/ ContentPresenter#PART_ContentPresenter">
    <Setter Property="Background" Value="Blue" />
</Style>

信息

您可以在默认主题(包括旧版 Default 和新版 Fluent)中看到所有控件的这种行为,不仅限于 Button。而且不仅限于 Background,还包括其他依赖于状态的属性。

信息

为什么默认样式直接更改 ContentPresenter 的 Background 属性而不是更改 Button.Background 属性?

这是因为如果用户在按钮上设置了本地值,它将覆盖所有样式,并使按钮始终具有相同的颜色。有关更多详情,请参见 撤销的 PR.

当样式不再应用时,特定属性的先前值不会恢复

在 Avalonia 中,我们有多种类型的属性,其中之一是直接属性(Direct Property),它根本不支持样式。这些属性以简化的方式工作,以实现较低的开销和更高的性能,并且不存储多个依赖于优先级的值。而是只保存最新的值,无法恢复之前的值。您可以在 此处 找到有关属性的更多详情。

典型的例子是 CommandProperty。它被定义为直接属性,因此它永远不会正常工作。将来,尝试为直接属性设置样式将导致编译时错误,详见 #6837

 

;