【C#】跨平台UI库Avalonia的学习笔记(下)

3 篇文章 2 订阅

Hello,这是下篇
太多了,打算分P了

上篇请看这里

模板

Implementing IDataTemplate

实现IDaataTemplate接口,如果你希望对DataTemplate有更多的控制,那么你可以通过自己创建类然后实现IDataTemplate接口。通过这种方式,不仅可以修改表示数据类型的方式,还可以做任何希望的修改。

为了实现这个接口,你需要实现两个方法

  1. public bool Match(object data)
  2. public IControl Build(object param)
    Match方法,你需要检查方法是不是匹配你提供的数据模板
    Build方法,你需要返回你希望呈现你的数据的控件
    官方也是提供了一个例子,我们看例子
    在这里插入图片描述
    看看工程目录结构
    在这里插入图片描述
    Model定义了用来选择是哪种形状的枚举数据类型
    在这里插入图片描述
    ViewModel中定义了ShapeType属性,用来给前台操作,做了依赖绑定
    MainWindow中定义了界面,就是一个文本框+一个组合框
<Window x:Class="IDataTemplateSample.Views.MainWindow"
        xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:dataTemplates="using:IDataTemplateSample.DataTemplates"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:vm="using:IDataTemplateSample.ViewModels"
        xmlns:model="using:IDataTemplateSample.Models"
        Title="IDataTemplateSample"
        d:DesignHeight="450"
        d:DesignWidth="800"
        x:CompileBindings="true"
        x:DataType="vm:MainWindowViewModel"
        Icon="/Assets/avalonia-logo.ico"
        mc:Ignorable="d">

    <Design.DataContext>
        <vm:MainWindowViewModel />
    </Design.DataContext>

    <Window.DataTemplates>
        <dataTemplates:ShapesTemplateSelector>
            <DataTemplate x:Key="RedCircle" DataType="model:ShapeType">
                <Ellipse Width="50"
                         Height="50"
                         Fill="Red"
                         Stroke="DarkRed"
                         StrokeThickness="2" />
            </DataTemplate>
            <DataTemplate x:Key="BlueCircle" DataType="model:ShapeType">
                <Ellipse Width="50"
                         Height="50"
                         Fill="Blue"
                         Stroke="DarkBlue"
                         StrokeThickness="2" />
            </DataTemplate>
            <DataTemplate x:Key="RedSquare" DataType="model:ShapeType">
                <Rectangle Width="50"
                           Height="50"
                           Fill="Red"
                           Stroke="DarkRed"
                           StrokeThickness="2" />
            </DataTemplate>
            <DataTemplate x:Key="BlueSquare" DataType="model:ShapeType">
                <Rectangle Width="50"
                           Height="50"
                           Fill="Blue"
                           Stroke="DarkBlue"
                           StrokeThickness="2" />
            </DataTemplate>
        </dataTemplates:ShapesTemplateSelector>
    </Window.DataTemplates>

    <StackPanel>
        <TextBlock Text="Select a Shape" />

        <ComboBox Items="{Binding AvailableShapes}"
                  SelectedIndex="0"
                  SelectedItem="{Binding SelectedShape}" />
    </StackPanel>

</Window>

在这里插入图片描述
组合框的选项由原来的文本变成了这种图形的样子,这个使用DataTemplate做的
可以看到数据模板中,前台为每一种图形单独定义了对应的处理,并且给每一个模板标记了独立的Key
后台是重点,我们看看代码
前台录了样式,然后后台把它加到模板里面这种模式

using Avalonia.Controls.Templates;
using Avalonia.Controls;
using Avalonia.Metadata;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using IDataTemplateSample.Models;

namespace IDataTemplateSample.DataTemplates
{
    public class ShapesTemplateSelector : IDataTemplate
    {
        // 用字典来存储图形, 并且标记属性 [Content], 这样待会可以直接用Add添加
        [Content]
        public Dictionary<string, IDataTemplate> AvailableTemplates { get; } = new Dictionary<string, IDataTemplate>();

        // 构建数据模板
        public IControl Build(object param)
        {
            //判断是不是空
            var key = param.ToString(); // Our Keys in the dictionary are strings, so we call .ToString() to get the key to look up
            if (key is null) // If the key is null, we throw an ArgumentNullException
            {
                throw new ArgumentNullException(nameof(param));
            }
            return AvailableTemplates[key].Build(param); // 添加数据模板,然后系统自动构建
        }

        // 检查数据是不是能够录入进来
        public bool Match(object data)
        {
            // Our Keys in the dictionary are strings, so we call .ToString() to get the key to look up
            var key = data.ToString();

            return data is ShapeType                       // the provided data needs to be our enum type
                   && !string.IsNullOrEmpty(key)           // and the key must not be null or empty
                   && AvailableTemplates.ContainsKey(key); // and the key must be found in our Dictionary
        }
    } 
}

Autoring Controls

Types of Control

如果你希望创建自己的控件,在Avalonia中有3种控件。

  1. 自定义控件
  2. 模板控件
  3. 基本控件
    自定义控件,UserControl最适合特定应用的View或者Page。UserControl的创建就和创建Window类似,也是从一个模板开始,一点点添加控件的。

模板控件,适合在不同的应用中共享的通用控件。他们是没有具体外观的控件,这意味着可以针对不同的注意和应用重新设计。Avalonia中大部分基本控件都是这一类。

在Avalonia中的模板控件,继承自TemplatedControl

如果希望给TemplatedControl以样式,请使用单独的文件,然后用StyleInclude

基础控件,用户界面的急促,重写自Visual.Render方法,TextBlock和Image等控件就是这一类

在Avalonia中,基础控件继承自Control

Define Properties

如果你需要创建控件,你会给控件定义属性,通过AvaloniaPropertys定义属性.属性包含两部分,属性定义和CLR的属性Setter和Getter.

Registering Styled Properties

你需要在定义控件的时候也把样式定义了。样式属性保证你的控件在Avalonia下工作正常。

你通过调用AvaloniaProperty.Register来注册样式,然后将结果存储在static readonly字段中。然后你接着创建标准的C#属性来访问它。

public static readonly StyledProperty<Brush> BackgroundProperty =
    AvaloniaProperty.Register<Border, Brush>(nameof(Background));

public Brush Background
{
    get { return GetValue(BackgroundProperty); }
    set { SetValue(BackgroundProperty, value); }
}

AvaloniaProperty.Register方法同时也接收其他几个参数:

  • 默认值defaultValue,属性的默认值。确定仅传递值类型和不可变的类型,因为传递引用类型将导致在注册该属性的时候都是用同样的属性。
  • 继承inherits,指定属性的默认值继承自哪一个父控件
  • 默认绑定模式defaultBindingMode,属性的默认绑定模式有OneWay,TwoWay,OneTime或者OneWayToSource.
  • 验证validate,验证函数的类型是Func<TOwner, TValue, TValue>.这个函数接收需要设置属性的类实例和正确应该返回的值或者抛出的异常值

样式属性与其他框架中的DependencyProperty是类似的

属性的命名与对应Avalonia所支持的属性字段是很重要的,字段的名称始终是属性的名称,并在后面附加Property

Using a StyledProperty on Another Class

有时你希望添加到自己控件的属性在别的控件里已经存在了。例如Background,如果希望注册定义于其他控件的属性到自己的控件,可以使用StyleProperty.AddOwner

public static readonly StyledProperty<IBrush> BackgroundProperty =
    Border.BackgroundProperty.AddOwner<Panel>();

public Brush Background
{
    get { return GetValue(BackgroundProperty); }
    set { SetValue(BackgroundProperty, value); }
}
Readonly Properties

为了创建一个只读的属性,你可以创建AvaloniaProperty.RegisterDirect方法

public static readonly DirectProperty<Visual, Rect> BoundsProperty =
    AvaloniaProperty.RegisterDirect<Visual, Rect>(
        nameof(Bounds),
        o => o.Bounds);

private Rect _bounds;

public Rect Bounds
{
    get { return _bounds; }
    private set { SetAndRaise(BoundsProperty, ref _bounds, value); }
}

只读属性作为一个字段存储在对象中。当注册属性时,Getter函数被用来通过GetValue来访问对应的属性值。然后SetAndersonRaise被用于当属性变化时通知监听器。

Attached Properties

Attached Properties附件属性使用RegisterAttached方法注册,访问器被定义为static方法
下面的例子定义了Grid中的Grid.Column是如何被附加属性的

public static readonly AttachedProperty<int> ColumnProperty =
    AvaloniaProperty.RegisterAttached<Grid, Control, int>("Column");

public static int GetColumn(Control element)
{
    return element.GetValue(ColumnProperty);
}

public static void SetColumn(Control element, int value)
{
    element.SetValue(ColumnProperty, value);
}
Direct AvaloniaProperties

RegisterDirect并不是仅用来注册只读属性的,它也可以通过传递Setter到RegisterDirect中来扩展标准C#属性作为Avalonia属性

StyledProperty通过AvaloniaProperty.Register来注册,维护着一个绑定优先级列表和起作用的样式值。但是对于很多属性来说,它不会被设置属性,也不需要样式

public static readonly DirectProperty<ItemsControl, IEnumerable> ItemsProperty =
    AvaloniaProperty.RegisterDirect<ItemsControl, IEnumerable>(
        nameof(Items),
        o => o.Items,
        (o, v) => o.Items = v);

private IEnumerable _items = new AvaloniaList<object>();

public IEnumerable Items
{
    get { return _items; }
    set { SetAndRaise(ItemsProperty, ref _items, value); }
}

Direct Properties 是一种轻量化的样式属性

支持

  1. AvaloniaObject.GetValue
  2. AvaloniaObject.SetValue for non-readonly properties
  3. PropertyChanged
  4. Binding (only with LocalValue priority)
  5. GetObservable
  6. AddOwner
  7. Metadata

不支持

  1. Validation/Coercion (although this could be done in the property setter)
  2. Overriding default values.
  3. Inherited values
Direct vs Styled Property

通常情况下,你需要定义你自己的属性为样式属性。但是,direct属性有优点和缺点。

优点:

  1. 不需要为属性弄额外的对象分配
  2. Getter是标准的C#Getter
  3. Setter是标准的C#Setter,通过依赖绑定事件
    缺点
  4. 不能从父对象继承值
  5. 无法使用Avalonia的样式系统
  6. 属性值是一个字段,因此无法确定属性是不是设置到了对象上

所以,使用direct 属性有以下要求

  • 属性不需要样式
  • 属性一直是保持不变的,一个值

事件路由

绝大多数的事件是路由事件,是从整个事件树中发起的而不是仅是控件发起的

什么是路由事件

一个典型的应用包括很多部件,不管是代码定义还是xaml定义,这些部件都包含在部件树中,并与其他部件产生关系。事件路由可以根据事件的定义是单向传输或者双向传输,但是一般的事件路由从源部件开始,然后“冒泡”向组件树的上方传递,直到传递到根节点(一般是window or page). 这种“冒泡“理论和HTML DOM操作很像

事件路由的顶层场景

控件的组合和封装
绝大多数的控件都有富文本模型,例如可以填充一个图片在按钮控件里,但是添加图片并不会影响按钮正常的响应click事件,即使用户点击到的仅仅是图片的一部分像素。

单一处理连接

在windows form 中,你需要附加同一个事件处理很多次去处理多个组件产生的事件。事件路由可以仅仅只需要附加一次,使用处理逻辑去决定事件的来源

private void CommonClickHandler(object sender, RoutedEventArgs e)
{
	var source = e.Source as Control;
	switch (source.Name)
	{
		case "YesButton":
			// do something here ...
			break;
		case "NoButton":
			// do something ...
			break;
		case "CancelButton":
			// do something ...
			break;
	}
	e.Handled=true;
}
类处理器

事件路由允许由类定义的静态处理,这个处理类有机会在任何处理实例附加之前处理事件。

无需反射的事件引用

具体的代码和装饰的技术需要一系列的方式去识别具体的事件。事件路由创建了一个RoutedEvent 字段来当做识别,而不需要静态变量和反射

事件路由是如何执行的

事件路由是由RoutedEvent类实现的CLR事件,并注册到Avalonia事件系统中的。RoutedEvent实例对象通常是以public static readonly的形式存在并注册路由事件的。与它同名的 CLR 事件(有时称为“Wrapper”事件)的连接是通过重写 CLR 事件的添加和删除方法实现来完成。通常,添加和删除方法保留为隐式默认值,使用适当的的事件语法用于添加和删除该事件的处理程序。事件路由背后的连接机制和Avalonia使用AvaloniaProperty类处理和注册CLR属性是类似的。

下面的例子展示了如何定义一个自定义Tape事件,包括注册和定义RoutedEvent,和实现该事件的add和remove方法。

//定义RoutedEvent实例
public static readonly RoutedEvent<RoutedEventArgs> TapEvent =
RoutedEvent.Register(nameof(Tap), RoutingStrategies.Bubble);

//提供事件CLR访问器
public event EventHandler<RoutedEventArgs> Tap
{
	add => AddHandler(TapEvent, value);
	remove => RemoveHandler(TapEvent, value);
}
事件处理器和XAML

通过XAML添加事件处理器,你需要将事件名称作为事件侦听器的元素上的属性。属性的值是具体的处理方法,必须在后台代码中存在。

<Button Click="b1SetColor">button</Button>

XAML 添加标准CLR事件处理器的语法和添加事件路由的语法是一样的。实际上是向CLR事件包装器添加对应的处理程序。
目前事件处理器必须要定义为public,后续会考虑解除这个限制。

事件路由的策略

事件路由提供三种策略:

  1. Bubbling冒泡。事件处理器从事件源被调用。事件路由接下来会不断往其父组件上路由,一直路由到组件树的根节点。大多数的路由事件使用冒泡机制。冒泡机制一般用来上报输入或者控件的状态变化或者其他UI组件
  2. Direct直接。仅源组件本身有机会响应调用事件处理。这个是类似与Windows Form的事件路由。但是它又不想标准的CLR事件,直接事件路由支持类处理器。
  3. Tunneling隧道。最初的时候,事件树的根节点的事件处理器被调用执行。事件路由接着路由到其子组件节点,一直路由到产生事件的事件源的组件。隧道路由事件通常作为组合式组件的一部分,这样来自组合式组件的事件可以被人为抑制不发生或替换为该组合式控件的事件。在Avalonia中输入事件一般包含Tunneling和Bubbling事件。
为啥要使用事件路由

作为一个开发者,你不需要总是知道或者在意你正在处理的事件作为事件路由被执行了。事件路由有特殊特性,但是这个特性在触发该事件的控件上处理事件时很大程度是不可见的。

这时事件路由就变得很有用了。假设这样的使用场景。在公共的根节点定义了一个公共的处理方法,由其他组件组成自己的控件或者定义属于自己的自定义控件类。

事件路由监听和事件路由源不需要共享在他们所属的层次下的公共事件。任何控件都可以时任何事件路由的事件监听器。因此,你可以将整个工程中可用的事件路由集合作为一个统一的“接口”,应用程序中的不同元素可以在这个接口中交换事件信息。这个“接口”的概念特别适用于输入事件。

事件路由还可以在这个组件树之间通信,因为事件在每一个组件路由过程中的事件数据将永久保存。一个组件对事件数据做些改变,这些改变可以在路由到下一个组件时可用。

抛开路由的概念,还有另外两个原因让Avalonia选择路由而不是标准的CLR事件。设想如果你自己执行自己定义的事件,你可能会考虑下面的原则:

  1. 某些样式和模板的概念要求引用的事件是事件路由。这个就是前面提到的事件标识符。
  2. 事件路由支持类处理器机制,因此这个类可以指定静态方法,这些方法有机会早于其他任何注册的实例处理器处理并访问事件路由。这在界面设计的时候是很有用的,因为你自己的类可以强制实施事件驱动类的的特性,可以不会错误的被处理实例上的事件打断,抑制。
为事件路由添加并执行事件处理器

为了使用XMAL添加事件处理器,你可以简单的为一个组件添加一个事件名作为属性,然后设置属性的名称作为事件的名称。
例如

<Button Click="b1SetColor">button</Button>

b1SetColor 是对应这个按钮的Click事件的事件处理器。它的原型必须和RoutedEventHandler 委托一样,对应的是Click事件的委托。所有事件路由委托的第一个参数对应的具体哪一个组件被添加了事件,第二个参数对应的是被指定给事件数据的参数。

void b1SetColor(object sender, RoutedEventArgs args)
{
  //logic to handle the Click event
}

RoutedEventHandler是一个基本的事件路由委托,对于在某些控件某些场景下的事件路由来说,对应的事件路由委托可能会更具体,因此他们可以传输更具体的事件数据。例如,在通用的输入场景中,你可能处理一个PointerPressed事件路由。你的处理函数需要执行这样的委托(RoutedEventHandler)。通过使用更加具体的委托,你可以处理PointerPressedEventArgs ,然后读取PointerEventArgs.Pointer属性,它包含了鼠标指针被按下的具体信息。

使用代码为应用添加事件路由的处理器更加直接。事件路由可以通过helper方法AddHandler 来添加(存在于后台有一个一样的方法去调用add方法)。但是,既有的Avalonia事件路由存在后台执行add和remove的逻辑,允许事件路由的处理函数去添加具体事件的语句,这个对于helper方法来说更加直观吧。例如:

void MakeButton()
{
    Button b2 = new Button();
    b2.AddHandler(Button.ClickEvent, Onb2Click);
}

void Onb2Click(object sender, RoutedEventArgs e)
{
    //logic to handle the Click event     
}

或者更加酷炫的写法,+号大法

void MakeButton2()
{
  Button b2 = new Button();
  b2.Click += Onb2Click2;
}

void Onb2Click2(object sender, RoutedEventArgs e)
{
  //logic to handle the Click event     
}
事件处理器的概念

所有的事件路由都是共享同一个事件数据基类RoutedEventArgs,它定义了Handler属性,接受bool变量值。Handler属性的目的是通过将其设置为True表示在任意事件处理函数在路由的过程中标记的事件路由已经被处理了。在路由过程中,事件处理器被一个组件处理了之后 ,事件数据会被共享到整条路由路径上的每一个监听者。

Handler的值影响事件路由如何上报和处理,因为它会一路跟着事件路由传递下去。如果Handler在事件路由的事件数据中的是True,那么接下来其他组件的事件路由监听事件就不会再调用对应的事件处理函数了。对于XMAL添加的事件处理函数还是说具体代码添加的事件处理函数都是一样的。对于通用的事件处理场景,标记Handler为True就会停止路由了,不管是Tunneling隧道模式还是Bubbling冒泡模式,还是类处理器的模式。

但是,这里有个handledEventsToo的机制,对于监听者还是可以继续运行对应的事件路由处理函数,即使事件数据的Handled 已经被设置为True了。换句话说,事件路由通过设置Handler为True并不会真正的停止。仅能通过代码来使用handledEventsToo 机制。

在代码中,调用Avalonia 独有的方法AddHandler(RoutedEvent, EventHandler handler, RoutingStrategies, bool) 来添加你的处理函数,并指定handledEventsToo 为True。

除了Handled状态在事件路由中产生事件处理的行为之外,它的设思想对如何编写事件处理代码有重要意义。你可以将这个Handled的概念扩展成一个简单的事件路由协议,它的设计思想如下:

  • 如果事件路由被标记为Handled,那么它在路由路径上的其他组件就不需要再处理了
  • 如果事件路由没有被标记为Handled,那么位于路由路径上较为靠前的其他的监听者可以选择不处理该事件,或者已经注册了对应处理事件的组件可以选择不操作 该事件,并设置它为True。或者当前监听器可能是事件路由中的第一个节点,那么就有三种可能的操作流程。
  1. 完全不采取措施。保持该事件是未处理的,路由到下一个监听者那去
  2. 执行该事件的处理代码,但是在这一步的操作还不能把这个事件标记为已处理,继续路由到下一个监听者继续处理
  3. 执行该事件的处理diamagnetic,标记Handled已经处理完了,这一步的操作已经可以处理完了。但是依然需要路由到下一个监听事件,这种情况下需要特殊的handledEventsToo监听者才能继续有机会监听处理该事件。

在前面这个路由的概念已经被强化设计了,即使位于路由线路中较早的节点处理程序已经将Handled设置为True了,后续的路由事件将会很难再处理这个事件了,虽然可以通过handledEventsToo来实现。

在实际的程序中,只处理冒泡路由事件,而根本不关心事件路由机制的本身是很常见的。但是,将事件路由数据中的Handled标记为处理完了依然是很好的,这样可以防止后续再其他组件中有相同的处理事件又处理一遍。

类处理器

如果你定义了一个类继承自AvaloniaObject,那么你也可以给事件路由定义或者附加一个类处理器。该事件路由可以是类的声明或者继承自事件成员。每当事件路由通过其路由路径到达了该控件,类处理器会在任何实例化的事件监听器之前被调用。

一些Avalonia 控件对于一些事件路由是通过固有的类处理完成的。从外部来看,像是事件路由没有真正发生似的,但是实际上它是被类处理器处理了,事件路由通过一些手段还是可以继续处理事件的。另外,很多基类和控件对外暴露了可以重新类控制器特性的虚拟方法给开发者。

为了附加类控制器给自己定义的控件,可以通过静态构造方法AddClassHandler

static MyControl()
{
    MyEvent.AddClassHandler<MyControl>((x, e) => x.OnMyEvent(e));
}

protected virtual void OnMyEvent(MyEventArgs e)
{
    // Handle event here.
}
Avalonia附加事件

通过XMAL可以定义一种特殊的事件叫做附加事件。附加事件允许你为任意控件的特定的事件添加一个处理器。这个控件对于的事件处理器不需要定义或者继承自附加事件,并且可能引发事件的对象和目标处理实例都不需要将“附加事件”定义或者拥有为类成员。

Avalonia输入系统使用附加事件进行扩展。但是,几乎所有这些附加事件都是通过基类元素进行转发的,然后输入事件作为基类元素成员等效于非附加的事件路由。例如,Gestures.Tapped可以通过Tapped很轻易的附加给Control类的控件,而不需要额外的写代码或者XMAL。

XMAL中一个合格的事件名

Another syntax usage that resembles typename.eventname attached event syntax but is not strictly speaking an attached event usage is when you attach handlers for routed events that are raised by child elements.

这句话我也没想好到底怎么翻译。。。。

你可以附加一个处理器给一个公共的父组件,甚至是公共的父组件可能都没有相关事件路由成员。
例如

<Border Height="50" Width="300">
  <StackPanel Orientation="Horizontal" Button.Click="CommonClickHandler">
    <Button Name="YesButton">Yes</Button>
    <Button Name="NoButton">No</Button>
    <Button Name="CancelButton">Cancel</Button>
  </StackPanel>
</Border>

在这里,父组件监听器的处理函数是添加在StackPanel中的。但是,实际上这个处理器是给Button类对象使用的。按钮拥有这个事件,但是事件路由系统允许将任意的事件路由处理函数附加在任何的控件事件监听器上,否则这些监听器可以为CLR事件附加监听器。这些附加事件的事件名对应的命名控件就是默认的Avalonia xmlns命名空间,当然你也可以指定自定义的命名空间。

输入事件

在Avalonia框架下常有到的事件路由是输入事件。输入事件常以一对出现。一个为冒泡事件,而另一个为隧道事件。偶尔,输入事件只有冒泡事件,或者是直接路由事件。

Avalonia输入事件常以一对出现来执行。用户触发输入,例如鼠标按下按钮,会按顺序触发一堆事件路由。首先,隧道事件被触发,然后开始路由。两个数据共享相同的事件数据,因为在具体实现类中引发冒泡事件RaiseEvent 方法调用了隧道事件中的事件数据,并在新的事件触发后重用它。具有隧道事件处理器的监听器首先会有机会将事件标记为Handled(首先是类处理器,然后是实例化的处理器)。如果隧道路由路径上的元素将事件已经标记为了Handled,则会为冒泡事件发送已处理了的事件数据,并且常见的附件给等效的冒泡输入事件的处理器不会被调用。从外表上看,就好像冒泡事件没有被触发似的。这个特性在组合式的控件中很有用。因为在组合式的控件中,用户可能希望最终的控件(而不是其中的一部分)报告所有的基于命中了的输入事件或者是基于焦点的输入事件。最终的控件会更接近根。因此有机会首先对隧道事件进行类处理,并且能将该事件替换成更符合于该控件本身的事件。

下面的图片展示了输入事件是如何处理的,子控件#2是PointerPressed的事件源。
在这里插入图片描述
事件触发的顺序如下

  1. PointerPressed位于根控件,通过隧道事件往子节点路由
  2. PointerPressed位于中间控件#1,通过隧道事件往子节点路由
  3. PointerPressed位于子控件#2,通过隧道事件往子节点路由
  4. PointerPressed位于子控件#2,通过冒泡事件往根节点路由
  5. PointerPressed位于中间控件#1,通过冒泡事件往根节点路由
  6. PointerPressed位于根控件,通过冒泡事件到达根节点

事件路由处理委托提供两个对象引用,一个是引发是引发事件的对象,一个是调用事件处理器的对象。调用事件处理器的对象是被sender参数上报的。第一个引发事件的对象是被事件数据中Source属性上报的。一个事件路由可以依旧被同一个对象触发和处理,这是sender和Source是完全一样的(注意看步骤3和4)

因为隧道和冒泡模式,父控件接收到了输入事件,那里Source属性对应的是其中一个他的子控件。这里需要清楚事件源控件是什么?通过识别Source属性来分辨事件源组件是哪个?

经常的,一旦输入事件被标记为了Handled,后面的事件处理器就不执行了。典型的,应该在调用事件处理器时立即将输入事件标记为Handled,此时的程序时为了解决特定输入事件的。

例外的情况是输入事件处理器被注册为忽略Handled状态的输入事件处理器,这样的话它仍然将会沿着任一路由路径继续调用下去。

某些类处理器处理输入事件,通常是为了重新定义该控件中特定的输入事件的具体含义,并引发新的事件。

剪贴板

Avalonia提供了访问剪贴板的接口IClipboard ,通过Application.Current.Clipboard访问当前应用的剪贴板。

await Application.Current.Clipboard.SetTextAsync("Hello World!");
var text = await Application.Current.Clipboard.GetTextAsync();

也可以存储对象到剪贴板,但是IOS和安卓都不支持

record Person(string Name, int Age);

var person = new Person("Peter Griffin", 58);

var dataObject = new DataObject();
dataObject.Set("my-app-person", person);

await Application.Current.Clipboard.SetDataObjectAsync(dataObject);

var storedPerson = (Person) await clipboard.GetDataAsync("my-app-person");

鼠标或指针设备

在Avalonia中将各种鼠标,触摸,笔等统一抽象成指针设备。对这种控件提供以下事件

  • PointerEnter
  • PointerLeave
  • PointerMoved
  • PointerPressed
  • PointerReleased
  • PointerWheelChanged

这里如何对鼠标按键按下事件做定义,获取坐标和哪一个键按下

control.PointerPressed += (args) =>
{
    var point = args.GetCurrentPoint();
    var x = point.Position.X;
    var y = point.Position.Y;
    if (point.Properties.IsLeftButtonPressed)
    {
        // left button pressed
    }
    if (point.Properties.IsRightButtonPressed)
    {
        // right button pressed
    }
}

每一个控件有自己特殊的手势事件。单击和双击。单击指的是当控件的指针被按下时,然后被释放。双击指的是同一个位置按下了两次。对于触摸设备来说,前后两次按下的距离和事件间隔会比桌面设备的大。

快捷键

很多控件都实现了ICommandSource 接口存在快捷键属性,可以设置或者绑定命令。按下快捷键会执行绑定给控件的命令。

<Menu>
    <MenuItem Header="_File">
        <MenuItem x:Name="SaveMenuItem" Header="_Save" Command="{Binding SaveCommand}" HotKey="Ctrl+S"/>
    </MenuItem>
</Menu>

另外也可以使用静态方法HotKeyManager使用代码设置或者获取快捷键

InitializeComponent();
var saveMenuItem = this.FindControl<Avalonia.Controls.MenuItem>("SaveMenuItem");
HotKeyManager.SetHotKey(saveMenuItem, new KeyGesture(Key.S, KeyModifiers.Control));

组合键

一个快捷键可能是由组合键组成的。当使用XMAL设置HotKey属性设置快捷键时,对应的键值文本会传递给KeyGesture。Enum.Parse(将一个或多个枚举常数的名称或数字值的字符串表示转换成等效的枚举对象)用来解析组合键

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值