
在 Avalonia 开发中,Style 是一个非常重要、同时也很容易让初学者困惑的机制。
如果你之前主要使用过 WPF,那么刚开始接触 Avalonia Style 时,很容易产生这样的疑问:
Style 和 WPF 的 Style 到底是不是一回事?Selector?Classes="primary" 到底有什么作用?Button.primary:pointerover 是什么意思?Trigger?:pointerover、:pressed、:focus 到底从哪里来的?Background,最终到底听谁的?Style、ControlTheme、Resource、Template 到底应该怎么区分?如果把这些问题弄明白,Avalonia 的 UI 样式系统其实非常清晰。
Avalonia 的 Style 设计理念更接近 CSS,而不是传统 WPF 的 Style + Trigger 模式。官方文档也明确将 Avalonia Style 描述为一种类似 CSS 的样式机制,而 WPF/UWP 中更接近传统控件主题的概念,在 Avalonia 中对应的是 ControlTheme。
本文从基础开始,一直到实际项目中的样式架构。
最简单地说:
Avalonia Style 就是:找到符合条件的控件,然后给它设置属性。
可以把它抽象成:
Selector
↓
找到符合条件的 Control
↓
Setter
↓
修改 Control 的属性最简单的 Style:
<Style Selector="Button">
<Setter Property="Background" Value="Blue"/>
<Setter Property="Foreground" Value="White"/>
</Style>它表达的意思就是:
找到所有
Button,把它们的Background设置成蓝色,把Foreground设置成白色。
这里其实只有两个核心概念:
Selector = 找谁
Setter = 找到以后改什么所以一个 Style 最核心的结构就是:
<Style Selector="选择器">
<Setter Property="属性" Value="值"/>
</Style>例如:
<Style Selector="TextBlock">
<Setter Property="FontSize" Value="16"/>
<Setter Property="Foreground" Value="Gray"/>
</Style>意思就是:
所有 TextBlock
↓
FontSize = 16
Foreground = GrayAvalonia 官方文档也将 Style 的工作过程概括为两个阶段:
如果你从 WPF 转 Avalonia,这一点非常重要。
WPF 中经常这么写:
<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 的思路是:
Style
├── Setter
└── Trigger
└── Setter而 Avalonia 更倾向于:
<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 的关键。
如果只能记住一个东西,我建议记住:
Avalonia Style 的灵魂是 Selector。
例如:
<Style Selector="Button">表示:
所有 Button而:
<Style Selector="Button.primary">表示:
所有具有 primary class 的 Button再例如:
<Style Selector="Button.primary:pointerover">表示:
具有 primary class
并且当前鼠标悬停
的 Button所以可以把 Selector 理解成:
Button
+
.primary
+
:pointerover三个条件组合起来。
Avalonia 的 Selector 语法整体上与 CSS 很相似。
最简单:
<Style Selector="Button">
<Setter Property="FontSize" Value="16"/>
</Style>匹配:
<Button Content="确定"/>
<Button Content="取消"/>
<Button Content="保存"/>这些 Button 都会受到影响。
这是 Avalonia Style 最重要的功能之一。
例如:
<Button Classes="primary"
Content="保存"/>然后:
<Style Selector="Button.primary">
<Setter Property="Background" Value="#1677FF"/>
<Setter Property="Foreground" Value="White"/>
</Style>此时:
<Button Classes="primary"/>会匹配。
但是:
<Button/>不会匹配。
所以:
Button代表控件类型。
而:
.primary代表 Style Class。
最终:
Button.primary就是:
类型是 Button,并且存在 primary class。
Avalonia 控件拥有一个 Classes 集合。
例如:
<Button Classes="primary large">实际上可以理解为:
Classes
├── primary
└── large因此:
<Style Selector="Button.primary">匹配。
而:
<Style Selector="Button.large">也匹配。
甚至:
<Style Selector="Button.primary.large">也匹配。
最后这个条件意味着:
Button 必须同时拥有
primary和large两个 Class。
官方 Selector 文档也明确规定,多 Class 选择器要求控件同时拥有这些 Class。
如果你熟悉 Web 开发,可以直接这样理解:
<button class="primary large">对应 Avalonia:
<Button Classes="primary large"/>CSS:
button.primary {
background: blue;
}Avalonia:
<Style Selector="Button.primary">
<Setter Property="Background" Value="Blue"/>
</Style>因此对于 Web 开发者来说,Avalonia Style 是比较容易理解的。
假设你有 20 个 Button。
其中:
15 个普通按钮
3 个主要按钮
2 个危险按钮你可以写:
<Button Classes="primary"/>
<Button Classes="danger"/>然后:
<Style Selector="Button.primary">
<Setter Property="Background" Value="#1677FF"/>
</Style>
<Style Selector="Button.danger">
<Setter Property="Background" Value="#FF4D4F"/>
</Style>这样比给每一个 Button 单独设置:
Background="..."
Foreground="..."
BorderBrush="..."更加容易维护。
Classes 不仅可以在 XAML 中设置。
C# 中同样可以操作:
button.Classes.Add("primary");删除:
button.Classes.Remove("primary");切换:
button.Classes.Toggle("primary");判断:
if (button.Classes.Contains("primary"))
{
// ...
}官方文档也提供了这些 API。
这意味着你可以在程序运行过程中动态改变控件状态。
例如:
if (isError)
{
statusText.Classes.Add("error");
}
else
{
statusText.Classes.Add("success");
}然后:
<Style Selector="TextBlock.error">
<Setter Property="Foreground" Value="Red"/>
</Style>
<Style Selector="TextBlock.success">
<Setter Property="Foreground" Value="Green"/>
</Style>Avalonia 甚至支持:
<Button Classes.accent="{Binding IsSpecial}">当:
IsSpecial = true相当于:
Classes="accent"当:
IsSpecial = false则不会拥有这个 Class。
官方文档将这种方式称为 Conditional Classes。
例如:
<Border Classes.error="{Binding HasError}">配合:
<Style Selector="Border.error">
<Setter Property="BorderBrush" Value="Red"/>
</Style>非常适合 MVVM。
如果说:
Classes代表:
我人为定义的状态。
那么:
PseudoClasses可以理解为:
控件自身产生的状态。
例如:
:pointerover
:pressed
:focus
:disabled
:checked等等。
因此:
<Style Selector="Button:pointerover">表示:
当 Button 处于鼠标悬停状态时应用这个 Style。
官方文档列举了 :pointerover、:focus、:disabled、:pressed、:checked 等常见伪类。
一个比较完整的 Button 样式可以这样写:
<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>对应的状态关系就是:
Button.primary
│
┌────────────┼────────────┐
↓ ↓ ↓
:pointerover :pressed :disabled
│ │ │
悬停颜色 按下颜色 禁用效果这就是 Avalonia 中非常典型的状态样式设计。
这个问题非常重要。
例如:
<Button Classes="primary"/>这里:
primary是你定义的 Class。
而:
Button:pointerover这里:
:pointerover是控件当前产生的 PseudoClass。
可以简单记成:
类型 | 示例 | 来源 |
|---|---|---|
Class | .primary | 开发者定义 |
Class | .danger | 开发者定义 |
PseudoClass | :pointerover | 控件状态 |
PseudoClass | :pressed | 控件状态 |
PseudoClass | :focus | 控件状态 |
PseudoClass | :checked | 控件状态 |
因此:
Button.primary:pointerover就是:
Button
+
primary Class
+
pointerover PseudoClassAvalonia Selector 可以组合多个条件。
例如:
<Style Selector="Button.primary:pointerover">可以拆成:
Button
↓
类型
.primary
↓
Class
:pointerover
↓
PseudoClass再比如:
<Style Selector="Button.primary.large:focus">要求:
Button
AND primary
AND large
AND focus全部成立。
也可以通过 Name:
<Button Name="LoginButton"/>然后:
<Style Selector="#LoginButton">
<Setter Property="Background" Value="Green"/>
</Style>也可以:
<Style Selector="Button#LoginButton">
<Setter Property="Background" Value="Green"/>
</Style>其中:
#表示 Name。
官方 Selector 语法中也支持 #name 形式。
不过在实际项目中,我一般不建议大量依赖 Name Selector。
如果是为了表达:
这是主要按钮
这是危险按钮
这是警告文本更适合:
Classes="primary"
Classes="danger"
Classes="warning"而不是:
Name="Button1"
Name="Button2"Avalonia 还支持类似 CSS 的层级选择。
例如:
<Style Selector="StackPanel Button">
<Setter Property="Margin" Value="5"/>
</Style>意思是:
StackPanel内部的 Button。
不要求 Button 是直接子元素。
例如:
<StackPanel>
<Border>
<Button/>
</Border>
</StackPanel>这个 Button 依然可以匹配。
官方 Selector 文档将这种形式称为 descendant selector。
如果只希望匹配直接子元素,可以使用:
<Style Selector="StackPanel > Button">例如:
<StackPanel>
<Button/>
<Border>
<Button/>
</Border>
</StackPanel>那么:
第一个 Button
↓
匹配
第二个 Button
↓
不匹配因为第二个 Button 中间隔了一层 Border。
:is() 非常重要Avalonia 中还提供:
:is(Button)它和简单的:
Button有一个重要区别。
普通:
Button主要匹配 Button 的 StyleKey。
而:
:is(Button)可以匹配 Button 以及派生类型。
例如:
<Style Selector=":is(Button)">
<Setter Property="FontSize" Value="16"/>
</Style>对于自定义 Button 派生控件也更有意义。
官方文档也特别说明,普通类型选择器不会匹配派生类型,而 :is() 可以用于这种场景。
:not() 排除某些控件例如:
<Style Selector="Button:not(.primary)">
<Setter Property="Background" Value="Gray"/>
</Style>可以理解为:
Button
但是没有 primary class因此:
<Button/>匹配。
而:
<Button Classes="primary"/>不匹配。
这是很多 Avalonia 开发者第一次接触时最容易懵的地方。
例如:
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="Red"/>
</Style>这里:
/template/表示:
进入控件的 ControlTemplate 内部进行选择。
也就是说:
Button
↓
:pointerover
↓
进入 Template
↓
找到 ContentPresenter然后修改 Template 内部的控件。
官方 Selector 文档也将 /template/ 作为专门的 Template selector。
这个功能对于修改:
等复杂控件的内部视觉结构非常重要。
这是 Avalonia Style 中非常常见的问题。
例如:
<Style Selector="Button">
<Setter Property="Foreground" Value="Red"/>
</Style>你发现:
为什么 Button 里面的文字没有变红?
原因很可能是:
Button
↓
ControlTemplate
↓
ContentPresenter
↓
TextBlock真正显示文字的可能不是 Button 本身,而是 Template 内部的元素。
所以你可能需要:
<Style Selector="Button /template/ ContentPresenter">
<Setter Property="Foreground" Value="Red"/>
</Style>或者根据具体 Theme 的模板结构进行针对性处理。
因此:
Avalonia Style 不仅可以修改控件本身,也可以深入 Template 修改内部控件。
Avalonia 支持嵌套 Style。
例如:
<Style Selector="TextBlock.title">
<Setter Property="FontSize" Value="24"/>
<Setter Property="FontWeight" Value="Bold"/>
<Style Selector="^:pointerover">
<Setter Property="Foreground" Value="Red"/>
</Style>
</Style>这里:
^非常重要。
它表示:
当前 Style 的父 Selector。
因此:
<Style Selector="^:pointerover">实际上相当于:
<Style Selector="TextBlock.title:pointerover">官方文档也明确要求嵌套 Style 使用 ^ 作为嵌套选择器的起始部分。
例如一个按钮:
<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>相比:
<Style Selector="Button.primary"/>
<Style Selector="Button.primary:pointerover"/>
<Style Selector="Button.primary:pressed"/>嵌套写法能够把一个组件的所有状态集中在一起。
对于复杂 UI,非常推荐这种写法。
Style 不一定只能写在 App.axaml。
可以写在:
<Application.Styles>也可以写在:
<Window.Styles>或者:
<UserControl.Styles>甚至某些 Control 的:
<StackPanel.Styles>例如:
<Window>
<Window.Styles>
<Style Selector="Button">
<Setter Property="FontSize" Value="16"/>
</Style>
</Window.Styles>
</Window>那么这个 Style 会作用于:
Window
├── Button
├── StackPanel
│ └── Button
└── UserControl
└── Button也就是说,Style 的定义位置会影响它的作用范围。
Avalonia 官方文档明确说明,Application 级 Style 可以用于整个应用,而 Window/UserControl 等位置定义的 Style 则具有相应的局部作用域。
例如:
<Application.Styles>
<Style Selector="Button.primary">
<Setter Property="Background" Value="#1677FF"/>
</Style>
</Application.Styles>整个应用都可以:
<Button Classes="primary"/>这非常适合定义:
全局按钮
全局字体
全局颜色
全局输入框
全局间距
全局状态不要把所有 Style 都堆在:
App.axaml里面。
可以拆成:
Styles/
├── Button.axaml
├── TextBox.axaml
├── TextBlock.axaml
├── Card.axaml
├── Navigation.axaml
├── Dialog.axaml
└── Colors.axaml然后通过:
<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>这样项目规模大起来以后会舒服很多。
这是另外一个非常重要的概念。
Resource:
<Application.Resources>
<SolidColorBrush x:Key="PrimaryBrush"
Color="#1677FF"/>
</Application.Resources>Style:
<Application.Styles>
<Style Selector="Button.primary">
<Setter Property="Background"
Value="{DynamicResource PrimaryBrush}"/>
</Style>
</Application.Styles>两者职责不同:
Resource
↓
存放可以复用的东西
Style
↓
规定控件应该怎么样Resource 可以保存:
Brush
Color
Thickness
Double
String
Style
DataTemplate
ControlTheme等可复用对象。
官方资源文档也将 Brush、Color、Thickness、String 和 Style 等列为常见资源。
例如:
<Application.Resources>
<SolidColorBrush x:Key="PrimaryBrush"
Color="#1677FF"/>
<SolidColorBrush x:Key="DangerBrush"
Color="#FF4D4F"/>
<SolidColorBrush x:Key="SuccessBrush"
Color="#52C41A"/>
</Application.Resources>然后:
<Style Selector="Button.primary">
<Setter Property="Background"
Value="{DynamicResource PrimaryBrush}"/>
</Style>这样以后修改主题颜色,只需要改:
PrimaryBrush而不需要搜索整个项目里的:
#1677FF如果资源可能随着 Theme 改变,通常更适合:
DynamicResource例如:
Background="{DynamicResource PrimaryBrush}"如果资源不会发生变化,也可以使用:
StaticResource在需要支持:
Light Theme
Dark Theme的时候,DynamicResource 尤其重要。
这是 Avalonia 中非常值得建立的一张概念图:
Resource
│
└── 保存可复用资源
├── Brush
├── Color
├── Thickness
└── 其他对象
Style
│
└── 根据 Selector 给控件设置属性
ControlTheme
│
└── 定义一个控件的完整主题/视觉结构
ControlTemplate
│
└── 定义控件内部视觉树可以简单理解为:
Resource
= 我有什么材料?
Style
= 哪些控件应该用什么材料?
ControlTemplate
= 控件内部长什么样?
ControlTheme
= 这个控件完整的一套主题是什么?Avalonia 当前官方文档将 Styles、Control Themes 和 Container Queries 作为不同层次的样式机制。
这是从 WPF 转 Avalonia 时非常容易产生的误区。
Avalonia:
<Style Selector="Button.primary">更接近:
CSS Style而:
<ControlTheme TargetType="Button">才更加接近传统 WPF:
<Style TargetType="Button">所承担的完整控件主题职责。
所以:
普通样式
→ Style
完整控件主题
→ ControlTheme如果你只是想:
把这个 Button 变蓝不要一上来就写 ControlTheme。
这是 Style 最容易出现“为什么我的样式不生效”的地方。
假设:
<Style Selector="Button">
<Setter Property="Background" Value="Blue"/>
</Style>然后:
<Style Selector="Button.primary">
<Setter Property="Background" Value="Green"/>
</Style>再:
<Style Selector="Button.primary:pointerover">
<Setter Property="Background" Value="Red"/>
</Style>那么:
Button
↓
Blue
Button.primary
↓
Green
Button.primary:pointerover
↓
Red因为后面的 Selector 更具体。
官方文档对 Selector specificity 有明确说明:Name Selector、属性/伪类、Style Class、Type Selector 等具有不同的优先级;当匹配条件相同时,后声明的样式可以覆盖前面的样式。
从一般到具体:
Button
↓
Button.primary
↓
Button.primary:pointerover
↓
Button#myButton越具体,通常越适合解决局部问题。
因此推荐:
先写通用 Style
↓
再写 Class Style
↓
再写状态 Style
↓
最后处理特殊控件例如:
<Style Selector="Button">
...
</Style>
<Style Selector="Button.primary">
...
</Style>
<Style Selector="Button.primary:pointerover">
...
</Style>这种结构比大量:
Button#xxx
Button#xxx2
Button#xxx3更容易维护。
*虽然可以使用非常宽泛的 Selector,但实际项目中要谨慎。
例如:
<Style Selector="*">这种样式非常容易影响大量控件。
例如:
<Style Selector="*">
<Setter Property="Margin" Value="10"/>
</Style>可能产生大量你没有预料到的布局问题。
官方最佳实践也建议避免过于宽泛的 Selector,例如 * 或 :is(Control),因为它们可能匹配整个控件树并造成意外副作用。
现在把前面讲的东西组合起来。
<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>使用:
<Button Classes="primary"
Content="保存"/>
<Button Classes="danger"
Content="删除"/>这就是比较标准的 Avalonia Style 写法。
如果你使用 Avalonia 开发一个比较完整的桌面软件,例如:
毕业搭子或者其他类似的桌面客户端,我会更推荐这种架构:
Styles
│
├── Colors.axaml
├── Typography.axaml
├── Button.axaml
├── TextBox.axaml
├── Card.axaml
├── Navigation.axaml
├── Dialog.axaml
├── List.axaml
└── Common.axaml然后:
Colors
↓
定义颜色
Typography
↓
定义字体
Button
↓
定义按钮
Card
↓
定义卡片
Navigation
↓
定义导航
Dialog
↓
定义弹窗这样你的 View XAML 就可以非常干净:
<Button Classes="primary"
Content="开始改写"/>而不是:
<Button
Background="#1677FF"
Foreground="White"
BorderBrush="#1677FF"
Padding="16,8"
FontSize="14"
CornerRadius="8"
... />不推荐:
<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="继续"/>推荐:
<Button Classes="primary"
Content="登录"/>
<Button Classes="primary"
Content="注册"/>
<Button Classes="primary"
Content="继续"/>Style:
<Style Selector="Button.primary">
...
</Style>这样 UI 和视觉设计真正实现了分离。
例如:
<Border Classes="success"
Classes.error="{Binding HasError}">然后:
<Style Selector="Border.success">
<Setter Property="Background"
Value="Green"/>
</Style>
<Style Selector="Border.error">
<Setter Property="Background"
Value="Red"/>
</Style>这实际上已经不仅仅是在“设置颜色”。
而是在做:
ViewModel State
↓
Classes
↓
Style
↓
Visual State这是一种非常适合 MVVM 的设计。
很多开发者遇到状态 UI 时第一反应是:
Background="{Binding Status,
Converter={StaticResource StatusToBrushConverter}}"当然可以。
但如果状态本身具有明确的 UI 语义:
success
warning
error
selected
active
disabled那么 Class 往往更直观:
Classes.success="{Binding IsSuccess}"
Classes.error="{Binding IsError}"然后:
<Style Selector="Border.success">
...
</Style>
<Style Selector="Border.error">
...
</Style>这样:
业务状态和:
视觉状态之间的关系更加明显。
这是 Avalonia Style 非常强大的地方。
假设你自己写一个:
public class StatusIndicator : TemplatedControl你可以定义:
:active状态。
例如:
PseudoClasses.Set(":active", IsActive);然后 XAML:
<Style Selector="local|StatusIndicator:active">
<Setter Property="Background"
Value="LimeGreen"/>
</Style>这样你的自定义控件也拥有类似:
Button:pressed
Button:pointerover
CheckBox:checked这样的状态系统。
官方文档提供了自定义 PseudoClass 的实现方式。
到这里,可以把整个系统总结成:
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/而完整的视觉系统还包括:
Style
│
├── 普通属性样式
│
├── Class
│
├── PseudoClass
│
├── Template
│
└── ControlTheme最后可以用三句话记忆:
规定控件应该长什么样。
<Style Selector="Button">我人为给控件贴了一个标签。
<Button Classes="primary"/>控件当前处于什么状态。
Button:pointerover
Button:pressed
Button:focus因此:
<Button Classes="primary"/>加:
<Style Selector="Button.primary">表示:
这个 Button 是 primary 类型。
而:
<Style Selector="Button.primary:pointerover">表示:
这个 primary Button 当前又处于 pointerover 状态。
这其实就是 Avalonia Style 最核心的设计思想。
如果是一个中大型 Avalonia 项目,可以按照下面的方式设计:
第一层: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
│
└── 对复杂控件进行深度定制这种方式的好处是:
颜色变化
↓
只修改 Resource
按钮视觉变化
↓
修改 Button Style
按钮状态变化
↓
修改 PseudoClass Style
某一个业务按钮特殊
↓
增加 Class
整个 Button 的视觉结构变化
↓
ControlTheme / Template职责非常清晰。
Avalonia Style 表面上看起来只是:
<Style Selector="...">
<Setter .../>
</Style>但真正理解以后,会发现它实际上是一套完整的 UI 样式系统。
最重要的几个概念可以浓缩成:
Style
↓
Selector + Setter
Selector
↓
Type
Class
PseudoClass
Name
Hierarchy
Template
Class
↓
人为定义的 UI 语义
PseudoClass
↓
控件自身状态
Resource
↓
统一管理颜色、Brush、尺寸等设计资源
ControlTemplate
↓
决定控件内部视觉结构
ControlTheme
↓
完整控制一个控件的主题如果从 WPF 转 Avalonia,最重要的思维转变就是:
WPF:
Style
+
Trigger
+
Template逐渐转变为:
Avalonia:
Selector
+
Classes
+
PseudoClasses
+
Template
+
ControlTheme而在日常业务开发中,最常用的组合其实只有:
<Button Classes="primary"/>配合:
<Style Selector="Button.primary">再配合:
<Style Selector="Button.primary:pointerover">以及:
<Style Selector="Button.primary:pressed">掌握这四种写法以后,Avalonia 绝大多数普通 UI 样式需求就已经能够覆盖。
可以把 Avalonia Style 简单理解成:用 Selector 找控件,用 Setter 改属性,用 Class 表达业务样式,用 PseudoClass 表达控件状态,用 Template/ControlTheme 控制复杂控件的内部视觉结构。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。