首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Avalonia Style 深度详解:从 Selector、Classes 到伪类、Theme 与 Template

Avalonia Style 深度详解:从 Selector、Classes 到伪类、Theme 与 Template

原创
作者头像
步步为营DotNet
发布2026-09-20 10:24:32
发布2026-09-20 10:24:32
740
举报

在 Avalonia 开发中,Style 是一个非常重要、同时也很容易让初学者困惑的机制。

如果你之前主要使用过 WPF,那么刚开始接触 Avalonia Style 时,很容易产生这样的疑问:

  • Avalonia 的 Style 和 WPF 的 Style 到底是不是一回事?
  • 为什么 Avalonia 使用 Selector
  • Classes="primary" 到底有什么作用?
  • Button.primary:pointerover 是什么意思?
  • Avalonia 为什么没有 WPF 那种 Trigger
  • :pointerover:pressed:focus 到底从哪里来的?
  • 为什么我写了 Style,却没有生效?
  • 两个 Style 同时设置 Background,最终到底听谁的?
  • StyleControlThemeResourceTemplate 到底应该怎么区分?

如果把这些问题弄明白,Avalonia 的 UI 样式系统其实非常清晰。

Avalonia 的 Style 设计理念更接近 CSS,而不是传统 WPF 的 Style + Trigger 模式。官方文档也明确将 Avalonia Style 描述为一种类似 CSS 的样式机制,而 WPF/UWP 中更接近传统控件主题的概念,在 Avalonia 中对应的是 ControlTheme

本文从基础开始,一直到实际项目中的样式架构。


一、先理解 Avalonia Style 到底是什么

最简单地说:

Avalonia Style 就是:找到符合条件的控件,然后给它设置属性。

可以把它抽象成:

代码语言:javascript
复制
Selector
   ↓
找到符合条件的 Control
   ↓
Setter
   ↓
修改 Control 的属性

最简单的 Style:

代码语言:javascript
复制
<Style Selector="Button">
    <Setter Property="Background" Value="Blue"/>
    <Setter Property="Foreground" Value="White"/>
</Style>

它表达的意思就是:

找到所有 Button,把它们的 Background 设置成蓝色,把 Foreground 设置成白色。

这里其实只有两个核心概念:

代码语言:javascript
复制
Selector = 找谁

Setter   = 找到以后改什么

所以一个 Style 最核心的结构就是:

代码语言:javascript
复制
<Style Selector="选择器">
    <Setter Property="属性" Value="值"/>
</Style>

例如:

代码语言:javascript
复制
<Style Selector="TextBlock">
    <Setter Property="FontSize" Value="16"/>
    <Setter Property="Foreground" Value="Gray"/>
</Style>

意思就是:

代码语言:javascript
复制
所有 TextBlock
    ↓
FontSize = 16
Foreground = Gray

Avalonia 官方文档也将 Style 的工作过程概括为两个阶段:

  1. Selection:通过 Selector 找到目标控件
  2. Substitution:通过 Setter 修改属性

二、Avalonia Style 和 WPF Style 最大的区别

如果你从 WPF 转 Avalonia,这一点非常重要。

WPF 中经常这么写:

代码语言:javascript
复制
<Style TargetType="Button">
    <Setter Property="Background" Value="Blue"/>

    <Style.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
            <Setter Property="Background" Value="Red"/>
        </Trigger>
    </Style.Triggers>
</Style>

WPF 的思路是:

代码语言:javascript
复制
Style
 ├── Setter
 └── Trigger
       └── Setter

而 Avalonia 更倾向于:

代码语言:javascript
复制
<Style Selector="Button">
    <Setter Property="Background" Value="Blue"/>
</Style>

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

也就是说:

Avalonia 通常使用 Selector + PseudoClass 来完成类似 WPF Trigger 的功能。

Avalonia 官方文档明确指出,Avalonia 没有 WPF 风格的 Trigger,条件样式通常通过 Style Classes、PseudoClasses 和 Converter 等方式实现。

这也是理解 Avalonia Style 的关键。


三、Style 最重要的组成部分:Selector

如果只能记住一个东西,我建议记住:

Avalonia Style 的灵魂是 Selector。

例如:

代码语言:javascript
复制
<Style Selector="Button">

表示:

代码语言:javascript
复制
所有 Button

而:

代码语言:javascript
复制
<Style Selector="Button.primary">

表示:

代码语言:javascript
复制
所有具有 primary class 的 Button

再例如:

代码语言:javascript
复制
<Style Selector="Button.primary:pointerover">

表示:

代码语言:javascript
复制
具有 primary class
并且当前鼠标悬停
的 Button

所以可以把 Selector 理解成:

代码语言:javascript
复制
Button
   +
.primary
   +
:pointerover

三个条件组合起来。

Avalonia 的 Selector 语法整体上与 CSS 很相似。


四、最基础的 Selector

4.1 按控件类型选择

最简单:

代码语言:javascript
复制
<Style Selector="Button">
    <Setter Property="FontSize" Value="16"/>
</Style>

匹配:

代码语言:javascript
复制
<Button Content="确定"/>
<Button Content="取消"/>
<Button Content="保存"/>

这些 Button 都会受到影响。


五、通过 Classes 精确控制样式

这是 Avalonia Style 最重要的功能之一。

例如:

代码语言:javascript
复制
<Button Classes="primary"
        Content="保存"/>

然后:

代码语言:javascript
复制
<Style Selector="Button.primary">
    <Setter Property="Background" Value="#1677FF"/>
    <Setter Property="Foreground" Value="White"/>
</Style>

此时:

代码语言:javascript
复制
<Button Classes="primary"/>

会匹配。

但是:

代码语言:javascript
复制
<Button/>

不会匹配。

所以:

代码语言:javascript
复制
Button

代表控件类型。

而:

代码语言:javascript
复制
.primary

代表 Style Class。

最终:

代码语言:javascript
复制
Button.primary

就是:

类型是 Button,并且存在 primary class。


六、Classes 是什么?

Avalonia 控件拥有一个 Classes 集合。

例如:

代码语言:javascript
复制
<Button Classes="primary large">

实际上可以理解为:

代码语言:javascript
复制
Classes
 ├── primary
 └── large

因此:

代码语言:javascript
复制
<Style Selector="Button.primary">

匹配。

而:

代码语言:javascript
复制
<Style Selector="Button.large">

也匹配。

甚至:

代码语言:javascript
复制
<Style Selector="Button.primary.large">

也匹配。

最后这个条件意味着:

Button 必须同时拥有 primarylarge 两个 Class。

官方 Selector 文档也明确规定,多 Class 选择器要求控件同时拥有这些 Class。


七、Classes 和 CSS Class 很像

如果你熟悉 Web 开发,可以直接这样理解:

代码语言:javascript
复制
<button class="primary large">

对应 Avalonia:

代码语言:javascript
复制
<Button Classes="primary large"/>

CSS:

代码语言:javascript
复制
button.primary {
    background: blue;
}

Avalonia:

代码语言:javascript
复制
<Style Selector="Button.primary">
    <Setter Property="Background" Value="Blue"/>
</Style>

因此对于 Web 开发者来说,Avalonia Style 是比较容易理解的。


八、为什么推荐使用 Classes?

假设你有 20 个 Button。

其中:

代码语言:javascript
复制
15 个普通按钮
3 个主要按钮
2 个危险按钮

你可以写:

代码语言:javascript
复制
<Button Classes="primary"/>
<Button Classes="danger"/>

然后:

代码语言:javascript
复制
<Style Selector="Button.primary">
    <Setter Property="Background" Value="#1677FF"/>
</Style>

<Style Selector="Button.danger">
    <Setter Property="Background" Value="#FF4D4F"/>
</Style>

这样比给每一个 Button 单独设置:

代码语言:javascript
复制
Background="..."
Foreground="..."
BorderBrush="..."

更加容易维护。


九、Style Class 还可以动态添加

Classes 不仅可以在 XAML 中设置。

C# 中同样可以操作:

代码语言:javascript
复制
button.Classes.Add("primary");

删除:

代码语言:javascript
复制
button.Classes.Remove("primary");

切换:

代码语言:javascript
复制
button.Classes.Toggle("primary");

判断:

代码语言:javascript
复制
if (button.Classes.Contains("primary"))
{
    // ...
}

官方文档也提供了这些 API。

这意味着你可以在程序运行过程中动态改变控件状态。

例如:

代码语言:javascript
复制
if (isError)
{
    statusText.Classes.Add("error");
}
else
{
    statusText.Classes.Add("success");
}

然后:

代码语言:javascript
复制
<Style Selector="TextBlock.error">
    <Setter Property="Foreground" Value="Red"/>
</Style>

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

十、条件 Class:非常实用

Avalonia 甚至支持:

代码语言:javascript
复制
<Button Classes.accent="{Binding IsSpecial}">

当:

代码语言:javascript
复制
IsSpecial = true

相当于:

代码语言:javascript
复制
Classes="accent"

当:

代码语言:javascript
复制
IsSpecial = false

则不会拥有这个 Class。

官方文档将这种方式称为 Conditional Classes。

例如:

代码语言:javascript
复制
<Border Classes.error="{Binding HasError}">

配合:

代码语言:javascript
复制
<Style Selector="Border.error">
    <Setter Property="BorderBrush" Value="Red"/>
</Style>

非常适合 MVVM。


十一、PseudoClass:Avalonia Style 的另一个核心

如果说:

代码语言:javascript
复制
Classes

代表:

我人为定义的状态。

那么:

代码语言:javascript
复制
PseudoClasses

可以理解为:

控件自身产生的状态。

例如:

代码语言:javascript
复制
:pointerover
:pressed
:focus
:disabled
:checked

等等。

因此:

代码语言:javascript
复制
<Style Selector="Button:pointerover">

表示:

当 Button 处于鼠标悬停状态时应用这个 Style。

官方文档列举了 :pointerover:focus:disabled:pressed:checked 等常见伪类。


十二、最常用的 Button 状态

一个比较完整的 Button 样式可以这样写:

代码语言:javascript
复制
<Style Selector="Button.primary">
    <Setter Property="Background" Value="#1677FF"/>
    <Setter Property="Foreground" Value="White"/>
</Style>

<Style Selector="Button.primary:pointerover">
    <Setter Property="Background" Value="#4096FF"/>
</Style>

<Style Selector="Button.primary:pressed">
    <Setter Property="Background" Value="#0958D9"/>
</Style>

<Style Selector="Button.primary:disabled">
    <Setter Property="Opacity" Value="0.5"/>
</Style>

对应的状态关系就是:

代码语言:javascript
复制
                 Button.primary
                       │
          ┌────────────┼────────────┐
          ↓            ↓            ↓
    :pointerover    :pressed    :disabled
          │            │            │
       悬停颜色      按下颜色      禁用效果

这就是 Avalonia 中非常典型的状态样式设计。


十三、Classes 和 PseudoClass 的区别

这个问题非常重要。

例如:

代码语言:javascript
复制
<Button Classes="primary"/>

这里:

代码语言:javascript
复制
primary

是你定义的 Class。

而:

代码语言:javascript
复制
Button:pointerover

这里:

代码语言:javascript
复制
:pointerover

是控件当前产生的 PseudoClass。

可以简单记成:

类型

示例

来源

Class

.primary

开发者定义

Class

.danger

开发者定义

PseudoClass

:pointerover

控件状态

PseudoClass

:pressed

控件状态

PseudoClass

:focus

控件状态

PseudoClass

:checked

控件状态

因此:

代码语言:javascript
复制
Button.primary:pointerover

就是:

代码语言:javascript
复制
Button
+
primary Class
+
pointerover PseudoClass

十四、Selector 的组合

Avalonia Selector 可以组合多个条件。

例如:

代码语言:javascript
复制
<Style Selector="Button.primary:pointerover">

可以拆成:

代码语言:javascript
复制
Button
   ↓
类型

.primary
   ↓
Class

:pointerover
   ↓
PseudoClass

再比如:

代码语言:javascript
复制
<Style Selector="Button.primary.large:focus">

要求:

代码语言:javascript
复制
Button
AND primary
AND large
AND focus

全部成立。


十五、根据 Name 选择控件

也可以通过 Name

代码语言:javascript
复制
<Button Name="LoginButton"/>

然后:

代码语言:javascript
复制
<Style Selector="#LoginButton">
    <Setter Property="Background" Value="Green"/>
</Style>

也可以:

代码语言:javascript
复制
<Style Selector="Button#LoginButton">
    <Setter Property="Background" Value="Green"/>
</Style>

其中:

代码语言:javascript
复制
#

表示 Name。

官方 Selector 语法中也支持 #name 形式。

不过在实际项目中,我一般不建议大量依赖 Name Selector。

如果是为了表达:

代码语言:javascript
复制
这是主要按钮
这是危险按钮
这是警告文本

更适合:

代码语言:javascript
复制
Classes="primary"
Classes="danger"
Classes="warning"

而不是:

代码语言:javascript
复制
Name="Button1"
Name="Button2"

十六、后代选择器

Avalonia 还支持类似 CSS 的层级选择。

例如:

代码语言:javascript
复制
<Style Selector="StackPanel Button">
    <Setter Property="Margin" Value="5"/>
</Style>

意思是:

StackPanel 内部的 Button。

不要求 Button 是直接子元素。

例如:

代码语言:javascript
复制
<StackPanel>

    <Border>
        <Button/>
    </Border>

</StackPanel>

这个 Button 依然可以匹配。

官方 Selector 文档将这种形式称为 descendant selector。


十七、直接子元素选择器

如果只希望匹配直接子元素,可以使用:

代码语言:javascript
复制
<Style Selector="StackPanel > Button">

例如:

代码语言:javascript
复制
<StackPanel>

    <Button/>
    
    <Border>
        <Button/>
    </Border>

</StackPanel>

那么:

代码语言:javascript
复制
第一个 Button
    ↓
匹配

第二个 Button
    ↓
不匹配

因为第二个 Button 中间隔了一层 Border。


十八、:is() 非常重要

Avalonia 中还提供:

代码语言:javascript
复制
:is(Button)

它和简单的:

代码语言:javascript
复制
Button

有一个重要区别。

普通:

代码语言:javascript
复制
Button

主要匹配 Button 的 StyleKey。

而:

代码语言:javascript
复制
:is(Button)

可以匹配 Button 以及派生类型。

例如:

代码语言:javascript
复制
<Style Selector=":is(Button)">
    <Setter Property="FontSize" Value="16"/>
</Style>

对于自定义 Button 派生控件也更有意义。

官方文档也特别说明,普通类型选择器不会匹配派生类型,而 :is() 可以用于这种场景。


十九、:not() 排除某些控件

例如:

代码语言:javascript
复制
<Style Selector="Button:not(.primary)">
    <Setter Property="Background" Value="Gray"/>
</Style>

可以理解为:

代码语言:javascript
复制
Button
但是没有 primary class

因此:

代码语言:javascript
复制
<Button/>

匹配。

而:

代码语言:javascript
复制
<Button Classes="primary"/>

不匹配。


二十、Template Selector

这是很多 Avalonia 开发者第一次接触时最容易懵的地方。

例如:

代码语言:javascript
复制
<Style Selector="Button:pointerover /template/ ContentPresenter">
    <Setter Property="Background" Value="Red"/>
</Style>

这里:

代码语言:javascript
复制
/template/

表示:

进入控件的 ControlTemplate 内部进行选择。

也就是说:

代码语言:javascript
复制
Button
  ↓
:pointerover
  ↓
进入 Template
  ↓
找到 ContentPresenter

然后修改 Template 内部的控件。

官方 Selector 文档也将 /template/ 作为专门的 Template selector。

这个功能对于修改:

  • Button
  • TextBox
  • ComboBox
  • ListBox
  • CheckBox
  • TabControl

等复杂控件的内部视觉结构非常重要。


二十一、为什么有时候直接设置 Button.Foreground 没有效果?

这是 Avalonia Style 中非常常见的问题。

例如:

代码语言:javascript
复制
<Style Selector="Button">
    <Setter Property="Foreground" Value="Red"/>
</Style>

你发现:

为什么 Button 里面的文字没有变红?

原因很可能是:

代码语言:javascript
复制
Button
  ↓
ControlTemplate
  ↓
ContentPresenter
  ↓
TextBlock

真正显示文字的可能不是 Button 本身,而是 Template 内部的元素。

所以你可能需要:

代码语言:javascript
复制
<Style Selector="Button /template/ ContentPresenter">
    <Setter Property="Foreground" Value="Red"/>
</Style>

或者根据具体 Theme 的模板结构进行针对性处理。

因此:

Avalonia Style 不仅可以修改控件本身,也可以深入 Template 修改内部控件。


二十二、Nested Style:嵌套 Style

Avalonia 支持嵌套 Style。

例如:

代码语言:javascript
复制
<Style Selector="TextBlock.title">

    <Setter Property="FontSize" Value="24"/>
    <Setter Property="FontWeight" Value="Bold"/>

    <Style Selector="^:pointerover">
        <Setter Property="Foreground" Value="Red"/>
    </Style>

</Style>

这里:

代码语言:javascript
复制
^

非常重要。

它表示:

当前 Style 的父 Selector。

因此:

代码语言:javascript
复制
<Style Selector="^:pointerover">

实际上相当于:

代码语言:javascript
复制
<Style Selector="TextBlock.title:pointerover">

官方文档也明确要求嵌套 Style 使用 ^ 作为嵌套选择器的起始部分。


二十三、为什么 Nested Style 很有用?

例如一个按钮:

代码语言:javascript
复制
<Style Selector="Button.primary">

    <Setter Property="Background" Value="#1677FF"/>
    <Setter Property="Foreground" Value="White"/>

    <Style Selector="^:pointerover">
        <Setter Property="Background" Value="#4096FF"/>
    </Style>

    <Style Selector="^:pressed">
        <Setter Property="Background" Value="#0958D9"/>
    </Style>

</Style>

相比:

代码语言:javascript
复制
<Style Selector="Button.primary"/>
<Style Selector="Button.primary:pointerover"/>
<Style Selector="Button.primary:pressed"/>

嵌套写法能够把一个组件的所有状态集中在一起。

对于复杂 UI,非常推荐这种写法。


二十四、Style 的作用域

Style 不一定只能写在 App.axaml

可以写在:

代码语言:javascript
复制
<Application.Styles>

也可以写在:

代码语言:javascript
复制
<Window.Styles>

或者:

代码语言:javascript
复制
<UserControl.Styles>

甚至某些 Control 的:

代码语言:javascript
复制
<StackPanel.Styles>

例如:

代码语言:javascript
复制
<Window>
    <Window.Styles>

        <Style Selector="Button">
            <Setter Property="FontSize" Value="16"/>
        </Style>

    </Window.Styles>
</Window>

那么这个 Style 会作用于:

代码语言:javascript
复制
Window
 ├── Button
 ├── StackPanel
 │    └── Button
 └── UserControl
      └── Button

也就是说,Style 的定义位置会影响它的作用范围。

Avalonia 官方文档明确说明,Application 级 Style 可以用于整个应用,而 Window/UserControl 等位置定义的 Style 则具有相应的局部作用域。


二十五、App.axaml 是全局 Style 的常用位置

例如:

代码语言:javascript
复制
<Application.Styles>

    <Style Selector="Button.primary">
        <Setter Property="Background" Value="#1677FF"/>
    </Style>

</Application.Styles>

整个应用都可以:

代码语言:javascript
复制
<Button Classes="primary"/>

这非常适合定义:

代码语言:javascript
复制
全局按钮
全局字体
全局颜色
全局输入框
全局间距
全局状态

二十六、推荐把大型项目的 Style 拆文件

不要把所有 Style 都堆在:

代码语言:javascript
复制
App.axaml

里面。

可以拆成:

代码语言:javascript
复制
Styles/
├── Button.axaml
├── TextBox.axaml
├── TextBlock.axaml
├── Card.axaml
├── Navigation.axaml
├── Dialog.axaml
└── Colors.axaml

然后通过:

代码语言:javascript
复制
<Application.Styles>

    <FluentTheme />

    <StyleInclude Source="avares://MyApp/Styles/Button.axaml"/>
    <StyleInclude Source="avares://MyApp/Styles/TextBox.axaml"/>
    <StyleInclude Source="avares://MyApp/Styles/Card.axaml"/>

</Application.Styles>

这样项目规模大起来以后会舒服很多。


二十七、Style 和 Resource 不要混淆

这是另外一个非常重要的概念。

Resource:

代码语言:javascript
复制
<Application.Resources>

    <SolidColorBrush x:Key="PrimaryBrush"
                     Color="#1677FF"/>

</Application.Resources>

Style:

代码语言:javascript
复制
<Application.Styles>

    <Style Selector="Button.primary">
        <Setter Property="Background"
                Value="{DynamicResource PrimaryBrush}"/>
    </Style>

</Application.Styles>

两者职责不同:

代码语言:javascript
复制
Resource
    ↓
存放可以复用的东西

Style
    ↓
规定控件应该怎么样

Resource 可以保存:

代码语言:javascript
复制
Brush
Color
Thickness
Double
String
Style
DataTemplate
ControlTheme

等可复用对象。

官方资源文档也将 Brush、Color、Thickness、String 和 Style 等列为常见资源。


二十八、推荐使用 Resource 管理颜色

例如:

代码语言:javascript
复制
<Application.Resources>

    <SolidColorBrush x:Key="PrimaryBrush"
                     Color="#1677FF"/>

    <SolidColorBrush x:Key="DangerBrush"
                     Color="#FF4D4F"/>

    <SolidColorBrush x:Key="SuccessBrush"
                     Color="#52C41A"/>

</Application.Resources>

然后:

代码语言:javascript
复制
<Style Selector="Button.primary">
    <Setter Property="Background"
            Value="{DynamicResource PrimaryBrush}"/>
</Style>

这样以后修改主题颜色,只需要改:

代码语言:javascript
复制
PrimaryBrush

而不需要搜索整个项目里的:

代码语言:javascript
复制
#1677FF

二十九、StaticResource 和 DynamicResource 怎么选?

如果资源可能随着 Theme 改变,通常更适合:

代码语言:javascript
复制
DynamicResource

例如:

代码语言:javascript
复制
Background="{DynamicResource PrimaryBrush}"

如果资源不会发生变化,也可以使用:

代码语言:javascript
复制
StaticResource

在需要支持:

代码语言:javascript
复制
Light Theme
Dark Theme

的时候,DynamicResource 尤其重要。


三十、Style、Resource、ControlTheme 到底有什么区别?

这是 Avalonia 中非常值得建立的一张概念图:

代码语言:javascript
复制
Resource
   │
   └── 保存可复用资源
        ├── Brush
        ├── Color
        ├── Thickness
        └── 其他对象

Style
   │
   └── 根据 Selector 给控件设置属性

ControlTheme
   │
   └── 定义一个控件的完整主题/视觉结构

ControlTemplate
   │
   └── 定义控件内部视觉树

可以简单理解为:

代码语言:javascript
复制
Resource
    = 我有什么材料?

Style
    = 哪些控件应该用什么材料?

ControlTemplate
    = 控件内部长什么样?

ControlTheme
    = 这个控件完整的一套主题是什么?

Avalonia 当前官方文档将 Styles、Control Themes 和 Container Queries 作为不同层次的样式机制。


三十一、Style 和 ControlTheme 不要混为一谈

这是从 WPF 转 Avalonia 时非常容易产生的误区。

Avalonia:

代码语言:javascript
复制
<Style Selector="Button.primary">

更接近:

代码语言:javascript
复制
CSS Style

而:

代码语言:javascript
复制
<ControlTheme TargetType="Button">

才更加接近传统 WPF:

代码语言:javascript
复制
<Style TargetType="Button">

所承担的完整控件主题职责。

所以:

代码语言:javascript
复制
普通样式
    → Style

完整控件主题
    → ControlTheme

如果你只是想:

代码语言:javascript
复制
把这个 Button 变蓝

不要一上来就写 ControlTheme。


三十二、Selector 优先级

这是 Style 最容易出现“为什么我的样式不生效”的地方。

假设:

代码语言:javascript
复制
<Style Selector="Button">
    <Setter Property="Background" Value="Blue"/>
</Style>

然后:

代码语言:javascript
复制
<Style Selector="Button.primary">
    <Setter Property="Background" Value="Green"/>
</Style>

再:

代码语言:javascript
复制
<Style Selector="Button.primary:pointerover">
    <Setter Property="Background" Value="Red"/>
</Style>

那么:

代码语言:javascript
复制
Button
    ↓
Blue

Button.primary
    ↓
Green

Button.primary:pointerover
    ↓
Red

因为后面的 Selector 更具体。

官方文档对 Selector specificity 有明确说明:Name Selector、属性/伪类、Style Class、Type Selector 等具有不同的优先级;当匹配条件相同时,后声明的样式可以覆盖前面的样式。


三十三、可以建立这样的理解

从一般到具体:

代码语言:javascript
复制
Button
   ↓
Button.primary
   ↓
Button.primary:pointerover
   ↓
Button#myButton

越具体,通常越适合解决局部问题。

因此推荐:

代码语言:javascript
复制
先写通用 Style
↓
再写 Class Style
↓
再写状态 Style
↓
最后处理特殊控件

例如:

代码语言:javascript
复制
<Style Selector="Button">
    ...
</Style>

<Style Selector="Button.primary">
    ...
</Style>

<Style Selector="Button.primary:pointerover">
    ...
</Style>

这种结构比大量:

代码语言:javascript
复制
Button#xxx
Button#xxx2
Button#xxx3

更容易维护。


三十四、不要滥用 *

虽然可以使用非常宽泛的 Selector,但实际项目中要谨慎。

例如:

代码语言:javascript
复制
<Style Selector="*">

这种样式非常容易影响大量控件。

例如:

代码语言:javascript
复制
<Style Selector="*">
    <Setter Property="Margin" Value="10"/>
</Style>

可能产生大量你没有预料到的布局问题。

官方最佳实践也建议避免过于宽泛的 Selector,例如 *:is(Control),因为它们可能匹配整个控件树并造成意外副作用。


三十五、一个完整的 Button Style

现在把前面讲的东西组合起来。

代码语言:javascript
复制
<Styles>

    <!-- 基础按钮 -->
    <Style Selector="Button">
        <Setter Property="Padding" Value="16,8"/>
        <Setter Property="FontSize" Value="14"/>
    </Style>

    <!-- Primary -->
    <Style Selector="Button.primary">

        <Setter Property="Background"
                Value="#1677FF"/>

        <Setter Property="Foreground"
                Value="White"/>

        <Style Selector="^:pointerover">

            <Setter Property="Background"
                    Value="#4096FF"/>

        </Style>

        <Style Selector="^:pressed">

            <Setter Property="Background"
                    Value="#0958D9"/>

        </Style>

        <Style Selector="^:disabled">

            <Setter Property="Opacity"
                    Value="0.5"/>

        </Style>

    </Style>

    <!-- Danger -->
    <Style Selector="Button.danger">

        <Setter Property="Background"
                Value="#FF4D4F"/>

        <Setter Property="Foreground"
                Value="White"/>

        <Style Selector="^:pointerover">

            <Setter Property="Background"
                    Value="#FF7875"/>

        </Style>

    </Style>

</Styles>

使用:

代码语言:javascript
复制
<Button Classes="primary"
        Content="保存"/>

<Button Classes="danger"
        Content="删除"/>

这就是比较标准的 Avalonia Style 写法。


三十六、一个实际项目中的 Style 架构

如果你使用 Avalonia 开发一个比较完整的桌面软件,例如:

代码语言:javascript
复制
毕业搭子

或者其他类似的桌面客户端,我会更推荐这种架构:

代码语言:javascript
复制
Styles
│
├── Colors.axaml
├── Typography.axaml
├── Button.axaml
├── TextBox.axaml
├── Card.axaml
├── Navigation.axaml
├── Dialog.axaml
├── List.axaml
└── Common.axaml

然后:

代码语言:javascript
复制
Colors
   ↓
定义颜色

Typography
   ↓
定义字体

Button
   ↓
定义按钮

Card
   ↓
定义卡片

Navigation
   ↓
定义导航

Dialog
   ↓
定义弹窗

这样你的 View XAML 就可以非常干净:

代码语言:javascript
复制
<Button Classes="primary"
        Content="开始改写"/>

而不是:

代码语言:javascript
复制
<Button
    Background="#1677FF"
    Foreground="White"
    BorderBrush="#1677FF"
    Padding="16,8"
    FontSize="14"
    CornerRadius="8"
    ... />

三十七、Style 的一个核心思想:不要在页面里重复写视觉属性

不推荐:

代码语言:javascript
复制
<Button Background="#1677FF"
        Foreground="White"
        CornerRadius="8"
        Padding="16,8"
        Content="登录"/>

<Button Background="#1677FF"
        Foreground="White"
        CornerRadius="8"
        Padding="16,8"
        Content="注册"/>

<Button Background="#1677FF"
        Foreground="White"
        CornerRadius="8"
        Padding="16,8"
        Content="继续"/>

推荐:

代码语言:javascript
复制
<Button Classes="primary"
        Content="登录"/>

<Button Classes="primary"
        Content="注册"/>

<Button Classes="primary"
        Content="继续"/>

Style:

代码语言:javascript
复制
<Style Selector="Button.primary">
    ...
</Style>

这样 UI 和视觉设计真正实现了分离。


三十八、Style 的另一个重要用途:状态管理

例如:

代码语言:javascript
复制
<Border Classes="success"
        Classes.error="{Binding HasError}">

然后:

代码语言:javascript
复制
<Style Selector="Border.success">
    <Setter Property="Background"
            Value="Green"/>
</Style>

<Style Selector="Border.error">
    <Setter Property="Background"
            Value="Red"/>
</Style>

这实际上已经不仅仅是在“设置颜色”。

而是在做:

代码语言:javascript
复制
ViewModel State
       ↓
Classes
       ↓
Style
       ↓
Visual State

这是一种非常适合 MVVM 的设计。


三十九、复杂状态不要全部塞进 Binding Converter

很多开发者遇到状态 UI 时第一反应是:

代码语言:javascript
复制
Background="{Binding Status,
    Converter={StaticResource StatusToBrushConverter}}"

当然可以。

但如果状态本身具有明确的 UI 语义:

代码语言:javascript
复制
success
warning
error
selected
active
disabled

那么 Class 往往更直观:

代码语言:javascript
复制
Classes.success="{Binding IsSuccess}"
Classes.error="{Binding IsError}"

然后:

代码语言:javascript
复制
<Style Selector="Border.success">
    ...
</Style>

<Style Selector="Border.error">
    ...
</Style>

这样:

代码语言:javascript
复制
业务状态

和:

代码语言:javascript
复制
视觉状态

之间的关系更加明显。


四十、自定义控件还可以创建自己的 PseudoClass

这是 Avalonia Style 非常强大的地方。

假设你自己写一个:

代码语言:javascript
复制
public class StatusIndicator : TemplatedControl

你可以定义:

代码语言:javascript
复制
:active

状态。

例如:

代码语言:javascript
复制
PseudoClasses.Set(":active", IsActive);

然后 XAML:

代码语言:javascript
复制
<Style Selector="local|StatusIndicator:active">
    <Setter Property="Background"
            Value="LimeGreen"/>
</Style>

这样你的自定义控件也拥有类似:

代码语言:javascript
复制
Button:pressed
Button:pointerover
CheckBox:checked

这样的状态系统。

官方文档提供了自定义 PseudoClass 的实现方式。


四十一、Avalonia Style 的完整思维模型

到这里,可以把整个系统总结成:

代码语言:javascript
复制
                    Avalonia Styling
                           │
        ┌──────────────────┼──────────────────┐
        │                  │                  │
     Selector           Setter             Resource
        │                  │                  │
        │                  │                  └── Brush
        │                  │                      Color
        │                  │                      Thickness
        │                  │
        │                  └── Background
        │                      Foreground
        │                      Padding
        │                      FontSize
        │
        ├── Type
        │      Button
        │
        ├── Class
        │      .primary
        │
        ├── PseudoClass
        │      :pointerover
        │      :pressed
        │
        ├── Name
        │      #LoginButton
        │
        ├── Relationship
        │      StackPanel Button
        │      StackPanel > Button
        │
        └── Template
               /template/

而完整的视觉系统还包括:

代码语言:javascript
复制
Style
  │
  ├── 普通属性样式
  │
  ├── Class
  │
  ├── PseudoClass
  │
  ├── Template
  │
  └── ControlTheme

四十二、Style、Class、PseudoClass 一句话区分

最后可以用三句话记忆:

Style

规定控件应该长什么样。

代码语言:javascript
复制
<Style Selector="Button">

Class

我人为给控件贴了一个标签。

代码语言:javascript
复制
<Button Classes="primary"/>

PseudoClass

控件当前处于什么状态。

代码语言:javascript
复制
Button:pointerover
Button:pressed
Button:focus

因此:

代码语言:javascript
复制
<Button Classes="primary"/>

加:

代码语言:javascript
复制
<Style Selector="Button.primary">

表示:

这个 Button 是 primary 类型。

而:

代码语言:javascript
复制
<Style Selector="Button.primary:pointerover">

表示:

这个 primary Button 当前又处于 pointerover 状态。

这其实就是 Avalonia Style 最核心的设计思想。


四十三、实际开发时推荐的 Style 思路

如果是一个中大型 Avalonia 项目,可以按照下面的方式设计:

代码语言:javascript
复制
第一层:Design Tokens
│
├── PrimaryBrush
├── DangerBrush
├── SuccessBrush
├── TextBrush
└── BorderBrush
        ↓
第二层:基础 Style
│
├── Button
├── TextBox
├── TextBlock
└── Border
        ↓
第三层:语义 Class
│
├── primary
├── danger
├── success
├── warning
└── muted
        ↓
第四层:状态
│
├── :pointerover
├── :pressed
├── :focus
├── :disabled
└── :checked
        ↓
第五层:Template / ControlTheme
│
└── 对复杂控件进行深度定制

这种方式的好处是:

代码语言:javascript
复制
颜色变化
    ↓
只修改 Resource

按钮视觉变化
    ↓
修改 Button Style

按钮状态变化
    ↓
修改 PseudoClass Style

某一个业务按钮特殊
    ↓
增加 Class

整个 Button 的视觉结构变化
    ↓
ControlTheme / Template

职责非常清晰。


四十四、最后总结

Avalonia Style 表面上看起来只是:

代码语言:javascript
复制
<Style Selector="...">
    <Setter .../>
</Style>

但真正理解以后,会发现它实际上是一套完整的 UI 样式系统。

最重要的几个概念可以浓缩成:

代码语言:javascript
复制
Style
    ↓
Selector + Setter

Selector
    ↓
Type
Class
PseudoClass
Name
Hierarchy
Template

Class
    ↓
人为定义的 UI 语义

PseudoClass
    ↓
控件自身状态

Resource
    ↓
统一管理颜色、Brush、尺寸等设计资源

ControlTemplate
    ↓
决定控件内部视觉结构

ControlTheme
    ↓
完整控制一个控件的主题

如果从 WPF 转 Avalonia,最重要的思维转变就是:

代码语言:javascript
复制
WPF:
Style
 +
Trigger
 +
Template

逐渐转变为:

代码语言:javascript
复制
Avalonia:

Selector
 +
Classes
 +
PseudoClasses
 +
Template
 +
ControlTheme

而在日常业务开发中,最常用的组合其实只有:

代码语言:javascript
复制
<Button Classes="primary"/>

配合:

代码语言:javascript
复制
<Style Selector="Button.primary">

再配合:

代码语言:javascript
复制
<Style Selector="Button.primary:pointerover">

以及:

代码语言:javascript
复制
<Style Selector="Button.primary:pressed">

掌握这四种写法以后,Avalonia 绝大多数普通 UI 样式需求就已经能够覆盖。

可以把 Avalonia Style 简单理解成:用 Selector 找控件,用 Setter 改属性,用 Class 表达业务样式,用 PseudoClass 表达控件状态,用 Template/ControlTheme 控制复杂控件的内部视觉结构。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、先理解 Avalonia Style 到底是什么
  • 二、Avalonia Style 和 WPF Style 最大的区别
  • 三、Style 最重要的组成部分:Selector
  • 四、最基础的 Selector
    • 4.1 按控件类型选择
  • 五、通过 Classes 精确控制样式
  • 六、Classes 是什么?
  • 七、Classes 和 CSS Class 很像
  • 八、为什么推荐使用 Classes?
  • 九、Style Class 还可以动态添加
  • 十、条件 Class:非常实用
  • 十一、PseudoClass:Avalonia Style 的另一个核心
  • 十二、最常用的 Button 状态
  • 十三、Classes 和 PseudoClass 的区别
  • 十四、Selector 的组合
  • 十五、根据 Name 选择控件
  • 十六、后代选择器
  • 十七、直接子元素选择器
  • 十八、:is() 非常重要
  • 十九、:not() 排除某些控件
  • 二十、Template Selector
  • 二十一、为什么有时候直接设置 Button.Foreground 没有效果?
  • 二十二、Nested Style:嵌套 Style
  • 二十三、为什么 Nested Style 很有用?
  • 二十四、Style 的作用域
  • 二十五、App.axaml 是全局 Style 的常用位置
  • 二十六、推荐把大型项目的 Style 拆文件
  • 二十七、Style 和 Resource 不要混淆
  • 二十八、推荐使用 Resource 管理颜色
  • 二十九、StaticResource 和 DynamicResource 怎么选?
  • 三十、Style、Resource、ControlTheme 到底有什么区别?
  • 三十一、Style 和 ControlTheme 不要混为一谈
  • 三十二、Selector 优先级
  • 三十三、可以建立这样的理解
  • 三十四、不要滥用 *
  • 三十五、一个完整的 Button Style
  • 三十六、一个实际项目中的 Style 架构
  • 三十七、Style 的一个核心思想:不要在页面里重复写视觉属性
  • 三十八、Style 的另一个重要用途:状态管理
  • 三十九、复杂状态不要全部塞进 Binding Converter
  • 四十、自定义控件还可以创建自己的 PseudoClass
  • 四十一、Avalonia Style 的完整思维模型
  • 四十二、Style、Class、PseudoClass 一句话区分
    • Style
    • Class
    • PseudoClass
  • 四十三、实际开发时推荐的 Style 思路
  • 四十四、最后总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档