目录
使用Visual Studio 2019创建和运行简单的Avalon项目
RenderTransform与LayoutTransform
介绍
在Microsoft停止使用Silverlight后,唯一可用于所有主要平台的UI编程的多平台解决方案是Web解决方案 (JavaScript/TypeScript)和Xamarin。两者都是不足的,并且代表了从WPF富UI客户端解决方案的巨大倒退,如下面将详细解释的那样。
然而,有一个相对较新的开源.NET解决方案,Avalonia与WPF非常相似,在许多方面都比WPF更强大,并且(与WPF或UWP不同)适用于任何平台——Windows、Linux和MAC(我自己在每个平台上都对其进行了测试)那些平台)。Avalonia也适用于移动平台——iOS和Android(我还没有在移动平台上测试过Avalonia,所以我依赖Avalonia文档)。
为什么Avalonia会大受欢迎
- Avalonia是完全开源的——您可以在Avalonia Source Code上找到在限制最少——MIT许可下发布的源代码。
- Avalonia是用.NET Standard编写的,它100%兼容所有版本的.NET Core和.NET Framework。
- Avalonia正在由全球各地非常聪明的人快速开发,他们面对客户并了解客户的需求。
- Avalonia有一些类似于WPF的snoop的工具,目前比snoop更差,但具有足以调试和修改Avalonia应用程序的基本功能。此外,考虑到Avalonia项目积极变化的速度,此类工具将快速改进。
- Avalonia为创建XAML文件和XAML intellisense提供了Visual Studio和Rider支持,尽管Avalonia XAML intellisense仍然落后于WPF XAML intellisense所能提供的功能。
- WPF和Silverlight框架提出了许多超出通常OOP编程范式的编程范式,这使得那些了解这些范式的人可以更清晰、更快地创建代码。在这些范例中,我列出了以下内容:
- 视觉和逻辑树
- 可以在使用它们的对象之外定义的附加或依赖属性,并且不会占用任何额外的内存,除非它们被分配了非默认值并且当它们的值发生变化时会触发一个特殊事件。
- 附加路由事件,可以在触发它们的对象之外定义,并且可以在树上传播和上下处理。
- 绑定和相关的MVVM模式
- 控制模板
- 数据模板
- 样式
- 行为——在不使用事件修改类本身的情况下修改C#类的行为的方法。
所有这些范例都已在Avalonia中以至少与WPF中一样强大的方式实现。
- Avalonia允许创建在不同平台上外观和行为相同的应用程序,但也支持特定于平台的定制。
- 几个非常流行的.NET开源项目从WPF切换到Avalonia,以使他们的产品具有多平台性——其中包括我使用的项目——
- Avalonia在Gitter上有不错的免费公共支持:Gitter上的Avalonia以及在Avalonia Support购买商业支持的一些选项
- Avalonia有一些关于Avalonia Documentation的文档,尽管需要更多文档,尤其是对于那些没有WPF背景的人,并且本文的目的是填补一些文档空白。
Avalonia的一些缺点
一旦我们谈到优势,我们还应该提到Avalonia的一些镜头——请注意,由于Avalonia是一个快速发展的项目,它们中的大多数将很快得到修复。
- 文档非常小,主要针对WPF开发人员。为了使项目更受欢迎和更广泛接受,需要更多的文档,本文的目的就是解决这个问题。
- 有一些怪癖和一些行为与WPF略有不同,因此我不得不在Avalonia UI讨论中询问有关gitter的问题,或者只是进入Avalonia代码以找出如何解决它们。
- Avalonia UI的控件带有两个主题(默认和流畅),这两个主题各有明暗模式——对于其他任何内容和一些更复杂的控件,Telerik或DevExpress等第三方商业控件尚不可用。我希望自己能帮助解决这个问题,因为我计划为Avalonia UI创建一些灵活的开源控件。我已经创建了一个可以在NP.Avalonia.Visuals上完全定制标头的CustomWindow。
- 似乎Avalonia与Skia相关联——一个2D图形库,这意味着不能轻松地将3-D图形添加到Avalonia(尽管几乎没有人使用WPF 3-D功能)。
Web和Xamarin框架在多平台开发中的缺点
与WPF/Avalonia编程相比,Web(JavaScript/TypeScript)开发有以下缺点:
- Web开发不是组合式的。您不能创建一个包含多个基元并在Web浏览器的不同部分以完全相同的方式显示的自定义控件。例如,您不能将HTML5画布矩形打包成一个可重用的按钮控件,该按钮控件将以类似的方式显示在Web页面的中心和底部(没有一些额外的自定义)。取而代之的是,这些控件是随浏览器预定义的,具有一百万种自定义方式(其中一些自定义特定于每个浏览器)。经验丰富的WPF/Avalonia开发人员可以使用设计人员要求的精确UI行为极快地创建所需的UI控件,并且此类控件将像控件开发人员希望的那样可重用和可定制。
- 上面提到的WPF范式(例如,绑定和模板)中很少有被著名的Web框架(如Angular和React)实现,但没有一个像在WPF和Avalonia中那样以如此强大的方式实现。例如,在Angular或React中都没有附加属性、自定义路由事件或与树祖先的绑定。
- 如果您使用的是web——将应用程序拆分为多个窗口时会遇到问题。甚至Visual Studio Code(它是最先进的typescript UI应用程序)也不允许将选项卡拖出主窗口。顺便说一句——如果你使用JavaSript或TypeScript开始一个桌面项目,不要期望编写任何接近Visual Studio Code的东西——我看到几个项目的这种期望都失败了。但是使用Avalonia创建像VS Code这样复杂的应用程序是完全可以实现的。
现在关于Xamarin及其继任者MAUI的缺点。
- Xamarin(可能还有MAUI)是为构建移动应用程序而明确创建的,而没有过多考虑桌面应用程序。相应地,所有商业Xamarin框架,例如Telerik或DevExpress都面向移动开发,很少考虑桌面开发。
- Xamarin Forms(它是一个多平台产品)通常需要针对Xamarin Forms未涵盖的功能进行一些本机编程。
- Xamarin表单不是组合式的——基本范式来自本机控件——按钮、菜单等。它们在每个平台上的显示方式也不同——这有时是可取的,有时不是。
- Xamarin没有实现上面列出的许多WPF范例。
你可以在这篇文章中找到什么
本文及其计划后续文章的目的是填补不熟悉WPF或Silverlight的人在编程Avalonia UI时可能存在的空白。更复杂的问题将在以后的文章中描述。
本文中的所有样本都经过了以下测试:
- Window 10
- Ubuntu 20.4
- Mac Catalina
本文仅涵盖Avalonia UI的基础知识——将成为任何应用程序一部分的构建块:
- 使用后置代码创建简单的Avalonia应用程序。注意——Code Behind是构建大型应用程序的最简单但最糟糕的方法。不要过度使用它!其他连接XAML代码和C#代码的方法将在以后的文章中介绍。
- 最有用的内置控件
- 基元(用作合成的基元构建块的控件)
- Panels
- Brushes
- Transforms
下一部分(第2部分)将涵盖以下主题:
- 视觉树
- 逻辑树
- Avalonia开发工具
- 附加、样式和直接属性
- 绑定
之后,我还计划涵盖:
- DataTemplates,ItemsPresenter和ContentPresenter
- 从XAML调用C#代码(Commands和CallAction行为)
- XAML(通过标记扩展重用XAML)
- 样式
- 动画
之后的文章将涵盖更复杂的主题,例如:
- 自定义(无外观)控件
- ControlTemplates
- 行为
- IoC与Avalonia
- 为原型驱动开发安排Avalonia项目。
如何阅读这篇文章
本文旨在成为Avalonia的动手教程,同时重点介绍Avalonia的基本功能。它充满了我建议在您的计算机上构建和运行的编码示例。如果您想很好地学习Avalonia功能,您还应该尝试创建自己的项目和示例,类似于本文中给出的那些。
阅读本文和复习练习不需要以前的WPF背景知识。
本文中的所有示例都是使用Visual Studio 2019、.NET 5.0和相应的C# 9.0创建的,尽管它们可以轻松降级到以前版本的.NET和C#。
使用Visual Studio 2019创建和运行简单的Avalon项目
首先,为了使用Avalonia,您需要使用位于“工具”下的VS扩展管理器或(对于VS2019)“扩展”菜单下的VS扩展管理器安装“Avalonia for Visual Studio”扩展。在网上找到这个扩展并安装它。该扩展包含各种Avalonia相关Visual Studio项目的模板、Avalonia特定文件类型和Avalonia XAML文件的智能感知(与WPF XAML文件略有不同)。
安装“Avalonia for Visual Studio”扩展后,启动Visual Studio并选择“Avalonia Application”项目类型:
按“下一步”按钮,选择项目的位置和项目名称,然后按“创建”按钮:
创建的项目将依赖于三个Avalonia包——Avalonia、Avalonia.Desktop和Avalonia.Diagnostics:
还创建了五个包含代码的文件——App.axaml、App.axaml.cs、MainWindow.axaml、MainWindow.axaml.cs和Program.cs。
带有“.axaml”扩展名的文件是重命名为“.axaml”的XAML文件,显然是为了将它们与WPF/UWP“.xaml”文件区分开来。Avalonia XAML语法与WPF XAML语法非常相似,在所谓的样式选择器方面有一些特殊性(将在以后的文章中解释)。
在上述五个文件中,您可能需要修改MainWindow.axaml和MainWindow.axaml.cs文件最多,然后可能稍微更改App.axaml和App.axaml.cs文件,您可能不必更改Program.cs文件。
您可以按原样运行空窗口,但在MainWindow中放置一些代码会更有趣。
打开MainWindow.xaml文件并将其内容(默认由"Welcome to Avalonia!"文本组成)替换为以下代码:
<Button x:Name="CloseWindowButton"
Content="Close Window"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Padding="10,5"/>
如果您现在运行应用程序,窗口中间会有一个Button(由水平和垂直对齐确保——单词“关闭窗口”将写在按钮中间(Button的内容)和从“关闭按钮”文本到按钮两侧的边距将在左右为10个通用像素,在顶部和底部为5个通用像素(由Padding属性指定):
到目前为止一切顺利,但如果你按下按钮,什么都不会发生。我们需要尝试将按钮单击事件连接到关闭窗口的操作。在第一个示例中,我们将使用最简单但也是最糟糕的方式来实现此目的 ——后置代码。文件MainWindow.xaml.cs包含所谓的“代码隐藏”——MainWindow.xaml文件的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();
}
现在,如果您运行应用程序并按“关闭”按钮,窗口将关闭。
此应用程序的代码位于NP.Demos.SimpleAvaloniaProject.sln下,但如果您是Avalonia新手,则必须复习上面的练习。
一些Avalonia内置控件
Avalonia控件简介
在本节中,我将描述一些对构建应用程序最有帮助的内置Avalonia控件。请注意,为简洁起见,我不会描述每个内置控件,只描述最常用的控件。
如果您想了解其余的内置控件,您可以:
- 从Github上的Avalonia Source Code下载或克隆AvaliniaUI源代码。
- 使用Visual Studio从顶级文件夹打开Avalonia.sln解决方案。
- 在解决方案资源管理器中导航到Samples Visual Studio文件夹下的ControlCatalog.Desktop项目。
- 将ControlCatalog.Desktop设置为您的启动项目,然后构建并运行它。
包含大部分(如果不是全部)内置控件的Windows应用程序将Popup,您将能够看到各种控件的功能。通过在Samples/Pages/ Visual Studio文件夹下的XAML代码中跟踪它们,您还可以了解如何创建和更改这些控件的属性。
新控件的创建和自定义将在以后的文章中讨论,以及最有用的控件ContentPresenter,ItemsPresenter它们可以相应地为非可视对象或非可视对象集合提供可视表示。
本节的目的是概述内置Avalonia控件,而不是对其功能的详细描述。
请注意,Avalonia中的Control类比WPF中的同名类更原始——Avalonia Control没有模板。具有模板的控件继承自实现了ITemplatedControl接口的TemplatedControl类。因为那个Avalonia Image、Shape和Panel类是从Control派生的,而不是从Visual类派生的(因为它们在WPF中)。
未来文章中将提供哪些模板的说明。
内置控件项目
在本小节中,我们将描述WPF意义上的一些最有用的控件,即模板化控件,而不是更原始的图像、形状和面板。
这些小节的代码位于NP.Demos.BuiltInControls.sln解决方案下。
如果您构建和运行解决方案,您将看到以下内容:
在这里,我描述了一些我认为最有用的内置控件。特定于这些控件的所有代码都位于MainWindow.xaml文件中。让我们回顾一下每个控件并描述与它们相关的XAML代码。
TextBlock
TextBlock是本节中描述的控件中唯一不能重新模板化的控件——它派生自Control(而不是TemplatedControl类)。所以它更像是一个原始的,而不是WPF意义上的复合控件,但我把它放在这里,因为它是最重要的构建块之一,并且(从我的角度来看)应该首先描述。
TextBlock表示简单的文本。它最重要的属性是C# string类型的Text。Text包含要显示为TextBlock的文本。
TextBlock有很多允许文本自定义的属性,包括:
- Foreground——文本的颜色
- FontSize——自我描述
- FontFamily——指定字体的名称
- FontWeight——通常在Normal和Bold之间切换——用于粗体文本
- TextWrapping——指定文本是否应换行成多行
- TextTrimming——指定如果部分文本不可见,是否应显示省略号('...')字符——文本超出控件的大小
上面列出的许多属性也适用于可能具有文本的其他控件,例如按钮、菜单、列表框等。
创建文本块的简单XAML代码如下:
<TextBlock Text="Hello World!">
TextBox
这是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绑定: Text="{Binding Path=Text, ElementName=TheTextBox}"将TextBlock的Text属性绑定到TextBox的Text属性(通过其x:Name属性命名为“TheTextBox”)。这会产生下面的文本重复输入到TextBox。
Button
我们将描述的下一个控件是Button:
这是我们按钮的代码:
<Button Content="Button"
Padding="10,5"
Grid.Row="1"/>
Padding="10,5"以上表示按钮从其内容向左和向右延伸 10个通用像素以及在顶部和底部延伸5个像素。Grid.Row="1"表示按钮位于第二行Grid(第一行被表头占据)。
按钮定义Click路由事件——单击按钮时触发。有三种方法可以在单击按钮时调用C#代码:
- 使用后面的代码——如NP.Demos.SimpleAvaloniaProject示例中所示(使用最差的方法)。
- 使用可以绑定到View Model属性的Command属性。View Model属性反过来可以定义要调用的lambda表达式,当单击按钮时,还可以定义控制按钮是否启用的属性。这将在以后的文章中详细介绍。
- 使用监听Click路由事件的行为并在事件触发时调用C#方法。(这也将在后面描述)——这是最好的方法。
ListBox
ListBox显示能够一次选择一个项目的项目集合。如果项目数超过ListBox的大小,它将显示滚动条。
使用列表框的最佳方法是将其Items属性绑定到集合。如何做到这一点将在下面显示。在我们的例子中,我们只是在XAML代码中创建了ListBoxItem来填充它:
<ListBox x:Name="TheListBox">
<ListBoxItem Content="Item 1"/>
<ListBoxItem Content="Item 2"/>
<ListBoxItem Content="Item 3"/>
<ListBoxItem Content="Item 4"/>
</ListBox>
ListBox最重要的属性是上面提到的和选择相关的Items属性:SelectedIndex和SelectedItem。这个ListBox的SelectedIndex绑定到下一小节中描述的ComboBox的SelectedIndex,因此当您更改它们中的每一个上的选定项目时,另一个将以相同的方式做出反应。
ComboBox
ComboBox在各种其他框架中也被叫做DropDownBox。就像ListBox还存储项目集合一样,但始终只显示选定的项目——其他项目仅在鼠标指针单击其右侧的箭头时显示在Popup窗口(或更确切地说是下拉菜单)中。上图显示ComboBox没有选定项目但打开的dropdown。这是我用来创建和填充的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的主要属性是Items(绑定到集合)SelectedIndex和SelectedItem。该示例显示了如何将ComboBox的SelectedIndex绑定到ListBox它左侧的: SelectedIndex="{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到它旁边的属性,以便它们同步更改。
CheckBox
CheckBox与ToggleButton非常相似,但看起来不同(如您所见)。这是CheckBox的XAML代码:
<CheckBox Content="Check Box"
VerticalAlignment="Top"
Grid.Row="1"
IsChecked="{Binding Path=IsChecked, ElementName=TheToggleButton}"/>
您可以看到将其IsChecked属性连接到左侧ToggleButton属性的绑定。
CheckBox还有一个switch属性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是一个控件,它在所谓的Popup的PlacementTarget旁边打开一个轻量级窗口:
Popup窗口是否打开由其IsOpen属性控制(和反映),在我们的例子中,该IsChecked属性与ToggleButton控制(和反映)Popup窗口状态的属性相关联:
<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}"
StaysOpen="False"
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属性,以便更改它们中的每一个都会影响另一个。
StaysOpen属性设置为false——意味着单击Popup区域将关闭Popup窗口。
PlacementTarget属性指定Popup要定位的元素。
PlacementMode="Bottom"表示Popup将定位在放置目标的底部。
Window
当您单击“Window”示例的按钮时,将打开一个小窗口。
下面是窗口示例的XAML代码:
<Button x:Name="OpenWindowButton"
Content="Open Window"/>
打开窗口的C#代码是通过代码隐藏挂钩的——不是因为它是一种很好的方法(实际上它是上面提到的最糟糕的方法)——而是因为它是最简单的方法和最容易理解的方法。以下是MainWindow.asaml.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()方法以显示(打开)窗口。
Modal Window
模态窗口也称为对话框——它是一个在关闭之前阻止对其祖先窗口进行任何操作的窗口。
按“打开模态(对话)窗口”按钮将打开一个对话窗口,该窗口完全阻塞主窗口,直到它被关闭。这是XAML代码:
<Button x:Name="OpenModalWindowButton"
Content="Open Modal (Dialog) Window"/>
同样,我们使用不好的代码隐藏范例连接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是在定义该ToolTip元素的鼠标指针旁边打开的临时Popup窗口:
我们在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 基元
介绍
基元是不可组合的Avalonia UI控件(它们也不是面板),它们源自Control而不是源自TemplatedControl。在WPF中,它们甚至不会被称为控件,而是视觉对象。上面已经描述了其中一个基元——它是TextBlock元素。
除了上面介绍的以外,TextBlock最重要的基元是Border、Viewbox、Image和Shape。形状是派生自Shape的控件——其中最常用的是Path(对于任何形状)Line、Rectangle和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中——它将Border.Child属性设置为TextBlock。
- BorderThickness="10"——指定Border元素实际边框的大小。
- BorderBrush——是边框的颜色。
- Background指定边框内的颜色。
- CornerRadius——指定边框角的圆度。我故意制作了不同半径的所有4个角,以展示该CornerRadius属性的全部功能。
重要提示:我建议不要设置边框Child属性,而是将边框放置在Grid面板内,并将您希望位于边框内的元素放置在与边框兄弟相同的面板内。这是因为我的WPF经验是,如果边框有阴影(并且通常需要),它们的后代元素会稍微抖动。但是,不是边框的直接后代的元素不会受到影响。我还没有测试过它在Avalonia中是如何工作的——这个建议是基于我的WPF经验。
Viewbox
Viewbox是一个控件,允许在视觉上缩小或扩展其中的所有内容。就像边框一样,它具有放置内容的Child属性。您可以在其中放置任何复杂的视觉元素或包含许多视觉元素的面板,并且所有内容都将根据Viewbox参数在视觉上调整大小。
Viewbox将从其后代那里获得大小。Viewbox的子控件总是会缩小到最小尺寸,即使它的HorizontalAlignment和VerticalAlignment属性设置为Stretch,它自己的后代也允许。如果Viewbox的子控件没有指定大小并且不能从它自己的子控件那里得到大小,例如,一个Grid没有Height和Width没有任何可以定义其大小的子控件,它将缩小到大小为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"。
如果将拉伸设置为None——视图框的子级根本不会调整大小——因此当您缩小窗口——您将切入如上所示的viewbox内容。
这是图片的Stretch="Fill":
Viewbox的子视图的width和height根据width和size的变化比例调整——在我们的例子中,我们使width变小(变薄),而使高度变大。在"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"
Items="{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”,而不是埃及希克索斯的首都。
Shapes
形状是——各种几何形状。以下是“形状”选项卡的外观:
您可以使用Stretch,StrokeThickness和Fill属性。您可以看到Rectangle和Ellipse不受Stretch影响,这是有道理的,因为它们是由它们的width和height决定的。
Line和Path受到Stretch的影响,就像Image和Viewbox一样。
StrokeThickness决定边框的粗细。Stroke属性(类型IBrush)确定边框的颜色。
Fill指定内部的颜色——在我们的例子中,当HasFill checkbox打开时,Fill是red,当它不打开时,Fill是null(本质上是透明的,但也命中测试不可见)。
这是该Line的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}}"/>
主要的line定义属性是确定line的起点StartPoint和EndPoint终点。
Rectangle和Ellipse两者的形状由它们的width-height决定——它们应该明确指定,或者由它们的容器为它们提供多少空间。
Path是最通用的形状。它由类型Geometry的Data属性决定。为不同的形状创建几何图形本身就是一门科学,超出了本教程的范围。对于现成的几何图形,您可以转到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解决方案下。
Panels 示例
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替换为Orentation="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属性可以采用值Left、Top、Right和Bottom。
在我们的示例中,我们有8个按顺时针方向排列的按钮——Left、Top、Right,Bottom然后是Left、Top、Right,Bottom最后一个按钮占据了其余空间:
这是我们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是一个面板,它允许通过左上角的坐标来放置控件,由附加属性Canvas.Left和Canvas.Top决定::
在上面的canvas上,按钮被放置在距离左上角右侧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 Brushes
介绍
就像WPF一样,Avalonia具有:
- SolicColorBrush表示单色的类
- LinearGradientBrush表示颜色在一个方向空间变化的类
- RadialGradientBrush基于某个点周围的椭圆曲线表示颜色变化的类。
除了WPF Brushes之外,Avalonia Brushes也有ConicGradientBrush等级。
该Brushes示例位于NP.Demos.Brushes.sln解决方案下:
Brushes
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允许RenderTransform在每个Avalonia控件(与WPF相同)上执行,但LayoutTransform只能在LayoutTransformControl上执行。如果您需要对任何控件执行LayoutTransform,你可以让它成为LayoutTransformControl的子控件,并在LayoutTransformControl上执行LayoutTransform,如我们的示例所示。
如果您尝试运行示例并更改RotationTransform Rotation角度,您会得到以下结果:
您可以看到Render Transformed按钮将显示在包含它的Grid面板之外,而Layout Transformed按钮将展开其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>
可以看到,为了实现布局转换,out 按钮被放置在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
缩放变换水平或垂直扩展或缩小控件。水平和垂直缩放分别由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模式、模板、样式、行为、安排代码以获得最佳编码等。