目录
- 1. 简介
- 2. 与WPF的对比差异
- 2.1 样式
- 2.1.1 样式
- 2.1.2 控件主题(ControlTheme)
- 2.2 主题变体
- 2.3 资产管理(图片、字体等)
- 2.4 动画与过渡效果
- 2.5 文件对话框
- 2.6 MessageBox弹框
- 2.7 编译绑定,如果换DataContext会不会报错?wpf有吗?
- 2.7.1 在某个Control或Window中启用编译绑定
- 2.7.2 全局启用
- 2.7.3 CompiledBinding与ReflectionBinding
- 2.8 绑定
- 2.8.1 异步绑定
- 2.8.2 直接绑定到方法
- 2.8.3 绑定到控件
- 2.9 资源的定义和合并
- 2.10 访问UI线程
- 2.10 创建自定义控件
- 2.11 获取Window
- 2.12 顶级控件 TopLevel
- 3. 跨平台的实现原理
- 3.1 架构
- 3.2 跨平台开发
- 3.2.1 在C#中进行跨平台开发
- 3.2.2 在Xaml中进行跨平台开发
- 4. 调试
- 5. 一个冷知识
1. 简介
Avalonia 是 WPF 的强大替代方案,它从头开始设计为跨平台,同时提供与 WPF 非常相似的开发体验。如果您是 XAML 和 MVVM 方面的专家,那么使用 Avalonia 开发应用程序时,您会感到宾至如归。它的日益普及反映了开发人员的优先事项向跨操作系统运行的强大解决方案的转变。
IDE支持
- 使用目前已免费的JetBrains Rider
- 或者安装VisualStudio2022插件: Avalonia for Visual Studio
支持的.NET版本
- .NET Framework 4.6.2+
- .NET Core 2.0+
- .NET 5+ (包括最新的 .NET 8)
2. 与WPF的对比差异
(▲WPF的类结构图)
(▲Avalonia的类结构图)
WPF中的UIElement
和FrameworkElement
是非模板控件的基类,大致对应于Avalonia中的Control
类。而WPF中的Control
类则是一个模板控件,Avalonia中相应的类是TemplatedControl
。
在WPF/UWP中,您将从Control
类继承来创建新的模板控件,而在Avalonia中,您应该从TemplatedControl
继承。
在WPF/UWP中,您将从FrameworkElement
类继承来创建新的自定义绘制控件,而在Avalonia中,您应该从Control
继承。
因此,简要总结如下:
-
UIElement
🠞Control
-
FrameworkElement
🠞Control
-
Control
🠞TemplatedControl
2.1 样式
样式是Avalonia与WPF最大的差别,Avalonia为控件提供了两种主要的样式机制:样式和主题。
2.1.1 样式
样式(Styles)类似于CSS样式,通常用于根据控件的内容或在应用程序中的用途对控件进行样式化。例如,创建用于标题文本块的样式。
工作原理
样式机制有两个步骤:选择和替换。样式的 XAML 可以定义如何进行这两个步骤,但通常你会在控件元素上定义 Classes
标签来帮助选择步骤。
在选择步骤中,样式系统从控件开始沿着逻辑树向上搜索(与WPF一致)。这意味着在应用程序的最高级别(例如 App.axaml
文件)定义的样式可以在应用程序的任何地方使用,但仍然可以在控件更近的地方(例如在窗口或用户控件中)进行覆盖。
当选择步骤找到匹配项时,匹配的控件的属性将根据样式中的设置器进行更改。
示例:
样式选择器实例:
样式选择器 | 描述 |
| 选择所有Button类型的控件 |
| 选择所有自定义的类型为 |
| 选择所有 |
| 选择所有 |
| 选择所有获得焦点的 |
| 选择所有 |
| 选择 |
| 选择所有带有 |
| 选择所有带有 |
| 选择所有在 |
样式选择器完整语法:https://docs.avaloniaui.net/zh-Hans/docs/reference/styles/style-selector-syntax#nesting
使用:
样式键
样式选择器匹配的对象的类型不是由控件的具体类型决定的,而是通过检查其 StyleKey
属性来确定的。
默认情况下,StyleKey
属性返回当前实例的类型。然而,如果你希望你的控件(继承自 Button
)被样式化为一个按钮,你可以在你的类中重写 StyleKeyOverride
属性,并让它返回 typeof(Button)
。
条件样式
如果你需要使用绑定条件添加或删除样式,则可以使用以下特殊语法:
当绑定的IsEnableH1
为true
时,TextBlock
就会应用h1
的样式。当为false
时,又应用了h2
样式。
也可以通过代码添加和删除样式:
2.1.2 控件主题(ControlTheme)
控件主题是在样式的基础上构建的,用于为控件创建可切换的主题。控件主题类似于WPF的Style
,但机制略有不同。通常用于对控件应用主题。
控件主题本质上是样式,但有一些重要的区别:
- 控件主题没有选择器:它们有一个
TargetType
属性,用于描述它们要针对的控件。 - 控件主题存储在
ResourceDictionary
中,而不是Styles
集合中。 - 控件主题通过设置
Theme
属性来分配给控件,通常使用{StaticResource}
标记扩展。
示例主题:
使用主题:
如何将主题应用到控件的所有实例?将x:Key
设置为x:Type
而非某个字符串即可:
TargetTypeControlTheme.TargetType
属性指定适用 Setter
属性的类型。如果您没有指定 TargetType
,则必须使用类名限定 Setter
对象中的属性,使用 Property="ClassName.Property"
的语法。例如,不要设置 Property="FontSize"
,而应该设置 Property
为 TextBlock.FontSize
或 Control.FontSize
。
2.2 主题变体
主题变体(theme variant)指的是基于选择的主题而具有的特定视觉外观的控件。可以理解为主题中的子主题,如某个主题中提供了Dark
和Light
两套子主题,这两套就称之为变体。Avalonia 内置的主题 SimpleTheme
和 FluentTheme
无需额外代码即可无缝支持 Dark
和 Light
变体。
使用参考:https://docs.avaloniaui.net/zh-Hans/docs/guides/styles-and-resources/how-to-use-theme-variants
2.3 资产管理(图片、字体等)
基本与WPF一致。
使用代码加载图片:
2.4 动画与过渡效果
Avalonia UI 中有两种类型的动画:
- 关键帧动画:可以在时间轴上的关键帧处改变一个或多个属性的值。关键帧在时间轴上的关键点上定义。在关键帧之间使用缓动函数(默认情况下是直线插值)调整正在更改的属性。关键帧动画是一种非常灵活的动画类型。
- 过渡动画:可以改变一个单一属性的值。可以理解为简易版的关键帧动画
参考:https://docs.avaloniaui.net/zh-Hans/docs/guides/graphics-and-animation/transitions
2.5 文件对话框
通过StorageProvider
的OpenFilePickerAsync
和SaveFilePickerAsync
使用:
打开文件对话框:
保存文件对话框:
2.6 MessageBox弹框
目前还不支持,可以使用第三方:
2.7 编译绑定,如果换DataContext会不会报错?wpf有吗?
wpf的binding默认使用反射进行解析且到运行时才知道绑定的属性是否存在。为了优化这个问题,Avalonia提供了编译绑定。当启用之后,如果编译时找不到绑定的属性则报错,而且因不在使用反射系统,所以运行时更快。
2.7.1 在某个Control或Window中启用编译绑定
在节点中添加x:DataType
和x:CompileBindings="True"
即可启用。其中DataType
可以添加到节点中也可以添加到Binding
中。如:
2.7.2 全局启用
在csproj中添加以下节点:
且同样需给控件设置x:DataType
。
2.7.3 CompiledBinding与ReflectionBinding
二者用来更加灵活的控制编译绑定。
-
CompiledBinding
:将Binding
改为CompileBinding
就可以在没有开启编译绑定的情况下对某个绑定开启编译绑定。 -
ReflectionBinding
:在开启了编译绑定的前提下,对某个控件禁用编译绑定。
2.8 绑定
2.8.1 异步绑定
avalonia绑定的属性可以是异步的,即Task<T>
类型,这在请求IO时很有用:
使用:
注意绑定的ImageFromWebsite
后面多了一个^
,用来告诉Avalonia这是一个异步绑定。还支持FallbackValue
。
2.8.2 直接绑定到方法
Avalonia可以直接绑定到方法上而无需创建ICommand
:
Avalonia默认会在绑定的方法上加上Can
,用来查找是否定义了可执行方法。
2.8.3 绑定到控件
绑定到命名控件:
等同于WPF中的ElementName=xxxx
:
绑定到祖先控件:
- 使用
$parent
或$parent[0]
:绑定到父控件 - 使用
$parent[1]
:1
是索引,从0
开始。此处表示绑定到父控件的父控件。索引为2
、3
、N
时等同类似。 - 使用
$parent[Border]
:绑定到类型是Border
的父控件,往上一直找直至找到。 - 使用
$parent[local:MyControl]
:绑定到类型是local:MyControl
的自定义父控件上。 - 使用
$parent[Border;1]
:结合使用。表示当自己有很多个父Boder时,绑定到索引为1
的上。
如:
注意:
Avalonia UI 还支持 WPF/UWP 的 RelativeSource 语法,类似但并不相同。RelativeSource 在VisualTree上起作用,而此处给出的语法在LogicTree上起作用。
2.9 资源的定义和合并
参考avatoolkit
2.10 访问UI线程
与WPF类似也是使用Dispatcher
:
- 异步执行不等待,使用
Post
:Dispatcher.UIThread.Post(()=>LongRunningTask(), DispatcherPriority.Background);
- 异步执行可等待,使用
InvokeAsync
:await Dispatcher.UIThread.InvokeAsync(()=>LongRunningTask(), DispatcherPriority.Background);
2.10 创建自定义控件
有三种创建方法:
- 创建
UserControl
:与创建Window
一样,右键直接创建。 - 继承
TemplateControl
:模板化控件,属于项目间无外观的通用控件。在WPF中一般是继承Control
实现。 - 继承
Control
:基本控件。是用户界面的基础,通过重写Render
方法并使用几何图形绘制实现,TextBlock
、Image
等都是这类型的控件。在WPF中一般是继承FrameworkElement
实现。
2.11 获取Window
主窗口是在App.axaml.cs
文件中传递给ApplicationLifetime.MainWindow
属性的,如:
所以可以通过将Application.Current.ApplicationLifetime
强制转换为IClassicDesktopStyleApplicationLifetime
来随时检索它。但是在移动和浏览器平台没有窗口的概念,需要强制转换为ISingleViewApplicationLifetime
并获取其MainView
属性。
2.12 顶级控件 TopLevel
顶级控件充当视觉根,并且是所有顶级控件(包括Window
)的基类。它处理布局、样式和渲染的调度,以及跟踪客户端大小。大多数服务都通过顶级控件访问。
获取方法:
- 通过
var topLevel = TopLevel.GetTopLevel(control);
:如果返回为null
,则控件可能此时尚未被附加(初始化)。 - 使用
Window
类:因为Window
也是继承自TopLevel
,所以通过var topLevel = window;
也能拿到。
常见成员:
成员 | 说明 |
ActualTransparencyLevel | 获取平台能够提供的实际 |
ClientSize | 获取窗口的大小。 |
Clipboard | 获取平台的 |
FocusManager | 获取根的 |
FrameSize | 获取顶级控件的总大小,包括系统框架(如果有)。 |
InsetsManager | 获取平台的 |
PlatformSettings | 获取平台的 |
RendererDiagnostics | 获取一个值,指示渲染器是否应绘制特定的诊断信息。 |
RenderScaling | 获取用于渲染的缩放因子。 |
RequestedThemeVariant | 获取或设置控件(及其子元素)用于资源确定的UI主题变体。您使用ThemeVariant指定的UI主题可以覆盖应用程序级别的ThemeVariant。 |
StorageProvider | 获取平台的 |
TransparencyBackgroundFallback | 获取或设置当不支持透明度时,透明度将与之混合的IBrush。默认情况下,这是一个纯白色的画刷。 |
TransparencyLevelHint | 获取或设置TopLevel在可能的情况下应使用的WindowTransparencyLevel。接受多个值,按照回退顺序应用。例如,使用"Mica,Blur",Mica仅在支持它的平台上应用,其余平台上使用Blur。默认值是一个空数组或"None"。 |
BackRequested | 在按下物理返回按钮或请求后退导航时发生。 |
Closed | 窗口关闭时触发。 |
Opened | 窗口打开时触发。 |
ScalingChanged | 当TopLevel的缩放发生变化时发生。 |
RequestAnimationFrame(Action< TimeSpan > action) | 将回调排队,以在下一个动画刻度上调用 |
RequestPlatformInhibition(PlatformInhibitionType type, string reason) | 请求抑制PlatformInhibitionType。行为将保持抑制,直到返回值被释放。可用的PlatformInhibitionType集取决于平台。如果在不支持此类型的平台上抑制行为,则请求将不起作用。 |
TryGetPlatformHandle() | 尝试获取派生自TopLevel的控件的平台句柄。 |
3. 跨平台的实现原理
3.1 架构
Avalonia能够跨平台的第一个前提是大部分代码都基于.netstandard2.0
编写,其次是使用了Skia。
Avalonia 所有的UI控件全部由SkiaSharp绘制(Skia的.NET版本)。
Skia 是一个开源的 2D 图形引擎,用于绘制文本、形状、图像等二维内容。广泛应用于多种平台的图形界面开发中,比如 Android、Chrome 浏览器、Flutter 框架等。
Skia 可以基于多种底层 API 进行绘图,如使用OpenGL、Vulkan、Metal在GPU上绘制或者直接在 CPU 上渲染。它提供跨平台的 2D 绘图能力,屏蔽了底层硬件加速 API 的差异。
Android早期通过skia库进行2d渲染,后来加入了hwui利用opengl替换skia进行大部分渲染工作,现在开始用skia opengl替换掉之前的opengl,从p的代码结构上也可以看出,p开始skia库不再作为一个单独的动态库so,而是静态库a编译到hwui的动态库里,将skia整合进hwui,hwui调用skia opengl,也为以后hwui使用skia vulkan做铺垫。
Skia虽然支持Vulkan、Metal,但是有和好用是完全不同的,Skia目前支持最好的应该是OpenGL,剩下两个平台支持有限而且还有bug。
出于性能(动画)和稳定性上的考虑,Avalonia自己也集成了渲染器,各个平台上的支持如下:
- Windows:使用Angle->Dx11渲染,当不支持Dx11时会回退到Dx9。
- macOS/iOS:使用OpenGL和Metal(beta)
- Linux:使用OpenGL和Vulkan(X11)
- Android:OpenGL和Vulkan
- Browser:WebGL(转Canvas)
3.2 跨平台开发
3.2.1 在C#中进行跨平台开发
.NET 6 及更高版本提供了一组 API,用于在运行时获取操作系统 - OperatingSystem
。
该类常用的静态方法有:
方法 | 描述 |
| 当前操作系统是否是Windows |
| 同理 |
| 同理 |
| 同理 |
| 同理 |
| 同理 |
| 判断是否运行在某个平台上,入参可以为:Browser, Linux, FreeBSD, Android, iOS, macOS, tvOS, watchOS, Windows. |
条件编译:
.NET内置了一下条件编译符号,可以按需使用:
符号 | 说明 |
| 略 |
| 略 |
| 略 |
| 略 |
| 略 |
| 略 |
| 略 |
| 如: |
| 如 |
(很遗憾没有Linux
,也许以后会被加上)
3.2.2 在Xaml中进行跨平台开发
使用OnPlatform
指定平台:
▲不同平台显示不同的文字,默认文字为Unknown
。
▼OnPlatform
同时也是一个标签,可以单独使用:
也可以添加到资源字典中:
为了避免编写过多重复代码,可以使用Options
一次性指定多个平台:
OnPlatform
工作原理与if-else
一样是条件运行,不同于条件编译,所以编辑器会生成所有平台代码。但是Avalonia团队也对OnPlatform
做了编译优化,支持编译裁剪和特定平台发布。比如OnPlatform
里你配制了Windows
和macOS
,但是发布时你只发布Windows
平台,那么macOS
配制项就会被删除,这样也能减少生成体积。
除了OnPlatform
之外,还有一个有用的标签是OnFormFactor
。这两个很类似,区别是后者用来判断当前运行是在手机还是桌面、也没有编译优化。使用方法如下:
指定解析类型
如上所示,Tag
默认为object类型,如果不指定x:TypeArguments=Thickness
,会被识别为字符串。使用时在C#层还要再做一次处理,而指定之后C#拿到的就是Thickness
类型了
4. 调试
一般新建Avalonia
项目时,就已经默认安装了Nuget包:Avalonia.Diagnostics
。如果没有的话就手动安一下。
安装完成之后,开始调试,然后按下F12就能看到类似于WPF Snoop的监视工具,用来调试布局、绑定、排查问题。
5. 一个冷知识
不知道大家是否知道一些与WPF相关的比较知名的库为什么都要以Avalon开头,比如:Avalonia,AvalonDock,AvalonEdit,AvalonStudio。
原因就是WPF当初内部立项的时候,项目代号就是Avalon。当初的xaml标签都是以<Avalon></Avalon>
开头。
微软所有产品内部代号与发布名称映射表可参见: https://zh.wikipedia.org/zh-cn/%E5%BE%AE%E8%BD%AF%E4%BA%A7%E5%93%81%E4%BB%A3%E5%8F%B7%E5%88%97%E8%A1%A8
更多资料:
https://avaloniaui.net/blog/avalonia-platform-support-why-it-s-simple https://avaloniaui.net/blog/avalonia-ui-and-maui-something-for-everyone https://docs.avaloniaui.net/zh-Hans/docs/guides/platforms/platform-specific-code/dotnet https://docs.avaloniaui.net/zh-Hans/docs/guides/platforms/platform-specific-code/xaml