65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 AvaloniaUI 在简易示例中进行多平台 UI 编码。第一部分 - AvaloniaUI 构建块

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (65投票s)

2021 年 7 月 21 日

MIT

34分钟阅读

viewsIcon

111089

Avalonia 是一个很棒的新的多平台 UI 框架,它与 WPF 相似,但在许多方面比 WPF 更好。这是一篇入门教程。

引言

请注意,文章和示例代码均已更新,以兼容最新版本的 Avalonia - 11.0.6

为什么选择 Avalonia?

近年来,软件应用公司面临客户的压力,要求不仅提供桌面解决方案,还要提供包含大部分甚至全部桌面功能的 Web 和移动解决方案。最重要的是,要扩大客户群,为多个客户端平台(主要是 Windows、Mac 和 Linux)创建桌面应用程序总是好的。

在今年夏天发布 Avalonia 11 之前,唯一能提供(成熟度不同,取决于平台)桌面(Windows、Mac 和 Linux)、Web(WASM)和移动(Android、iOS 和 Tizen)解决方案的 XAML/C# 多平台框架是 Uno Platform

还有一些 JavaScript 和 TypeScript 解决方案,它们也有潜力用于桌面和移动端,但它们提供的功能要弱得多,性能差,稳定性差,且更依赖于平台(如下文所述)。 

Microsoft 的 MAUI - 不允许用户创建非常重要的 Web 和 Linux 桌面应用程序。此外,我听说 MAUI 的桌面功能不够稳定,不适合生产环境应用。

在当前竞争平台(主要是 Avalonia、Uno,以及程度较低的 MAUI 和 JavaScript)中,Avalonia 是最强大、性能最好、在多个平台间最统一的,正如我在 多平台 XAML/C# 神奇框架:Avalonia。将 Avalonia 与 WinUI 解决方案进行比较 以及下文中详细解释的。

为什么 Avalonia 如此受欢迎

  • 使用 Avalonia,可以构建
    • 适用于 Windows、MacOS 和多个 Linux 版本的桌面应用程序
    • 适用于 Android、iOS 和 Tizen 的移动应用
    • Web 应用程序(通过 WebAssembly)
  • 与 Uno 不同,Avalonia 代码几乎可以跨平台重用,无需平台特定的代码。 
  • Avalonia 应用程序的性能明显优于同等的 Uno/WinUI 应用程序。
  • Avalonia 已被许多知名客户测试过 - 请参阅 Avalonia 主页 了解主要客户列表。
  • Avalonia 代码的编译速度明显快于 Uno 在所有平台上的编译速度。为什么我提到编译时间?- 因为开发速度和软件学习曲线在很大程度上取决于原型开发的快速性,而这又取决于编译速度的快慢。 
  • Avalonia 是完全开源的 - 您可以在 Avalonia 源代码 上找到其源代码,该源代码是在限制最少的 MIT 许可证下发布的。
  • Avalonia 源代码易于编译(我已多次完成)和运行。因此,如果您需要测试某些 Avalonia 功能,您可以随时使用 Avalonia 源代码。 
  • Avalonia 源代码编写得很好,易于理解。
  • Avalonia 使用 .NET Standard 编写。它 100% 兼容所有版本的 .NET Core 和 .NET Framework。
  • Avalonia 由全球各地 才华横溢的人们 快速开发,他们了解客户的需求。
  • Avalonia 具有类似 WPF 的检查工具(snoop),尽管目前不如 snoop 强大,但已包含调试和修改 Avalonia 应用程序所需的关键功能。此外,考虑到 Avalonia 项目的积极变化速度,此类工具也会迅速改进。
  • Avalonia 支持 Visual Studio 和 Rider 用于创建 XAML 文件和 XAML 智能感知,尽管 Avalonia 的 XAML 智能感知仍落后于 WPF 的 XAML 智能感知。
  • WPF 和 Silverlight 框架引入了许多超越传统 OOP 编程范式的编程范例,这些范例允许理解这些范例的人员编写出更简洁、更快速的代码。在这些范例中,我列出以下几项:所有这些范例都在 Avalonia 中得到实现,其强大程度至少与 WPF 相当。
    • 视觉树和逻辑树
    • 附加属性或依赖属性,它们可以在使用的对象外部定义,并且除非被赋予非默认值,否则不会占用额外内存,并且在值更改时会触发特殊事件。
    • 附加路由事件,它们可以在触发它们的对象外部定义,并且可以在树上向上或向下传播和处理。
    • 绑定和相关的 MVVM 模式
    • 控件模板
    • 数据模板
    • 样式
    • 行为 - 通过事件修改 C# 类行为而不修改类本身。
  • Avalonia 允许创建在不同平台上外观和行为一致的应用程序,同时也支持特定平台的自定义。
  • Avalonia 通过 Avalonia Telegram 频道 或 Gitter 上的 Gitter:Avalonia on Gitter 提供不错的免费公共支持,并提供一些购买商业支持的选项,请访问 Avalonia Support
  • Avalonia 的文档相当完善。

Avalonia 的不足之处

Avalonia 的主要不足是缺乏第三方库(可能很快就会出现 - 因为我正在构建其中一个,而 Actipro 正在构建另一个)。 

此外,Avalonia 还发布了其 XPF 产品 的第一个版本,该产品不是免费的,但允许重用为 WPF 构建的第三方控件。

Avalonia 与 Web、MAUI 框架、Uno Platform 和 WinUI 在多平台开发方面的比较

与 WPF/Avalonia 编程相比,Web(JavaScript/TypeScript)开发存在以下缺点:

  • Web 开发不是组合式的。您无法创建一个由多个基本元素组成的自定义控件,使其在 Web 浏览器的不同部分显示方式完全相同。例如,您无法将 HTML5 canvas 矩形打包成一个可重用的按钮控件,使其在网页的中心和底部以相似的方式显示(无需额外定制)。相反,控件由浏览器预定义,并提供一百万种自定义方式(其中一些自定义特定于每个浏览器)。经验丰富的 WPF/Avalonia 开发人员可以极快地创建所需的 UI 控件,并具有设计人员要求的确切 UI 行为,并且该控件将与控件开发人员想要的完全一样可重用和可定制。在 Web 开发中,创建可重用的自定义控件几乎是不可能的 - 您只能定制已有的控件。
  • 上述 WPF 范例中很少有(例如,绑定和模板)也被著名的 Web 框架(如 Angular 和 React)实现,但没有一个像 WPF 和 Avalonia 中那样强大。例如,Angular 或 React 中都没有类似附加属性、自定义路由事件或绑定到树祖先的功能。
  • 如果您使用 Web,将应用程序拆分成多个窗口将是一个问题。即使是 Visual Studio Code(这是一个最先进的 TypeScript UI 应用程序)也不允许将选项卡拖出主窗口。顺便说一句,如果您使用 JavaScript 或 TypeScript 开始桌面项目,不要期望能写出接近 Visual Studio Code 的东西 - 我见过一些抱有这种期望的项目失败了。但使用 Avalonia 创建像 VSCode 这样复杂的应用程序是完全可行的。
  • Avalonia 被编译成 .NET 二进制代码,其性能明显优于 JavaScript 或 TypeScript 代码。 

将 Avalonia 与 MAUI(和 Xamarin)进行比较。

  • MAUI 和 Xamarin 不允许构建 Web 应用程序
  • MAUI(和 Xamarin)最初是为了构建移动应用程序而创建的,并未过多考虑桌面应用程序。根据我所读到的信息,MAUI 尚未准备好用于构建桌面应用程序。此外,MAUI 的所有商业第三方组件,例如 Telerik 或 DevExpress,都专注于移动开发,对桌面开发关注甚少。
  • Xamarin Forms(一个多平台产品)通常需要一些原生编程来处理 Xamarin Forms 未覆盖的功能。
  • Xamarin Forms 不是组合式的 - 其基本范例来自原生控件 - 按钮、菜单等。它们在每个平台上的显示方式也不同 - 有时这是期望的,有时则不然。
  • Xamarin 没有实现上面列出的许多 WPF 范例。

现在将 Avalonia 与 Uno(或 Windows 上的 WinUI)进行比较

  • Avalonia(如上所述)的性能明显优于 Uno,至少在 Windows、Android 和 Web 平台,或者优于 Windows 上的 WinUI。我没有测试过其他平台,但假设这也适用于它们。 
  • Avalonia 在所有平台上的编译速度都明显快于 Uno,尤其是在移动和 Web 端(从而加快了开发和调试速度)。
  • Avalonia 在不同平台上的行为差异明显少于 Uno。这是因为 Avalonia 试图尽可能多地重用各个平台的代码,而 Uno essentially 为每个平台重新实现了所有功能。 
  • Uno 是 Uno Platform 的 WinUI 实现,而 WinUI 并不提供 WPF(仅限 Windows)中许多重要的功能。Avalonia 则实现了 WPF 的所有功能及更多功能,因此 Uno (WinUI) 可以被认为是 WPF--,而 Avalonia 是 WPF++。

更详细的比较以及 XAML 开发的历史可以在 多平台 XAML/C# 神奇框架:Avalonia。将 Avalonia 与 WinUI 解决方案进行比较 中找到。

本文内容

本文及其后续文章的目的是弥补不熟悉 WPF 或 Silverlight 的人在编程 Avalonia UI 时可能存在的知识空白。更复杂的问题将在未来的文章中介绍。

本文中的所有示例均已在以下系统上进行测试:

  • Windows 10 和 11
  • Ubuntu 20.4
  • Mac Catalina

本文仅涵盖 Avalonia UI 的基础知识 - 将成为任何应用程序一部分的构建块。

  • 使用 Code Behind 创建简单的 Avalonia 应用程序。注意 - Code Behind 是构建大型应用程序最简单但也是最糟糕的方法。不要过度使用!连接 XAML 代码和 C# 代码的其他方法将在未来的文章中展示。
  • 最有用的内置控件
  • 基本元素(作为组合的基本构建块的控件)
  • 面板
  • 画笔
  • 变换

下一部分(第二部分)将涵盖以下主题:

  • 视觉树
  • 逻辑树
  • Avalonia 开发工具
  • 附加属性、样式属性和直接属性
  • 绑定

之后,我还计划涵盖:

  • 数据模板ItemsControl 和ContentPresenter
  • 从 XAML 调用 C# 代码(Commands 和CallAction 行为)
  • XAML(通过 Markup Extensions 重用 XAML)
  • 样式
  • 动画

接下来的文章将涵盖更复杂的主题,例如:

  • 自定义(无样式)控件
  • 控件模板
  • 行为
  • IoC 与 Avalonia
  • 安排 Avalonia 项目以实现原型驱动开发。

如何阅读本文

本文旨在成为一个实践性强的 Avalonia 教程,同时强调 Avalonia 的基本功能。其中充满了代码示例,我建议您在计算机上构建并运行它们。如果您想深入学习 Avalonia 功能,还应该尝试创建自己的项目和示例,类似于本文中提供的示例。

阅读本文和完成练习不需要任何 WPF 背景。

本文中的所有示例均使用 Visual Studio 2022、.NET 6.0 和 C# 9.0 创建,但可以轻松降级到早期版本的 .NET 和 C#。

使用 Visual Studio 2022 创建和运行简单的 Avalonia 项目

首先,为了使用 Avalonia,您需要使用位于“工具”或(对于 VS2022)“扩展”菜单下的 VS 扩展管理器安装“Avalonia for Visual Studio”扩展。在线查找此扩展并安装它。该扩展包含各种 Avalonia 相关 Visual Studio 项目的模板、Avalonia 特定的文件类型以及 Avalonia XAML 文件(与 WPF XAML 文件略有不同)的智能感知。

安装“Avalonia for Visual Studio”扩展后,启动 Visual Studio 并选择“Avalonia Application”项目类型。

按“**Next**”按钮,选择项目的目录和名称,然后按“**Create**”按钮。

创建的项目将依赖三个 Avalonia 包 - AvaloniaAvalonia.Desktop 和Avalonia.Diagnostics

还创建了五个包含代码的文件 - App.axamlApp.axaml.csMainWindow.axamlMainWindow.axaml.cs 和Program.cs

扩展名为“*.axaml”的文件是将 XAML 文件重命名为“*.axaml”,显然是为了将其与 WPF/UWP 的“*.xaml”文件区分开。Avalonia XAML 语法与 WPF XAML 语法非常相似,但在所谓的 Style Selectors(将在未来的文章中解释)方面有一些特殊之处。

在上述五个文件中,您可能需要修改最多的文件是 MainWindow.axamlMainWindow.axaml.cs,然后可能还会稍微修改 App.axamlApp.axaml.cs 文件,而 Program.cs 文件您可能永远不需要更改。

您可以运行空窗口,但如果将一些代码放在 MainWindow 中会更有趣。

打开 MainWindow.xaml 文件,将其内容(默认为“Welcome to Avalonia!”文本)替换为以下代码:

<Button x:Name="CloseWindowButton"
        Content="Close Window"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        Padding="10,5"/>

如果您现在运行应用程序,窗口中间将有一个按钮(这是通过 Horizontal 和 Vertical Alignments 实现的 - 按钮的文本(Button 的内容)将是“**Close Window**”,并且“**Close Button**”文本与按钮的侧边之间的边距为右侧和左侧 10 个通用像素,顶部和底部 5 个通用像素(由Padding 属性指定)。

到目前为止一切顺利,但如果您按下按钮,什么也不会发生。我们需要尝试将按钮单击事件连接到关闭窗口的操作。在此第一个示例中,我们将采用最简单但也是**最糟糕**的方法来实现此目的 - Code Behind。文件 MainWindow.xaml.cs 包含 MainWindow.xaml 文件的“code behind” - C# 代码。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
#if DEBUG
        this.AttachDevTools();
#endif
    }

    private void InitializeComponent()
    {
        AvaloniaXamlLoader.Load(this);
    }
}

我们将按钮命名为“CloseWindowButton”。在 WPF 中,将生成相应的类成员。Avalonia 尚未包含此功能,但我们可以通过添加以下行轻松找到按钮:

var button = this.FindControl<Button>("CloseWindowButton")

在构造函数中调用 InitializeComponent(); 之后。

然后我们可以为按钮的Click 事件添加一个处理程序:button.Click += Button_Click;

最后,在处理程序中,我们可以调用窗口上的Close() 方法。

private void Button_Click(object? sender, RoutedEventArgs e)
{
    this.Close();
}

您必须在文件顶部有一个 using Avalonia.Interactivity; 语句,才能引用 RoutedEventArgs

MainWindow 构造函数和处理程序的完整代码如下所示:

public MainWindow()
{
    InitializeComponent();
#if DEBUG
    this.AttachDevTools();
#endif
    var button = this.FindControl<Button>("CloseWindowButton");

    button.Click += Button_Click;
}

private void Button_Click(object? sender, RoutedEventArgs e)
{
    this.Close();
}

现在,如果您运行应用程序并按下 **Close** 按钮,窗口将关闭。

此应用程序的代码位于 NP.Demos.SimpleAvaloniaProject.sln 下,但如果您是 Avalonia 新手,务必完成上述练习。

一些 Avalonia 内置控件

Avalonia 控件简介

在本节中,我将介绍一些构建应用程序最有用的内置 Avalonia 控件。请注意,为了简洁起见,我不会描述每个内置控件,只描述最常用的。

如果您想了解其余内置控件,可以:

  • Github 上的 Avalonia 源代码 下载或克隆 AvaloniaUI 源代码。
  • 使用 Visual Studio 打开顶层文件夹中的 Avalonia.sln 解决方案。
  • 在解决方案资源管理器中导航到Samples Visual Studio 文件夹下的 ControlCatalog.Desktop 项目。
  • ControlCatalog.Desktop 设为启动项目,然后生成并运行它。

包含大部分(如果不是全部)内置控件的 Windows 应用程序将弹出,您将能够看到各种控件的功能。通过在Samples/Pages/ Visual Studio 文件夹下的 XAML 代码中跟踪它们,您还将能够看到如何创建和更改这些控件的属性。

新控件的创建和自定义将在未来的文章中讨论,以及最有用的控件 ContentPresenter 和ItemsControl,它们分别可以为非视觉对象或非视觉对象集合提供视觉表示。

本节的目的是概述 Avalonia 的内置控件,而不是对其功能进行详细描述。

请注意,Avalonia 中的 Control 类比 WPF 中的同名类更基本 - Avalonia Control 没有模板。具有模板的控件继承自 TemplatedControl 类,该类实现了 ITemplatedControl 接口。因此,Avalonia 的 ImageShape 和Panel 类是从 Control 派生的,而不是从 Visual 类派生的(如 WPF 中那样)。

关于模板的解释将在未来的文章中提供。

内置控件项目

在本小节中,我们将描述一些 WPF 意义上最有用的控件,即模板化控件,而不是更基本的图像、形状和面板。

这些小节的代码位于 NP.Demos.BuiltInControls.sln 解决方案下。

这是您生成并运行解决方案时将看到的:

在这里,我描述了一些我认为最有用的内置控件。这些控件的所有特定代码都位于 MainWindow.xaml 文件中。让我们逐一介绍这些控件,并描述与之相关的 XAML 代码。

TextBlock

TextBlock 是本节介绍的控件中唯一无法重新模板化的控件 - 它继承自 Control (而不是 TemplatedControl 类)。所以它更像一个基本元素,而不是 WPF 意义上的复合控件,但我把它放在这里,因为它是一个最重要的构建块,并且(在我看来)应该首先被描述。

TextBlock 表示简单的文本。其最重要的属性是 C# string 类型的 TextText 包含您要显示为 TextBlock 的文本。

TextBlock 有许多允许文本自定义的属性,包括:

  • Foreground - 文本的颜色
  • FontSize - 自解释
  • FontFamily - 指定字体名称
  • FontWeight - 通常在 Normal 和 Bold 之间切换 - 用于粗体文本
  • TextWrapping - 指定文本是否应换行到多行
  • TextTrimming - 指定当部分文本不可见时是否应显示省略号 (...) 字符 - 文本超出控件大小。

上面列出的许多属性也适用于可能包含文本的其他控件,例如按钮、菜单、ListBox 等。

创建文本块的简单 XAML 代码例如是:

<TextBlock Text="Hello World!">

文本框

这是 TextBox 示例的图片。

如果您在 TextBox 中输入任何内容,其 Text 属性将更新为输入的文本。

我使用绑定来重复 TextBox 中输入的文本,由下面的 TextBlock 显示。这是 XAML 代码:

<Grid Grid.Row="1">
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <TextBox x:Name="TheTextBox"
             Width="150"
             Height="27"
             HorizontalAlignment="Left"
             Grid.Row="0"/>
    <TextBlock Text="{Binding Path=Text, ElementName=TheTextBox}"
               Grid.Row="1"
               Height="17"/>
  </Grid>
</Grid>

我们定义了带有两行的 Grid 面板 - 文本框在顶行(Grid.Row="0"),TextBlock 在第二行(Grid.Row="1")。

我们使用 ElementName 绑定将 TextBlock 的 Text 属性绑定到 TextBox (通过其 x:Name 属性命名的“TheTextBox”)的 Text 属性:Text="{Binding Path=Text, ElementName=TheTextBox}"。这产生了文本下方重复 TextBox 中输入的文本的效果。

Button

接下来我们将介绍 Button 控件。

这是我们的按钮的代码:

<Button Content="Button"
      Padding="10,5"
      Grid.Row="1"/>

上面的 Padding="10,5" 表示按钮的内容向左和向右延伸 10 个通用像素,向上和向下延伸 5 个通用像素。Grid.Row="1" 表示按钮放置在 Grid 的第二行(第一行由标题占用)。

按钮定义了 Click 路由事件 - 当按钮被单击时触发。有三种方法可以在按钮单击时调用 C# 代码:

  • 使用 Code Behind - 如 NP.Demos.SimpleAvaloniaProject 示例所示(**最差**的方法)。
  • 使用其 Command 属性,可以将其绑定到 ViewModel 属性。ViewModel 属性可以定义一个 lambda 表达式来在按钮单击时调用,并定义一个属性来控制按钮是否启用。这将在以后的文章中详细介绍。
  • 使用监听 Click 路由事件并触发 C# 方法的行为。(这也将在以后介绍)- 这是**最佳**方法。

ListBox

ListBox 显示项目集合,一次只能选择一个项目。如果项目数量超过 ListBox 的大小,它将显示滚动条。

使用列表框的最佳方法是将其 ItemsSource 属性绑定到集合。下面将演示如何操作。在我们的例子中,我们只是在 XAML 代码中创建了 ListBoxItem 来填充它。

<ListBox x:Name="TheListBox">
  <ListBoxItem Content="Item 1"/>
  <ListBoxItem Content="Item 2"/>
  <ListBoxItem Content="Item 3"/>
  <ListBoxItem Content="Item 4"/>
</ListBox>

ListBox 最重要的属性是上面提到的 ItemsSource 以及与选择相关的属性:SelectedIndex 和SelectedItem。此 ListBox 的 SelectedIndex 绑定到下一小节介绍的 ComboBox 的 SelectedIndex ,因此当您在其中一个中更改选定的项目时,另一个将以相同的方式做出反应。

ComboBox

ComboBox 在其他各种框架中也称为 DropDownBox。与 ListBox 一样,它也存储项目集合,但始终只显示选定的项目 - 其他项目仅在鼠标指针单击其右侧的箭头时在弹出窗口(或者更确切地说,下拉列表)中显示。上面的图片显示了一个没有选定项目的 ComboBox 但下拉列表已打开。这是我用来创建和填充 ComboBox 的代码:

<ComboBox VerticalAlignment="Top"
          Grid.Row="1"
          SelectedIndex="{Binding Path=SelectedIndex, ElementName=TheListBox}">
  <ComboBoxItem Content="Item 1"/>
  <ComboBoxItem Content="Item 2"/>
  <ComboBoxItem Content="Item 3"/>
  <ComboBoxItem Content="Item 4"/>
</ComboBox>

ListBox 一样,ComboBox 的主要属性是 ItemsSource (绑定到集合)、SelectedIndex 和SelectedItem。示例演示了如何将 ComboBox 的 SelectedIndex 绑定到其左侧的 ListBox 的 SelectedIndexSelectedIndex="{Binding Path=SelectedIndex, ElementName=TheListBox, Mode=TwoWay}",这样当一个改变选择时,另一个也会改变。

ToggleButton

ToggleButton 是一个有两个状态的控件 - Checked 和 Unchecked,由其布尔属性 IsChecked 控制。每次单击按钮时,其 IsChecked 属性都会将其值从 false 切换到 true ,反之亦然。按钮的背景会根据其是否被选中而变化。

<ToggleButton x:Name="TheToggleButton" 
              Content="Toggle Button"/>

同样,为了演示绑定的强大功能,我将 ToggleButton 的 IsChecked 属性绑定到旁边的 CheckBox 的 IsChecked 属性,以便它们同步更改。

CheckBox

CheckBox 与切换按钮非常相似,但外观不同(如您所见)。这是 CheckBox 的 XAML 代码:

<CheckBox Content="Check Box"
          VerticalAlignment="Top"
          Grid.Row="1"
          IsChecked="{Binding Path=IsChecked, ElementName=TheToggleButton}"/>

您可以看到连接其 IsChecked 属性与其左侧 ToggleButton 的 IsChecked 属性的绑定。

CheckBox 还有一个切换属性 IsThreeState ,当设置为 true 时,CheckBox 可以在三个状态之间切换 - false、true 和 undefined - 这对应于其 IsChecked 属性设置为 null

这是三态 CheckBox 的代码:

<CheckBox Content="Three State Check Box"
          IsThreeState="True"/>

ContextMenu

ContextMenu 在右键单击某个区域或控件时打开。这是代码:

<Grid Grid.Row="1"
    Background="Transparent">
<Grid.ContextMenu>
  <ContextMenu Grid.Row="1">
    <MenuItem Header="Item1">
      <MenuItem Header="SubItem1"/>
      <MenuItem Header="SubItem2"/>
    </MenuItem>
    <MenuItem Header="Item2"/>
    <MenuItem Header="Item3"/>
    <MenuItem Header="Item4"/>
  </ContextMenu>
</Grid.ContextMenu>
<TextBlock Text="Right Click To Open Context Menu"
           VerticalAlignment="Center"/>
</Grid>

Menu

菜单通常位于窗口顶部,但也可以出现在其他地方。这是 Menu 示例的 XAML 代码:

<Menu Grid.Row="1">
    <MenuItem Header="FILE">
      <MenuItem Header="New"/>
      <MenuItem Header="Open"/>
      <MenuItem Header="Save"/>
    </MenuItem>
    <MenuItem Header="EDIT">
      <MenuItem Header="Copy"/>
      <MenuItem Header="Paste"/>
    </MenuItem>
</Menu>

弹出窗口

Popup 是一个控件,它在所谓的 popup 的 PlacementTarget 旁边打开一个轻量级窗口。

Popup 是否打开由其 IsOpen 属性控制(并反映),在我们的例子中,它与控制(并反映)popup 状态的 ToggleButton 的 IsChecked 属性绑定。

<Grid Grid.Row="1">
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <ToggleButton x:Name="OpenClosePopupButton"
                  Content="Open/Close Popup"/>

    <Popup x:Name="ThePopup"
           Grid.Row="1"
           IsOpen="{Binding Path=IsChecked, ElementName=OpenClosePopupButton, Mode=TwoWay}"
           PlacementMode="Bottom"
           PlacementTarget="{Binding ElementName=OpenClosePopupButton}">
      <Grid x:Name="PopupsContent"
            Background="Red"
            Width="150"
            Height="70">
        <TextBlock Text="Popup's Content"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"/>
      </Grid>
    </Popup>
</Grid>

我们使用双向绑定将 Popup 的 IsOpen 属性绑定到 ToggleButton 的 IsChecked 属性,以便更改其中任何一个都会影响另一个。

PlacementTarget 属性指定一个元素,Popup 将相对于该元素进行定位。

PlacementMode="Bottom" 表示 Popup 将定位在放置目标(placement target)的底部。

Window

当您单击 Window 示例中的按钮时,将打开一个小窗口。

这是窗口示例的 XAML 代码:

<Button x:Name="OpenWindowButton"
        Content="Open Window"/>

打开窗口的 C# 代码是通过 code-behind 连接的 - 不是因为它是一种好方法(实际上它**最差**,如上所述) - 而是因为它最简单且最容易理解。这是 MainWindow.axaml.cs 文件中相关的 C# 代码:

public MainWindow()
{
    ...

    var openWindowButton = this.FindControl<Button>("OpenWindowButton");

    openWindowButton.Click += OpenWindowButton_Click;
    ...
}
private void OpenWindowButton_Click(object? sender, RoutedEventArgs e)
{
    // Create the window object
    Window sampleWindow = 
        new Window 
        { 
            Title = "Sample Window",
            Width = 200,
            Height = 200
        };

    // open the window
    sampleWindow.Show();
}

MainWindow 的构造函数中(在调用 InitializeComponent() 之后),我们通过名称找到按钮并为其 Click 事件附加一个处理程序。在该处理程序中,我们创建一个新的 Window 对象并调用其 Show() 方法来显示(打开)窗口。

模态窗口

模态窗口也称为对话框 - 它是一个窗口,在关闭之前会阻止对其祖先窗口执行任何操作。

按下“**Open Modal (Dialog) Window**”按钮将打开一个对话框窗口,该窗口会完全阻止主窗口,直到关闭为止。这是 XAML 代码:

<Button x:Name="OpenModalWindowButton"
        Content="Open Modal (Dialog) Window"/>

同样,我们使用不好的 code-behind 范例来连接 C# 代码。

public MainWindow()
{
    ...

    var openModalWindowButton = this.FindControl<Button>("OpenModalWindowButton");

    openModalWindowButton.Click += OpenModalWindowButton_Click;
}
...
private void OpenModalWindowButton_Click(object? sender, RoutedEventArgs e)
{
    // Create the window object
    Window sampleWindow = 
        new Window 
        { 
            Title = "Sample Modal (Dialog) Window",
            Width = 200,
            Height = 200
        };

    // open the modal (dialog) window
    sampleWindow.ShowDialog(this);
}

与前面示例的唯一区别是,我们在此调用 sampleWindow.ShowDialog(...) 方法而不是 sampleWindow.Show() ,并将当前窗口作为对话框的父级传递给它。

工具提示

ToolTip 是当鼠标指针悬停在定义该 ToolTip 的元素上时弹出的临时弹出窗口。

我们在 XAML 中定义 ToolTip 如下:

<Grid Height="40"
      Background="Aqua"
      ToolTip.Tip="This is the ToolTip">

大多数时候 ToolTip 只是文本(如本例),但有时它会变得更复杂,将在未来讨论。

TabControl

TabControl 允许显示不同的标签页 - 每个标签页包含不同的内容。

在我们的示例中,切换标签页将在“Hello World!”和“Hi World!”之间更改显示的文本。

这是实现此目的的非常简单的 XAML 代码:

<TabControl Grid.Row="1">
    <TabItem Header="Tab 1">
        <TextBlock Text="Hello World!"/>
    </TabItem>
    <TabItem Header="Tab2">
        <TextBlock Text="Hi World!"/>
    </TabItem>
</TabControl>

Avalonia 基本元素

引言

基本元素(Primitives)是不可组合的 Avalonia UI 控件(也不是面板),它们继承自 Control 而不是 TemplatedControl。在 WPF 中,它们甚至不被称为控件,而是视觉元素。其中一个基本元素上面已经介绍过了 - 它是 TextBlock 元素。

除了上面介绍的 TextBlock 之外,最重要的基本元素是 BorderViewboxImage 和Shapes。Shapes 是继承自 Shape 的控件 - 其中最常用的是 Path(用于任何形状)、LineRectangle 和Ellipse

基本元素示例代码位置

本节的代码位于 NP.Demos.Primitives 解决方案下。

与前一节为每个基本元素使用一个页面不同,我将使用 TabControl 的一个标签页来介绍每种基本元素类型。这样,我们可以更详细地介绍这些非常重要的 Avalonia 基本构建块。

基本元素示例

Border

包含 Border 示例的标签页显示在上面的图片中。这是用来创建带有内部文本的边框的 XAML 代码:

<Border Margin="20"
      BorderThickness="10"
      BorderBrush="Red"
      Background="Blue"
      CornerRadius="0, 10, 40, 120">
    <TextBlock Foreground="White"
               FontSize="20"
               Text="Border Example!"
               HorizontalAlignment="Center"
               VerticalAlignment="Center"/>
</Border>

TextBlock “Border Example”位于 Border 内部 - 它将 Border.Child 属性设置为 TextBlock

  • BorderThickness="10" - 指定 Border 元素实际边框的大小。
  • BorderBrush - 边框的颜色。
  • Background 指定边框内部的颜色。
  • CornerRadius - 指定边框角的圆角程度。我故意将所有 4 个角的半径设置为不同,以展示 CornerRadius 属性的全部功能。

重要提示:我建议不要设置边框的 Child 属性,而是将边框放在 Grid 面板中,并将您想要放在边框内的元素放在同一面板中,作为边框的同级。这是因为根据我的 WPF 经验,如果边框有阴影(它们通常需要阴影),则它们的后代元素会轻微抖动。然而,不直接是边框后代元素的元素将不受影响。我还没有测试过这在 Avalonia 中是如何工作的 - 这个建议是基于我的 WPF 经验。

Viewbox

Viewbox 是一个控件,它允许在其内部的所有内容在视觉上缩小或放大。与边框一样,它有一个 Child 属性用于放置内容。您可以将任何复杂的视觉元素或包含多个视觉元素的面板放在其中,并且所有内容都会根据 Viewbox 的参数在视觉上进行缩放。

Viewbox 将从其后代那里接收大小。即使其 HorizontalAlignment 和VerticalAlignment 属性设置为 Stretch ,Viewbox 的子元素也总是会收缩到其后代允许的最小尺寸。如果 Viewbox 的子元素未指定大小且无法从其自己的子元素推导出大小,例如,一个没有 Height 和Width 并且没有任何可以定义其大小的子元素的 Grid ,它将收缩到 0 大小。

我们在“Viewbox”选项卡下的示例演示了当 Viewbox 的 Stretch 属性设置为不同值时,带有标签的 TextBox 如何变化。您可以通过在左侧将 Stretch 属性设置为不同的值,然后尝试调整窗口大小来亲自尝试。

这是我们的 Viewbox 示例的代码:

<Viewbox x:Name="TheViewBox" 
         Stretch="{Binding Path=SelectedItem, ElementName=StretchChooser, Mode=TwoWay}"
         Grid.Column="1">
    <Grid  Width="640"
           Height="238"
           Background="LightBlue">
      <TextBlock FontSize="20"
                 Text="Enter Text: "
                 HorizontalAlignment="Left"
                 VerticalAlignment="Center"
                 Margin="20"/>
      <TextBox Grid.Column="1"
               Width="300"
               FontSize="20"
               Margin="20"
               Text="Hello World!!!"
               HorizontalAlignment="Right"
               VerticalAlignment="Center"/>
    </Grid>
</Viewbox>

这是我们在收缩控件并设置 Stretch="None" 时的情况。

如果 stretch 设置为 None - viewbox 的子元素根本不会缩放 - 所以当您使窗口变小时 - 您将裁剪 viewbox 的内容,如上所示。

这是 Stretch="Fill" 的图片:

Viewbox 的子元素的 width 和height 会与其 width 和size 的变化成比例地缩放 - 在我们的例子中,我们使 width 变小(变窄),使 height 变大。在“Fill”下,原始宽高比(width/height)不会被保留,但一切都取决于 Viewbox 的缩放方式。

其余可能的 Stretch 值会保留原始控件的宽高比。在 Stretch="Uniform" 下,当我们收缩 width 但增加 height 时,会发生以下情况:

控件会收缩(或扩展),同时保留原始宽高比,以便所有内容都能适应空间。

最后,当 Stretch="UniformToFill" 时 - 子控件仍然保留宽高比,使得其一个维度完全填充分配给它的空间,而另一个维度则被裁剪 - 如下面的图片所示 - Y 维度填充 height ,而 X 维度被裁剪。

请注意,其他一些基本元素 - Images 和Shapes - 也具有行为完全相同的 Stretch 属性。

Image

您应该尝试“Image”选项卡下的 Image 示例。

尝试选择不同的 Stretch 模式,看看 Image 在每种模式下的不同缩放方式。

这是 Image 示例的相关 XAML 代码:

<Grid ColumnDefinitions="Auto, *">
    <StackPanel VerticalAlignment="Top"
        HorizontalAlignment="Center">
      <TextBlock Text="Choose Stretch Type"/>
      <ComboBox x:Name="ImageStretchChooser"
                ItemsSource="{Binding Source={x:Type Stretch}, 
                Converter={x:Static local:EnumTypeToCollectionConverter.Instance}}"
                Width="100"
                Height="30"
                Margin="10"/>
    </StackPanel>
    <Grid Grid.Column="1">
        <Image x:Name="TheImage"
               Stretch="{Binding #ImageStretchChooser.SelectedItem, Mode=TwoWay}"
               Source="/Images/LinuxIcon.jpg"/>
    </Grid>
</Grid>

Image 上定义的最重要属性是 Source。在 XAML 中,它指向实际的 Image png 或 jpg 或其他文件:Source="/Images/LinuxIcon.jpg"。请注意,图像文件 LinuxIcon.jpg 定义在同一个项目中,其 Build Action 为 AvaloniaResource

请注意,Image 类的 Source 属性是 IImage 类型,因此如果您想在 C# 代码中为其赋值,您需要这样写,例如:

Image image = this.FindControl<Image>("TheImage");
var assets = AvaloniaLocator.Current.GetService<IAssetLoader>();
image.Source = new Bitmap(assets.Open
               (new Uri("avares://NP.Demos.Primitives/Images/LinuxIcon.jpg")));

XAML 类型转换使得 Source 赋值相当简单,但有时您无法避免使用 C#。

请注意,Bitmap 是 IImage 的实现之一,这就是为什么上面的代码有效。

URL“avares://NP.Demos.Primitives/Images/LinuxIcon.jpg”中的神秘前缀“avares”代表“Avalonia Resource”,而不是埃及 Hyksos 的首都。

Shapes

Shapes - 是各种几何形状。这是 **Shapes** 选项卡的样子:

您可以尝试 StretchStrokeThickness 和Fill 属性。您可以看到 Rectangle 和Ellipse 不受 Stretch 的影响 - 这是有道理的,因为它们由它们的 width 和height 决定。

Line 和 Path 受 Stretch 的影响,与 Image 和Viewbox 基本相同。

StrokeThickness 决定边框的粗细。Stroke 属性(类型为 IBrush)决定边框的颜色。

Fill 指定内部颜色 - 在我们的例子中,当 HasFill checkbox 打开时,Fill 为 red,而在它关闭时,Fill 为 null (基本上是透明的,但也是 hit test invisible)。

这是线的 XAML 代码:

<Line StartPoint="0,0"
      EndPoint="50, 50"
      Grid.Row="1"
      Stretch="{Binding #ShapeStretchChooser.SelectedItem}"
      Stroke="Black"
      Margin="20"
      StrokeThickness="{Binding #ThicknessSlider.Value}"
      Fill="{Binding #HasFillCheckBox.IsChecked, 
           Converter={StaticResource FillConverter}}"/>

主要的线定义属性是 StartPoint 和EndPoint ,它们决定线的起点和终点。

Rectangle 和Ellipse 的形状由它们的 width 和height 决定 - 它们应该显式指定,或者由其容器为它们提供的空间决定。

Path 是最通用的形状。它由其 Data 属性(类型为 Geometry)决定。创建不同形状的 Geometry 是一个独立的领域,超出了本教程的范围。对于现成的 Geometry,您可以访问 Material Design Icons,选择您想要的图标,然后查看其 XAML 表示。

这是我们的 Path 示例的代码:

<Path Data="M11.92,19.92L4,12L11.92,4.08L13.33,5.5L7.83,
            11H22V13H7.83L13.34,18.5L11.92,19.92M4,12V2H2V22H4V12Z"
      Stretch="{Binding #ShapeStretchChooser.SelectedItem}"
      Stroke="Black"
      Grid.Row="1"
      Margin="20"
      StrokeThickness="{Binding #ThicknessSlider.Value}"
      Fill="{Binding #HasFillCheckBox.IsChecked, Converter={StaticResource FillConverter}}"/>

Data 设置为从 Material Design Icons 复制的神秘 Geometry string

Avalonia 面板

引言

面板是 Avalonia 的基本控件,用于排列放置在它们内部的其他控件。除了它们的 Background color 之外,面板本身没有视觉表示,但它们对于排序和排列其他控件是必不可少的。

面板的代码位于 NP.Demos.Panels.sln 解决方案下。

面板示例

StackPanel

打开应用程序时,您将进入 StackPanel 选项卡。

有三个 100x100 通用像素的方形按钮堆叠排列 - 左侧是垂直堆叠,右侧是水平堆叠。

这是垂直堆栈的 XAML 代码:

<StackPanel Orientation="Vertical"
            Grid.Row="1"
            VerticalAlignment="Top"
            HorizontalAlignment="Left">
  <Button Content="1"
          Width="100"
          Height="100"/>
  <Button Content="2"
          Width="100"
          Height="100"/>
  <Button Content="3"
          Width="100"
          Height="100"/>
</StackPanel>

Orientation="Vertical" 定义了方向。垂直方向是默认值 - 因此第一个属性可以省略。

为了实现右侧显示的水平方向,您只需要将 Orientation 替换为 Orientation="Horizontal"。这就是实现右侧堆叠的方式。

如果您调整窗口大小,您会发现当窗口变得太小时,StackPanels 的末端会被裁剪。WrapPanel 解决了这个问题。

WrapPanel

转到“**WrapPanel**”选项卡来玩 WrapPanel。如果您将其缩小,您会发现最后一个项目/项目不会被裁剪 - 而是换行,垂直面板将最后一个项目/项目换行到右侧,水平面板则换行到下方。

Grid

Net 选项卡显示 Grid ,这是最复杂也是最有用的面板。

我们的 Grid 有 4 行和 4 列 - 定义如下:

<Grid RowDefinitions="80, Auto, *, 2*"
      ColumnDefinitions="80, Auto, *, 2*"
      ...>

上面的定义是允许的,因为 Avalonia 提供了输入行和列定义的快捷方式。标准的 WPF 方法也允许,下面是这种定义的样子:

<Grid>
  <Grid.RowDefinitions>
    <RowDefinition Height="80"/>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
    <RowDefinition Height="2*"/>
  </Grid.RowDefinitions>
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="80"/>
    <ColumnDefinition Width="Auto"/>
    <ColumnDefinition Width="*"/>
    <ColumnDefinition Width="2*"/>
  </Grid.ColumnDefinitions>
</Grid>

您可以看到 Avalonia 快捷方式节省了大量空间。

这是行高和列宽的解释。第一行(Height="80")的高度是 80 个通用像素。第二行(Height="Auto")的大小与其内容匹配 - 即,其大小由其包含的内容决定。第三行和第四行是星形行(Height="*" 和Height=2*)。总而言之,所有星形行和列都占据了 Grid 允许占据的所有剩余空间,并且它们之间的空间是根据它们的星形系数分布的。由于最后一行系数为 2,因此分配给它的空间将始终是倒数第二行分配空间的双倍。

列的宽度计算方式完全相同。

打开应用程序的 **Grid** 选项卡,并通过水平和垂直调整窗口大小来玩它。

DockPanel

DockPanel 允许将子元素排列在其侧面,而最后一个子元素(未停靠)将占据剩余空间。

停靠值由 DockPanel.Dock 附加属性确定,该属性可以取 LeftTopRight 和Bottom 等值。

在我们的示例中,我们有 8 个按钮顺时针排列 - LeftTopRightBottom ,然后再次 LeftTopRightBottom ,然后最后一个按钮占据剩余空间。

这是我们的 DockPanel 示例的代码:

<DockPanel Margin="20">
  <Button Content="1"
          DockPanel.Dock="Left"
          Width="30"/>
  <Button DockPanel.Dock="Top"
          Content="2"
          Height="30"/>
  <Button DockPanel.Dock="Right"
          Content="3"
          Width="30"/>
  <Button DockPanel.Dock="Bottom"
          Content="4"
          Height="30"/>
  <Button DockPanel.Dock="Left"
          Content="5"
          Width="30"/>
  <Button DockPanel.Dock="Top"
          Content="6"
          Height="30"/>
  <Button DockPanel.Dock="Right"
          Content="7"
          Width="30"/>
  <Button DockPanel.Dock="Bottom"
          Content="8"
          Height="30"/>
  <Button Content="The Rest"/>
</DockPanel>

请注意,垂直停靠的按钮的 width 和水平停靠的按钮的 height 都是 30

画布

Canvas 是一个面板,它允许通过从左上角开始的坐标来放置控件,这些坐标由附加属性 Canvas.Left 和Canvas.Top 确定。

在上面的画布上,按钮从左上角向右偏移 300 个通用像素,向下偏移 200 个像素。这是 XAML 代码:

<Canvas>
  <Button Content="1"
          Width="100"
          Height="100"
          Canvas.Left="300"
          Canvas.Top="200"/>
</Canvas>

RelativePanel

WPF 中不存在 RelativePanel。但是,它可能非常有用,尤其是在为平板电脑和手机编码时。它允许指定元素相对于面板本身以及相对于同一面板内其他命名元素的位置。

这是我们的 RelativePanel 示例的样子:

RelativePanel 提供了许多所谓的附加属性,这些属性允许选择子元素相对于面板本身或相对于同一面板的其他命名子元素的位置。这是上面示例的 XAML 代码:

<RelativePanel Margin="20"
               Background="LightBlue">
  <Button x:Name="Button1"
          Height="50"
          Content="Button1 - TopLeftCorner by default"/>
  <Button x:Name="Button2"
          Height="50"
          RelativePanel.AlignTopWithPanel="True"
          RelativePanel.AlignHorizontalCenterWithPanel="True"
          Content="Button2 - Mid Top"/>

  <Button x:Name="Button3"
          Height="50"
          RelativePanel.AlignBottomWithPanel="True"
          RelativePanel.AlignRightWithPanel="True"
          Content="Button3 - Bottom Right"/>

  <Button x:Name="Button4"
          Height="50"
          RelativePanel.AlignHorizontalCenterWithPanel="True"
          RelativePanel.AlignVerticalCenterWithPanel="True"
          Content="Button4 - Center"/>

  <Button x:Name="Button5"
          Height="50"
          RelativePanel.RightOf="Button4"
          RelativePanel.Below="Button4"
          Content="Button5 - Bottom right from Button4"/>
</RelativePanel>

从这个示例中可以看出,可以通过使用,例如,RelativePanel.AlignBottomWithPanel 和RelativePanel.AlignRightWithPanel 属性将控件放置在 RelativePanel 的任何侧面。您还可以使用 RelativePanel.RightOf 和RelativePanel.Below 属性将一个控件放置在另一个控件的右下方。

Avalonia 画笔

引言

与 WPF 一样,Avalonia 拥有:

  • SolicColorBrush 类,用于表示单色。
  • LinearGradientBrush 类,用于在一个方向上表示空间变化的颜色。
  • RadialGradientBrush 类,用于表示围绕特定点基于椭圆曲线变化的颜色。

在 WPF 画笔之上,Avalonia 画笔还具有 ConicGradientBrush 类。

Brushes 示例位于 NP.Demos.Brushes.sln 解决方案下。

画笔

SolidColorBrush

SolidColorBrush 由前两个示例覆盖:第一个示例按名称指定颜色(Background="Red"),第二个按 ARGB 值指定颜色(Background="#FF43A047")。

LinearGradientBrush

这是 LinearGradientBrush 示例的 XAML:

<LinearGradientBrush StartPoint="0%,0%"
                      EndPoint="100%,100%" >
  <LinearGradientBrush.GradientStops>
    <GradientStop Offset="0" Color="Red"/>
    <GradientStop Offset="0.25" Color="Blue"/>
    <GradientStop Offset="0.5" Color="Brown"/>
    <GradientStop Offset="0.75" Color="Green"/>
    <GradientStop Offset="1" Color="Purple"/>
  </LinearGradientBrush.GradientStops>
</LinearGradientBrush>

起点和终点的坐标从控件(在本例中为 Button)的左上角开始计算。在我们的例子中,StartPoint 是左上角,终点是右下角(100%、100% 点)。

渐变停止点指定从起点到终点的线段上的比例偏移以及在相应偏移处应有的颜色。

RadialGradientBrush

这是 RadialGradientBrush 的 XAML:

<RadialGradientBrush GradientOrigin="25%,25%"
                      Center="50%, 50%"
                      Radius="0.5" >
  <RadialGradientBrush.GradientStops>
    <GradientStop Offset="0" Color="Red"/>
    <GradientStop Offset="0.25" Color="Blue"/>
    <GradientStop Offset="0.5" Color="Brown"/>
    <GradientStop Offset="0.75" Color="Green"/>
    <GradientStop Offset="1" Color="Purple"/>
  </RadialGradientBrush.GradientStops>
</RadialGradientBrush>

RadialGradientBrush 有两个重要点 - GradientOrigin 和Center。渐变圆围绕连接这两个点的线排列,圆的中心在偏移 0 处收敛到 GradientOrigin ,在偏移 1 处收敛到 Center。圆的大小由 Radius (0 到 1 之间的双精度数) 控制。

Avalonia 的 RadialGradientBrush 不如 WPF 的强大,因为 WPF 允许为半径输入两个数字:RadiusX 和RadiusY,从而在圆形之上实现椭圆形。

ConicGradientBrush

这是 ConicGradientBrush 的 XAML 代码:

<ConicGradientBrush Center="30%, 30%"
                    Angle="90">
  <ConicGradientBrush.GradientStops>
    <GradientStop Offset="0" Color="Red"/>
    <GradientStop Offset="0.25" Color="Blue"/>
    <GradientStop Offset="0.5" Color="Brown"/>
    <GradientStop Offset="0.75" Color="Green"/>
    <GradientStop Offset="1" Color="Purple"/>
  </ConicGradientBrush.GradientStops>
</ConicGradientBrush>

ConicRadialBrush 的情况下,颜色围绕由 Center 属性指定的某个点进行锥形排列。Angle 属性指定垂直轴和第一种颜色(偏移 0 处的颜色)之间的顺时针角度(以度为单位)- 在我们的例子中,它是 90 度,所以红色从水平方向开始。

Avalonia UI 变换

变换允许对任何 Avalonia UI 控件进行 2D 线性仿射变换。

变换代码位于 NP.Demos.Transforms.sln 解决方案下。

RenderTransform 与 LayoutTransform

在 WPF 中,每个元素都可以对其进行渲染变换和布局变换。RenderTransform 在布局完成后执行变换(不影响控件周围的布局)。LayoutTransform 在计算出新(变换后的)控件坐标后执行布局操作。

由于 RenderTransform 比 LayoutTransform 使用得更频繁,Avalonia 允许对每个 Avalonia 控件执行 RenderTransform (与 WPF 相同),但 LayoutTransform 只能在 LayoutTransformControl 上执行。如果您需要在任何控件上执行 LayoutTransform ,您可以始终将其作为 LayoutTransformControl 的子项,并在该 LayoutTransformControl 上执行 LayoutTransform ,就像我们的示例中所示。

如果您尝试运行示例并更改 RotationTransform 的旋转角度,您将获得以下结果:

您可以看到,渲染变换的按钮将显示在其包含的 Grid 面板之外,而布局变换的按钮将扩展其 Grid 容器(容器面板显示为浅蓝色)。

这是示例的相关代码:

<Grid Margin="5"
      Grid.Column="2">
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
  </Grid.RowDefinitions>
  <TextBlock Text="Render Rotate Transform Sample:"
              Classes="h1"/>

  <Grid Width="200"
        VerticalAlignment="Center"
        Grid.Row="1"
        Background="LightBlue">
    <Button Width="100"
            Height="25"
            RenderTransformOrigin="50%, 50%"
            HorizontalAlignment="Center"
            VerticalAlignment="Center">
      <Button.RenderTransform>
        <TransformGroup>
          <RotateTransform Angle="{Binding Path=Value, ElementName=AngleSlider}"/>
        </TransformGroup>
      </Button.RenderTransform>
    </Button>
  </Grid>
</Grid>

<Grid Margin="5"
      Grid.Column="4">
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
  </Grid.RowDefinitions>
  <TextBlock Text="Layout Rotate Transform Sample:"
              Classes="h1"/>

  <Grid Width="200"
        VerticalAlignment="Center"
        Grid.Row="1"
        Background="LightBlue">
    <LayoutTransformControl HorizontalAlignment="Center"
                            VerticalAlignment="Center">
      <Button Width="100"
              Height="25"
              HorizontalAlignment="Center"
              VerticalAlignment="Center"/>
      <LayoutTransformControl.LayoutTransform>
        <TransformGroup>
          <RotateTransform Angle="{Binding Path=Value, ElementName=AngleSlider}"/>
        </TransformGroup>
      </LayoutTransformControl.LayoutTransform>
    </LayoutTransformControl>
  </Grid>
</Grid>

您可以看到,为了实现布局变换,我们的按钮被放置在 LayoutTransformControl 内部,并且 RotateTransform 应用于这个 LayoutTransformControl 而不是按钮本身。

RotateTransform 的重要属性是以度为单位的角度,它指定旋转角度。

TranslateTransform

更改 TranslateTransform 的 X 和Y 属性会将控件向右移动 X 像素,向下移动 Y 像素。

<Button Width="100"
        Height="25"
        HorizontalAlignment="Center"
        VerticalAlignment="Center">
  <Button.RenderTransform>
    <TranslateTransform X="{Binding #XSlider.Value}"
                        Y="{Binding #YSlider.Value}"/>
  </Button.RenderTransform>
</Button>

ScaleTransform

Scale 变换在水平或垂直方向上扩展或收缩控件。水平和垂直缩放分别由 ScaleX 和ScaleY 属性控制。

<Button Width="100"
        Height="25"
        Grid.Column="2"
        HorizontalAlignment="Center"
        VerticalAlignment="Center">
  <Button.RenderTransform>
    <ScaleTransform ScaleX="{Binding #ScaleXSlider.Value}"
                    ScaleY="{Binding #ScaleYSlider.Value}"/>
  </Button.RenderTransform>
</Button>

SkewTransform

SkewTransform 根据其 AngleX 和AngleY 属性,在水平或垂直方向(或两者)上倾斜图像。

<Button Width="100"
        Height="25"
        Grid.Column="2"
        HorizontalAlignment="Center"
        VerticalAlignment="Center">
  <Button.RenderTransform>
    <SkewTransform  AngleX="{Binding #SkewXSlider.Value}"
                    AngleY="{Binding #SkewYSlider.Value}"/>
  </Button.RenderTransform>
</Button>

更多关于变换的内容

Avalonia 具有 MatrixTransform ,这是一个通用的线性仿射变换。Avalonia 中所有其他可用的变换(旋转、平移、缩放和倾斜变换)只是 MatrixTransform 的private 特例。但由于它不直观,因此很少使用。

可以通过将多个变换放在 TransformGroup 对象内来组合它们。

结论

本文的目的是让没有 WPF 背景的人开始使用 Avalonia 进行多平台编码。本文致力于介绍 Avalonia 的基本构建块。

我计划撰写更多文章,涵盖其他令人兴奋的 Avalonia 主题,包括但不限于绑定、MVVM 模式、模板、样式、行为、代码的最佳组织等。

历史

  • 2021 年 7 月 21 日:初始版本
© . All rights reserved.