WPF学习(5)-路由事件

先从winform开始说起,下面是一个窗体,有三个按钮控件,每个按钮有一个按钮处理方法,弹窗,展示是第几个按钮被按下了。

按其中任何一个按钮都只会显示自己的那个按钮弹窗。

上图非常清晰地看到,按钮绿色框框表示被点击,然后对应弹出窗体。

同样的布局,我们用wpf来实现,同时输出是第几个按钮触发。

可以看到,当我们点击最里面的按钮的时候,先触发最里面那个按钮,接着是第二层,最后是最外层。

当我们点击第二层按钮的时候,先触发第二层按钮的事件,再出发最外层的事件。

那么非常清晰,我们的路由事件就有了定义,当某个事件会沿着逻辑树传递,这个事件就是路由事件,我们对比winform的三个按钮事件,都是独立的,其中一个触发了,其他的都不会触发,这个就是普通事件。

wpf的界面,由树组成,这棵树按照松紧程度,分为逻辑树和视觉树,其实和大家了解的html都差不多。比如,我们打开VS的文档大纲,看到的就是一棵逻辑树。我们利用.NET自带的两个系统类,来遍历逻辑树和视觉树。

     

 private void PrintLogicTree(int depth, object obj)        
{             
Console.WriteLine(depth + " " + obj);                      
if (!(obj is DependencyObject))                
return;
foreach (object child in LogicalTreeHelper.GetChildren(obj as DependencyObject))                
PrintLogicTree(depth + 1, child);        
}

首先,我们使用LogicalTreeHelper来遍历逻辑树,打印结果如下。

0 WpfApplication1.MainWindow
1 System.Windows.Controls.Grid
2 System.Windows.Controls.Button: 按钮
3 System.Windows.Controls.Button: 按钮
4 System.Windows.Controls.Button: 按钮
5 按钮 

看到最上面其实相当于是从上到下的树干, 是我们的mainwindow,下面是大的树枝,GRID,在下面是嵌套的三层button,最后则是最里面的button的内容,就相当于最小的那片树叶,由大到小,一层一层折叠,最后形成了我们整个界面,多说一句,只有图形界面才有树的概念,别的是没有的。

接着,我们遍历视觉树。

     public void PrintVisualTree(int index, Visual visual)       
 {            
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)            {                
Visual childVisual = (Visual)VisualTreeHelper.GetChild(visual, i);                Console.WriteLine(index + " " + childVisual);                                 PrintVisualTree(index + 1, childVisual);            
}        
}

结果如下

0 System.Windows.Controls.Border
1 System.Windows.Documents.AdornerDecorator
2 System.Windows.Controls.ContentPresenter
3 System.Windows.Controls.Grid
“WpfApplication1.vshost.exe”(CLR v4.0.30319: WpfApplication1.vshost.exe):  已加载“C:\Windows\Microsoft.Net\assembly\GAC_MSIL\PresentationFramework.resources\v4.0_4.0.0.0_zh-Hans_31bf3856ad364e35\PresentationFramework.resources.dll”。模块已生成,不包含符号。
4 System.Windows.Controls.Button: 按钮
5 System.Windows.Controls.Border
6 System.Windows.Controls.ContentPresenter
7 System.Windows.Controls.Button: 按钮
8 System.Windows.Controls.Border
9 System.Windows.Controls.ContentPresenter
10 System.Windows.Controls.Button: 按钮
11 System.Windows.Controls.Border
12 System.Windows.Controls.ContentPresenter
13 System.Windows.Controls.TextBlock
2 System.Windows.Documents.AdornerLayer 

可以看到视觉树的元素更多,简单理解就是把逻辑树进一步扩展,比如button,是有border,ContentPresenter,TextBlock进一步组成的,在我看来,视觉树暂时并不需要更多地去理解,只要把目光放到逻辑树上,因为wpf的每个方面,包括属性,事件,资源等等都是依赖逻辑树的,下节,我将会通过datacontext,数据上下文来解释逻辑树究竟干了些什么。

进一步阐述逻辑树的意义,先上代码,前台就是两个label和textbox,显示人的名字和年龄。

 <Label Content="姓名" HorizontalAlignment="Left" Margin="83,67,0,0" VerticalAlignment="Top"/>        
<TextBox  HorizontalAlignment="Left" Height="25" Margin="150,67,0,0" TextWrapping="Wrap" Text="{Binding Path=Name}" VerticalAlignment="Top" Width="120"/>        
<Label Content="年龄" HorizontalAlignment="Left" Margin="83,125,0,0" VerticalAlignment="Top" />        
<TextBox HorizontalAlignment="Left" Height="25" Margin="150,125,0,0" TextWrapping="Wrap" Text="{Binding Path=Age}" VerticalAlignment="Top" Width="120"/>


然后两个textbox的text绑定一个到Name,一个到Age,当然我们需要有一个类,比如我们做养老院定位系统,里面的老人类,只放两个属性,姓名和年龄,如下

   class Older
    {
        private int age;
        public int Age
        {
            get { return age; }
            set { age = value; }
        }
        private string name;
        public string Name
        {
            get { return name; }
            set { name = value; }
        }              
    }

然后在窗体加载的时候,new一个老人的对象,并且把这个老人对象直接给整个窗体的数据上下文。

 Older o = new Older();
            o.Name = "洪波";
            o.Age = 31;
            this.DataContext = o;

当我们运行的时候,结果如下

自动就显示到了,其实这个就是逻辑树的用处,我们窗体上的textbox,绑定了一个路径的Name和Age的,首先textbox会找自己的数据上下文,由于咱们没有定义,那么就顺着逻辑树向上爬,找grid的datacontext,咱们还是没有赋值,继续顺着逻辑树去找,哎,this.datacontext,咱有值了,于是就自动绑定上了,所以很明显,数据上下文是整个树上共享的。

如果我们把绑定路径故意写错,会怎么样呢?

<TextBox HorizontalAlignment="Left" Height="25" Margin="150,125,0,0" TextWrapping="Wrap" Text="{Binding Path=Age2}" VerticalAlignment="Top" Width="120"/>

没有报错,程序正常运行,只不过没有显示,这个就是数据上下文的灵活之处,当然也提醒我们,千万别绑定错了,其他逻辑树的更进一步的,咱们可以以后慢慢体会,到这里基本上对于新手就足够了,下面咱们正式进入路由事件的分析。 

       简单的说,就是可以在逻辑树中传递的事件,就类似于我们之前说的datacontext,还用咱们之前的三个按钮的例子来说明。

    <Grid>        
<Button  HorizontalAlignment="Left" Margin="70,50,0,0" VerticalAlignment="Top" Width="379" Height="211" Click="Button_Click" >           
 <Button Width="255" Height="121"  Click="Button_Click_1"  >                
<Button Content="按钮" Width="75" Click="Button_Click_2"/>           
 </Button>        
</Button>    
</Grid>

     private void Button_Click(object sender, RoutedEventArgs e)
        {
            Console.WriteLine("最外层按钮");
          
        } 

        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
          
              Console.WriteLine("第二层按钮");
          
        }

        private void Button_Click_2(object sender, RoutedEventArgs e)
        {
           // e.Handled = true;
            Console.WriteLine("第三层按钮");
         
        }

GRID里面嵌套了三个button,每个按钮都有一个click事件,点击显示按钮顺序,结果如下

第三层按钮
第二层按钮
最外层按钮

这个就是基本的冒泡事件,我们按了最里面第三层的按钮,首先最里面按钮事件被触发,接着第二层被触发,最后最外面被触发,从细枝到树干。

接着,我们试一下阻止冒泡,也很简单

  private void Button_Click_2(object sender, RoutedEventArgs e)
        {
            e.Handled = true;
            Console.WriteLine("第三层按钮");
         
        }

结果如下

第三层按钮

第三层之后handled设置为true,就是告诉系统,不要再继续路由了。

换一个思路理解下,其实,你点击最里面的按钮,因第二层是他的父元素,肯定也被点击到了,那么做为第二层父元素的最外层,也被点击到了,那么自然要三个按钮事件都被触发了,这个冒泡事件是最好理解的了。

接着我们看隧道路由事件,上代码

<Button Width="255" Height="121"  Click="Button_Click_1" PreviewMouseDown="Button_PreviewMouseDown" >
                <Button Content="按钮" Width="75" Click="Button_Click_2"/>
            </Button>

  private void Button_PreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            Console.WriteLine("我是隧道事件");

        }

结果如下

我是隧道事件
第三层按钮
第二层按钮
最外层按钮

首先,隧道事件被触发,然后再冒泡顺序往上爬,你只要看preview这个前缀,就明白了,预览,预先,先执行,他执行的顺序和冒泡相反,从根元素开始再到子节点,具体就不写了,很简单。

是不是wpf就有这两种路由事件呢?不是,看代码

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

      开始自定义路由事件

      首先,事件肯定有几个要素,定义事件(什么样的行为或数据发送变化触发该事件),事件的触发(什么时候触发这个事件),事件处理程序(触发事件后干什么),那么我们一步步来。

    </Grid>     

   <Button Content="Button" HorizontalAlignment="Left" Margin="92,95,0,0" VerticalAlignment="Top" Width="105" Height="25" Click="Button_Click"/> 

   <Grid>

一个控件上只有一个按钮。自定义一个事件

 public static readonly RoutedEvent ClickEvent;

注意这个是静态的,而且是只读的,是不是很眼熟?就和依赖属性很像很像的东西。

 UserControl1.ClickEvent = EventManager.RegisterRoutedEvent("随便", RoutingStrategy.Bubble, typeof(RoutedEvent), typeof(UserControl1));
 接着,注册事件,注册事件有专门的类EventManager,里面有个注册方法,参数,看注释就行拉。

为什么要注册呢?如果不注册,你在别的地方要触发这个事件,根本就找不着呀。

   private void Button_Click(object sender, RoutedEventArgs e)
        {      
          
            RoutedEventArgs rea = new RoutedEventArgs(UserControl1.ClickEvent, this);
            this.RaiseEvent(rea);
        }

按下按钮,我们来触发这个事件,也很简单,一个参数对象,然后直接Raise就行了,接着我们就是去绑定触发事件后的事件处理程序啦,有人可能会疑问,我们看别人写的好多地方,都有事件包装器,你这个怎么没有啦?

       这个先放这边,后面某一讲,我们会详细解释,你也可以自己思考一下

回到我们刚新建的过程,里面主页面,咱们直接添加事件处理函数

            this.AddHandler(UserControl1.ClickEvent, new RoutedEventHandler(myfunc));

    private void myfunc(object sender,RoutedEventArgs e)
        {
            MessageBox.Show("Test");
        }

执行结果如下

这里讲一个疑问,我为何这么麻烦,直接在窗体上按钮直接点击click事件处理不就可以了,那么麻烦,比如你的业务有很多类似的比如增删改查的需求,每一个页面都是增加,删除,修改,然后下面一个datagrid,当然你可以一个个单独的页面去写,但是多麻烦,自定义一个用户控件,里面放上三个按钮,一个datagrid,注册三个事件,增加,修改,删除,不同模块的时候,事件处理程序不一样,就可以了,这样你的代码看起来就很优雅,而且会变小。

当然大部分时候,你的参数会多样,那么就自定义参数好了。

     public class MyEventArgs : RoutedEventArgs
     {
         public MyEventArgs(RoutedEvent routedEvent, object source)  
             :base(routedEvent,source){}  
         public DateTime EventTime { get; set; } 
      
     }

、触发事件的地方改一下参数

  MyEventArgs rea = new MyEventArgs(UserControl1.ClickEvent, this);
            rea.EventTime = DateTime.Now;
            this.RaiseEvent(rea);

再改一下事件处理程序

    private void myfunc(object sender,RoutedEventArgs e)
        {
            MyEventArgs me = (MyEventArgs)e;
            MessageBox.Show(me.EventTime.ToString());
        }

很简单,也很明了了。
 

猜你喜欢

转载自blog.csdn.net/whjhb/article/details/84372349