首页 > 其他分享 >10.路由事件

10.路由事件

时间:2024-01-31 15:44:07浏览次数:34  
标签:10 Widget 单击 事件 public 路由 冒泡

先看一段代码:

<Window x:Class="HelloWorld.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:HelloWorld" 
        xmlns:controls="clr-namespace:HelloWorld.Controls"
        xmlns:helper="clr-namespace:HelloWorld.MVVM"
        mc:Ignorable="d" 
        Title="WPF中文网 - www.wpfsoft.com" Height="350" Width="500">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Border>
        <Canvas>
            <Button Content="确定" Width="80" Height="30" Canvas.Left="150" Canvas.Top="100"/>
            <Button Content="取消" Width="80" Height="30" Canvas.Left="280" Canvas.Top="100"/>
        </Canvas>
    </Border>
</Window>

这个布局最终形成的逻辑树应该是下面这个样子

<Window>
    <Border>
        <Canvas>
            <Button/>
            <Button/>
        </Canvas>
    </Border>
</Window> 

在WPF的元素树中,若某一个元素引发了一个事件,那么这个事件会沿着整棵树进行传播,而开发者可以在事件传播的沿途进行侦听(有点像设立关卡打劫)。一旦侦听到这个事件,便可以执行事件的回调函数。当然,只有被声明为RoutedEvent路由事件才具备传播功能

隧道事件:

我们以上面的代码为例,假如用户单击了确定按钮,此时,从整个窗体的视角来看,窗体会说,哎呀,你把我给单击了,且单击的是我其中的确定按钮,于是,这个单击事件首先会经历第一个关卡——Window对象。如果开发者恰好订阅了Window对象的单击事件,那首先被执行的就是Window窗体的单击事件回调函数。紧接着单击事件会经历第二道关卡——Border对象,第三道关卡——Canvas对象,直到确定按钮为止。可见这个单击事件一路经历了4个控件,它们分别是Window->Border->Canvas->Button,开发者可以在这4个控件上去订阅单击事件。像这个从根目录一直路由到事件源对象的路由事件,我们称为隧道事件(预览事件),这类事件都是Preview单词开头

冒泡事件:

但是,从Button按钮的视角,用户肯定是先单击的我呀,我这里才是事件源,事件应该就像小孩向水中投石之后,平静的水面会泛起一圈圈的涟漪,最终消失在岸边——即最外层的Window窗体对象。那么,此时的事件路由方向就反过来了,Button->Canvas->Border->Window。像这种从事件源一直路由到元素树根的路由事件,我们称为冒泡事件。

直接事件:

除了这两种事件,WPF还支持直接事件,即只有事件源才能响应触发的事件,我们称为直接事件。三种事件由定义事件时通过RoutingStrategy 枚举进行标识。

public enum RoutingStrategy
{
    //
    // 摘要:
    //     路由事件使用隧道策略,以便事件实例通过树向下路由(从根到源元素)。
    Tunnel = 0,
    //
    // 摘要:
    //     路由事件使用冒泡策略,以便事件实例通过树向上路由(从事件元素到根)。
    Bubble = 1,
    //
    // 摘要:
    //     路由事件不通过元素树路由,但支持其他路由的事件功能。
    Direct = 2
}

我们为Window、Border、Canvas、Button的PreviewMouseUp隧道事件都订阅了回调函数,然后F5调试运行,并单击确定按钮,观察输出结果如下:

Window对象的隧道事件PreviewMouseUp被触发
Border对象的隧道事件PreviewMouseUp被触发
Canvas对象的隧道事件PreviewMouseUp被触发
Button确定按钮的隧道事件PreviewMouseUp被触发

击的确定Button按钮,但是首先被触发的却是元素树的根元素Window对象,最后才是Button对象

 

我们为Window、Border、Canvas、Button的MouseUp冒泡事件都订阅了回调函数,然后F5调试运行,并单击确定按钮,观察输出结果如下

Canvas对象的冒泡事件MouseUp被触发
Border对象的冒泡事件MouseUp被触发
Window对象的冒泡事件MouseUp被触发

总结:第一点,从输出结果看,隧道事件是从根元素路由到事件源,冒泡事件是从事件源路由到根元素。第二点,如果同时订阅了隧道事件和冒泡事件,那么两条路由路线都将执行。谁先谁后?隧道事件先完成路由

 

路由事件实践:

注册一个路由事件,是向WPF的事件系统注册的,由静态类EventManager提供注册服务,具体则交给RegisterRoutedEvent方法成员完成。

public static readonly RoutedEvent 路由事件名称 = EventManager.RegisterRoutedEvent(
            name: "路由事件名称",
            routingStrategy: 冒泡事件/隧道事件/直接事件,
            handlerType: 路由事件委托的反射实例,
            ownerType: 路由事件拥有者的反射实例;

当一个路由事件注册到WPF的事件系统之后,还需要利用event关键词对其进行包装,包装成普通事件的样子,方便开发者调用。

/// <summary>
/// 通过event包装成普通事件的外观
/// </summary>
public event RoutedEventHandler 普通事件名称
{
    add { AddHandler(路由事件名称, value); }
    remove { RemoveHandler(路由事件名称, value); }
}

最后,寻找合适的时候,利用UIElement基类中的RaiseEvent,触发这个路由事件即可。这样就完成了路由事件的注册流程。

<UserControl x:Class="HelloWorld.Controls.Widget"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:HelloWorld.Controls"
             mc:Ignorable="d" 
             x:Name="userControl"
             FontSize="30"
             Foreground="#666666"
             BorderBrush="#8CDDCD"
             d:DesignHeight="450" 
             d:DesignWidth="800">
    <Border BorderBrush="{Binding ElementName=userControl,Path=BorderBrush}">
        <Border.Style>
            <Style TargetType="Border">
                <Setter Property="Padding" Value="10"/>
                <Setter Property="Background" Value="White"/>
                <Setter Property="BorderBrush" Value="#8CDDCD"/>
                <Setter Property="BorderThickness" Value="0 3 0 0"/>
                <Setter Property="Margin" Value="0"/>
                <Style.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter Property="Background" Value="#F7F9F9"/>
                    </Trigger>
                </Style.Triggers>
            </Style>
        </Border.Style>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition Width="auto"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Value,StringFormat={}{0:C}}" 
                       Foreground="{Binding ElementName=userControl,Path=Foreground}" 
                       FontSize="{Binding ElementName=userControl,Path=FontSize}" />
            <TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding Title}" 
                       Foreground="{Binding ElementName=userControl,Path=Foreground}" 
                       FontSize="14" TextWrapping="Wrap"/>
            <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Icon}" 
                       Foreground="{Binding ElementName=userControl,Path=BorderBrush}" 
                       FontSize="26" Grid.RowSpan="2" VerticalAlignment="Center"/>
        </Grid>    
    </Border>
</UserControl>

Widget自定义控件的后端代码

/// <summary>
/// Widget.xaml 的交互逻辑
/// </summary>
public partial class Widget : UserControl
{  
    public Widget()
    {
        InitializeComponent();
 
        DataContext = this;
    }
 
    /// <summary>
    /// 注册RoutedEvent路由事件
    /// </summary>
    public static readonly RoutedEvent CompletedEvent = EventManager.RegisterRoutedEvent(
        name: "CompletedEvent",
        routingStrategy: RoutingStrategy.Bubble,
        handlerType: typeof(RoutedEventHandler),
        ownerType: typeof(Widget));
 
    /// <summary>
    /// 通过event包装成普通事件的外观
    /// </summary>
    public event RoutedEventHandler Completed
    {
        add { AddHandler(CompletedEvent, value); }
        remove { RemoveHandler(CompletedEvent, value); }
    }
 
    /// <summary>
    /// 触发路由事件
    /// </summary>
    void RaiseRoutedEvent()
    {
        RoutedEventArgs routedEventArgs = new RoutedEventArgs(CompletedEvent, this);
        RaiseEvent(routedEventArgs);
    }
 
 
    public string Icon
    {
        get { return (string)GetValue(IconProperty); }
        set { SetValue(IconProperty, value); }
    }
 
    public static readonly DependencyProperty IconProperty =
        DependencyProperty.Register("Icon", typeof(string), typeof(Widget), 
            new PropertyMetadata("❤"));
 
 
    public string Title
    {
        get { return (string)GetValue(TitleProperty); }
        set { SetValue(TitleProperty, value); }
    }
 
    public static readonly DependencyProperty TitleProperty =
        DependencyProperty.Register("Title", typeof(string), typeof(Widget), 
            new PropertyMetadata("请输入标题"));
 
    /// <summary>
    /// 销售目标
    /// </summary>
    public double Target
    {
        get { return (double)GetValue(TargetProperty); }
        set { SetValue(TargetProperty, value); }
    }
 
    public static readonly DependencyProperty TargetProperty =
        DependencyProperty.Register("Target", typeof(double), typeof(Widget), 
            new PropertyMetadata(0.0));
 
 
 //依赖属性
    public double Value
    {
        get { return (double)GetValue(ValueProperty); }
        set { SetValue(ValueProperty, value); }
    }
 /*当Value大于Target时,触发Completed事件*/
    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register("Value", typeof(double), typeof(Widget),
            new PropertyMetadata(0.0,new PropertyChangedCallback(OnValuePropertyChangedCallback)));
 
    private static void OnValuePropertyChangedCallback(DependencyObject d, 
        DependencyPropertyChangedEventArgs e)
    {
        if(d is Widget control && e.NewValue is double value )
        {
            if (value >= control.Target && control.Target != 0)//当业绩大于100万美元时
            {
                control.Icon = "☻";                    
                control.RaiseRoutedEvent();//触发路由事件,完成销售目标
            }
            else
            {
                control.Icon = "❤";
            }
        }
    }
}

我们在ValueProperty 的回调函数中去判断销售业务,如果完成销售目标,就引发完成事件。接下来演示怎么使用这个控件。首先在主窗体中实例化Widget,并将其Value绑定到一个Slider的Value

<Window x:Class="HelloWorld.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:HelloWorld" 
        xmlns:controls="clr-namespace:HelloWorld.Controls"
        xmlns:helper="clr-namespace:HelloWorld.MVVM"
        mc:Ignorable="d" Background="Lavender"
        MouseUp="Window_MouseUp" PreviewMouseUp="Window_PreviewMouseUp"        
        Title="WPF中文网 - www.wpfsoft.com" Height="350" Width="500">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Border MouseUp="Border_MouseUp" PreviewMouseUp="Border_PreviewMouseUp" 
            Background="Transparent">
        <Canvas MouseUp="Canvas_MouseUp" PreviewMouseUp="Canvas_PreviewMouseUp" 
                Background="Transparent">
            
            <controls:Widget Value="{Binding ElementName=slider,Path=Value}" 
                             Target="1000000"
                             Completed="Widget_Completed"
                             Title="第四季度北美市场总销售额统计" 
                             Canvas.Left="116" Canvas.Top="17" 
                             Height="103" Width="269"/>
            
            <Slider x:Name="slider" Value="0" Maximum="2000000" 
                    Canvas.Left="116" Canvas.Top="138" Width="269"/>
            
            <ListBox x:Name="listBox" Height="133" 
                     Canvas.Left="116" Canvas.Top="167" Width="269"/>
        </Canvas>
    </Border>
</Window>

当用户拖动Slider时,Widget的Value值就跟着改变,并判断当前销售业绩与目标业绩,我们像订阅普通事件一样,Completed="Widget_Completed"表示订阅完成事件。

private void Widget_Completed(object sender, RoutedEventArgs e)
{
    Widget widget = sender as Widget;
    listBox.Items.Insert(0, $"完成目标销售额:{widget.Value}");
}

CompletedEvent路由事件在注册时被注册成RoutingStrategy.Bubble类型,即冒泡事件,如果想注册成隧道事件,建议取名为:PreviewCompletedEvent,并使用PreviewCompleted进行包装。

 

标签:10,Widget,单击,事件,public,路由,冒泡
From: https://www.cnblogs.com/MingQiu/p/17999408

相关文章

  • P3002 [USACO10DEC] Threatening Letter G
    https://www.luogu.com.cn/problem/P3002首先考虑一个显然的dp,设\(f_i\)表示最后一刀切在\(i\)上,并将\(1\simi\)全部剪出的最小刀数。转移显然是\(f_i=\min_{0\lej<i,t_{j+1\simi}\ins}f_j+1\),其中\(t_{j+1\simi}\)表示字符串\(t\)的子串\([j+1,i]\),\(t\ins\)......
  • 洛谷题单指南-暴力枚举-P1036 [NOIP2002 普及组] 选数
    原题链接:https://www.luogu.com.cn/problem/P1036题意解读:题目即要在n个数中,枚举出所有的子集,当子集中数字个数刚好为k时,求和,判断是否是素数。解题思路:方法一:二进制法通过二进制法,可以枚举一个集合中所有元素“选”或者“不选”的情况,用二进制1表示选该元素,二进制0表示不选。......
  • 解决win10照片查看器无法使用的问题
    把下面的内容,复制到.Reg的文件中,然后执行此文件。文件内容:WindowsRegistryEditorVersion5.00;ChangeExtension'sFileType[HKEY_CURRENT_USER\Software\Classes\.jpg]@="PhotoViewer.FileAssoc.Tiff";ChangeExtension'sFileType[HKEY_CURRENT_USER\Softwa......
  • 34进制转化为10进制
    最近遇到了34进制转化为10进制的问题,记录下。将34进制的字符串'H2V'转换成为10进制数,进而转换为日期。百度了下34进制的资料,记录如下:34进制指以0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、F、G、H、J、K、L、M、N、P、Q、R、S、T、U、V、W、X、Y、Z为基数做累加。0-9和A-Z......
  • D20XB100-ASEMI整流桥D20XB100参数、封装、规格
    编辑:llD20XB100-ASEMI整流桥D20XB100参数、封装、规格型号:D20XB100品牌:ASEMI正向电流(Id):20A反向耐压(VRRM):1000V正向浪涌电流:300A正向电压(VF):1.05V引脚数量:5芯片个数:4芯片尺寸:MIL功率(Pd):大功率设备封装:GBJ-5工作温度:-40°C~150°C类型:插件、整流桥D20XB100描述:ASEMI品牌D20XB100是采......
  • D20XB100-ASEMI整流桥D20XB100参数、封装、规格
    编辑:llD20XB100-ASEMI整流桥D20XB100参数、封装、规格型号:D20XB100品牌:ASEMI正向电流(Id):20A反向耐压(VRRM):1000V正向浪涌电流:300A正向电压(VF):1.05V引脚数量:5芯片个数:4芯片尺寸:MIL功率(Pd):大功率设备封装:GBJ-5工作温度:-40°C~150°C类型:插件、整流桥D20XB100描述:ASEMI......
  • Go语言的100个错误使用场景(11-20)|项目组织和数据类型
    目录前言2.Codeandprojectorganization2.11没有使用函数式选项模式(#11)2.12项目缺乏组织(#12)2.13创建公共设施包(#13)2.14忽略包名的冲突(#14)2.15代码文档缺失(#15)2.16不使用code-linter(#16)3.Datatypes3.1八进制产生的混乱局面(#17)3.2忽略整型溢出(#18)3.3不理解浮点数(#19)3......
  • 如何在vue3项目app.ts中获取第三方跳转过来的token 提前处理携带token情况的初始化 两
    如何在vue3项目app.ts中获取第三方跳转过来的token提前处理携带token情况的初始化两种方式路由守卫和window.location在Vue3项目的app.ts文件中获取第三方跳转过来的token,你可以使用VueRouter的route对象来获取URL参数。假设你的token参数位于URL的查询字......
  • T100 背景执行遇到的问题
    如cwssp500里生成订单的操作里会背景执行cxmp500这个程序去更新库存档: 如果想要其他角色用户也能够执行这个功能的话就必须在azzi850里赋予权限(下图是赋予给了客服人员这个角色的权限):如果不在此处给对应角色施加权限的话,那么这背景执行的功能只有在你管理员角色下才会有效,而......
  • 这样加个中间件,接口速度提升 1000%
    本文是在开发mockm周边过程中的创作。它可以快速生成api以及创造数据,开箱即用,便于部署,恳求不吝提出宝贵意见。动机最近在做一个curd项目,这里我们代名为myApi,用于实现0代码、无需声明模型、自动实现增删改查一些列的接口,支持任意关系型数据库。经过几天的努力,终于把基......