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());
}
很簡單,也很明瞭了。