1. 程式人生 > >WPF學習(5)-路由事件

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());
        }

很簡單,也很明瞭了。