1. 程式人生 > 實用技巧 >從WinForm程式中看委託和事件

從WinForm程式中看委託和事件

作為一個自學C#的小白,無論我們的學習起點是各種書籍還是視訊,最開始總是從控制檯程式和窗體應用程式,一行簡單的Console.WriteLine("Hello World");或者是一個窗體幾個控制元件就能實現一個小程式。作者本意或許是想告訴初學者們,程式設計並不難,並且很有趣。消除學生的畏難情緒,培養學習興趣。但是,這會不會在學生心中留下一個印象,程式設計不過如此?從此很長一段時間內,程式設計水平只是停留在拖控制元件。我們所不知道的是,這種簡單背後,是功能強大的visual studio和C#語言規範在支撐。在我學習委託和事件時,在部落格園,程式設計書,視訊網站各種渠道去找資料,卻總是一知半解。並不是這些資料講解的不到位,相反,每篇文章都會在某一點上對我有新的啟發,如果沒有這些資料,今天我也寫不出這篇文章,只是在看資料之後,之前的我沒有真正的去思考,更準確一點說,是不知道如何去思考。

這篇文章不敢說寫的多好,存在錯誤也絕非我所願,這並不是一句謙辭,實在是我目前程式設計水平有限,也歡迎看到這篇文章的朋友能夠指出其中錯誤。我願意把它當成一個起點,從這裡開始,一步步去積累提高自己。

就從窗體應用程式說起吧

在建立專案時,vs自動生成的Form1類,繼承了Form類。在主程式program.cs中,建立了Form1類的例項,並呼叫Application.Run(new Form1())來執行。這裡有兩點需要注意:

1、 Form1類就是一個普通類,和我們自己後面新增的類檔案沒有什麼不同。

2、 Form1類的修飾符partial,這個類有兩部分組成:第一部分是開發人員自己編寫的程式程式碼,存放在Form1.cs檔案,我們在vs中選中窗體按F7顯示的那些程式碼。第二部分是Form1.Designer.cs檔案,這裡的程式碼是vs自動生成的,都是和Form1窗體中的控制元件有關的,可以認為該檔案負責管理窗體中的所有控制元件。

先來看Form1.Designer.cs(後面用Form1類代替,知道這些程式碼是寫在該檔案中即可)

其中包含兩個方法,Dispose()和InitializeComponent(),還有一些私有欄位,如panel1,button1。Dispose()是在Form1類中實現父類Form類中的虛方法,釋放Form1類所佔用的資源,不用多說。從InitializeComponent()中,可以知道當我們在vs中向一個窗體拖動一個控制元件時,究竟發生了什麼?

例如,向一個空窗體中新增一個button按鈕,這是每個學習winform程式設計的人第一節課必備操作,此時vs的操作是:

1、 在Form1類中宣告一個私有欄位 button1

private System.Windows.Forms.Button button1;

button1是System.Windows.Forms.Button型別的變數,轉到Button類的定義,可以看到該類繼承了System.Windows.Forms.ButtonBase和System.Windows.Forms.IButtonControl,而System.Windows.Forms.ButtonBase繼承了System.Windows.Forms.Control類。到此為止,我們等下再去仔細研究Control類的內容。

【拓展:和Button類似,Winform中其他控制元件類也都是這種方式實現的。如果想自己設計一款控制元件,就可以按照這種方式來實現。先設計一個MyComponent類,讓它繼承自Control類,然後在MyComponent類中實現該控制元件的特有功能】

2、 vs做的第二件事,是在InitializeComponent()方法中對button1變數進行了例項化:

this.button1 = new System.Windows.Forms.Button();

我們轉到Button類、ButtonBase類、Control類的定義中可以看到,其中有許多屬性

3、 vs做的第三件事就是對button1物件的常用屬性進行了初始化(這裡我擷取一部分)

//

// button1

//

this.button1.BackColor = System.Drawing.Color.ForestGreen;

this.button1.Font = new System.Drawing.Font("Microsoft YaHei", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));

this.button1.Location = new System.Drawing.Point(20, 273);

如果我們後面在設計器中對控制元件的其他屬性做了修改,vs也會將程式碼新增到這裡。

到此為止,在我們沒有對button1控制元件做任何操作之前,vs為我們做了以上三件事,來簡化程式設計流程,讓我們更專注於Form1.cs檔案中程式碼的開發。

按鈕控制元件最常用的功能就是點選Click,下面來看下這個過程是怎麼實現的。

在設計器中雙擊該控制元件(button控制元件的預設事件就是click),vs在Form1.cs中會自動生成並跳轉到button1_Click方法,我們在這個方法體內部實現button1按鈕被點選時程式要執行的操作。

private void button1_Click(object sender, EventArgs e)

{

//

}

可以看到,該方法有兩個引數,第一個是object型別,sender;第二個是System.EventArgs型別,e。這兩個引數是什麼意思,會留到很後面再說,在這裡簡單提一下。

同時,vs在Form1.Designer.cs中,會生成如下程式碼:

this.button1.Click += new System.EventHandler(this.button1_Click); (1)

這行程式碼什麼意思?它跟我們點選按鈕要實現的功能有什麼關係?

舉個例子,你用美團app定了一份水餃,+=符號就是“訂”這個動作。+=左邊就表示水餃,而右邊明確指出這是一份牛肉水餃。(簡單理解)

+=:稱為委託操作符,字面意思可理解為“訂閱”。

在它的左邊是this.button1.Click是一個“事件Event”,檢視其定義可以發現,這是一個宣告在Control類中的東西(在不知道它到底是啥之前,暫且稱之為東西):

public event EventHandler Click; (2)

這東西既不是屬性,也不是欄位,更不是方法,看起來更像變數宣告。從宣告中可以看出,有一個event關鍵字。這個東西是EventHandler型別的。

我們從EventHandler型別入手,在vs中F12轉到EventHandler型別的定義,可以看到以下內容:

public delegate void EventHandler(object sender, EventArgs e); (3)

很明顯,這又是一個宣告,看起來和方法宣告差不多,多了一個delegate關鍵字,這個單詞有“授權,委託”的意思,當我們再想看看delegate是什麼東西的時候,vs出現了“無法導航到插入符號下的符號”彈窗。

到這裡,我們既沒有搞明白+=左邊的this.button1.Click到底是什麼?它跟Control類中的Click有什麼關係?而且還多了一個delegate關鍵字和event關鍵字不知道是幹嘛的。

這個時候把程式編譯下,看原始碼。需要弄明白

public delegate void EventHandler(object sender, EventArgs e);

這句程式碼到底做了什麼。這句程式碼的字面意思是宣告一個名為EventHandler的委託,顯然,delegate是宣告委託的關鍵字。

從下面這張圖可以看出,當宣告GreetingDelegate委託時,編譯器自動宣告一個密封類,類名就是GreetingDelegate。所以宣告委託就是宣告類,委託本質上就是一個類。

類中有一個建構函式和BeginInvoke、EndInvoke、Invoke方法。

——圖片引用自https://www.cnblogs.com/JimmyZhang/archive/2007/09/23/903360.html,作者是《.NET之美》一書的作者,有興趣的朋友可以看下。

我們知道,String類表示字串,Int32類表示32位有符號整數。既然委託就是類,那EventHandler類表示什麼?

“為了增強靈活性和減少重複程式碼,可以將方法作為引數傳遞給另一方法。為了能將方法作為引數傳遞,必須要有一個能表示方法的資料型別。這個資料型別就是委託”

                            ——《C#本質論第六版》

現在我們知道,EventHandler類表示方法。C#中方法成千上萬,都用一個類肯定不能表示。所以一個委託只能表示一類方法。哪一類方法呢?

看一下最開始vs自動生成的button1_Click方法:

private void button1_Click(object sender, EventArgs e)

委託宣告:

public delegate void EventHandler(object sender, EventArgs e)

二者相同點有返回值,引數型別和引數順序。所以,委託就表示它宣告中返回值,引數型別和順序相同的一類方法。

現在回到(2),現在我們知道這個Click就是一個EventHandler型別的“事件”,表示一個引數是(object sender, EventArgs e)的方法。這裡要注意,Click是一個事件,而不是一個委託。

event關鍵字是幹嘛用的呢?這就涉及到publish-subscribe模式和委託的缺點。具體內容見《C#本質論第六版》P378-384。簡單來講,就是委託的封裝不充分,而事件的封裝更充分,更不容易出錯,事件是一種特殊的委託。所以在publish-subscribe模式中,我們使用事件。

最後一個問題,我們知道Click事件宣告在Control類中,而Button類通過ButtonBase類,間接地繼承於Control類,所以Button類的例項button1自然也可以呼叫Click事件了。

總結一下,等號左邊的this.button1.Click是一個事件,是EventHandler型別的一個變數。

+=右邊,就很簡單了,使用new關鍵字,例項化一個EventHandler型別的物件。這個物件指向Form1類中button1_Click方法

this.button1.Click += new System.EventHandler(this.button1_Click);

這行程式碼含義:為該類中button1_Click()方法訂閱button1.Click事件。

問題在於,為什麼是+=,而不是=?

我們習慣了下面這種寫法:把右邊的物件賦值給左邊的變數。

String str = “abc”;

FileInfo f = new FileInfo(@“”);

這就是事件的作用,也就是前面提到的,為什麼事件比委託封裝的更充分,更不易出錯。具體內容見C#本質論P383“高階主題:事件的內部機制”

這裡只要知道,在呼叫事件時,賦值操作符是禁用的。只能使用+=或-=來訂閱或者取消訂閱。

繼續說,

我們廢了這麼多勁去訂閱這個事件,為了什麼呢?答:人機互動。

當用戶點選了介面上的按鈕(Click事件被觸發,實際上是呼叫了Click事件的Invoke()方法,前面有提到),就會呼叫button1_Click方法,程式就會繼續執行來執行某種我們希望的操作。

在控制檯應用程式中,Control類是Click事件的釋出者(Publisher),而Form1類中的button1_Click()是Click事件的訂閱者(Subscriber)。當Click.Invoke()方法被呼叫,釋出者就會通知所有訂閱者。

假設這樣一種情況,如果釋出者中包含訂閱者感興趣的資料,這些資料對訂閱者的執行至關重要,資料應該如何傳遞給訂閱者??

更直接一點,委託宣告:

public delegate void EventHandler(object sender, EventArgs e)

sender和e是什麼?EventArgs是什麼型別?要弄明白這個問題,再看一段新的程式碼:

 1 using System;
 2 
 3 namespace ConsoleApp1
 4 {
 5     public class Thermostat
 6     {
 7         public class TemperatureArgs:EventArgs
 8         {
 9             public float NewTemperature { get; set; }
10 
11             public TemperatureArgs(float newTemperature)
12             {
13                 this.NewTemperature = newTemperature;
14             }
15 
16         }
17 
18         public event MyEventHandler<TemperatureArgs> OnTemperatureChange;
19 
20         private float currentTemperature;
21         public float CurrentTemperature
22         {
23             get { return currentTemperature; }
24 
25             set
26             {
27                 if (value != currentTemperature)
28                 {
29                     currentTemperature = value;
30 
31                     OnTemperatureChange?.Invoke(this, new TemperatureArgs(value));
32                 }
33             }
34         }
35         public int Number { get; set; }
36 
37     }
38 }
 1 using System;
 2 
 3 
 4 namespace ConsoleApp1
 5 {
 6     class Program
 7     {
 8         static void Main(string[] args)
 9         {
10             Heater heater1 = new Heater(90);
11             
12             Cooler cooler = new Cooler(60);
13             Thermostat thermostat1 = new Thermostat() { CurrentTemperature =0,Number=1};
14             Thermostat thermostat2 = new Thermostat() { CurrentTemperature = 0, Number = 2 };
15 
16             thermostat1.OnTemperatureChange += heater1.OnTemperatureChanged;
17             thermostat2.OnTemperatureChange += heater1.OnTemperatureChanged;
18 
19             thermostat1.OnTemperatureChange += cooler.OnTemperatureChanged;
20 
21             thermostat1.CurrentTemperature = 100;    //溫度變化
22             Console.ReadKey();
23 
24             Console.WriteLine("Hello World");
25         }
26     }
27 
28     //宣告泛型委託
29     public delegate void MyEventHandler<TEventArgs>(object sender, TEventArgs e)
30         where TEventArgs : EventArgs;
31 
32     public class Heater
33     {
34         public Heater(float temp)
35         {
36             this.Temperature = temp;
37         }
38 
39         public float Temperature;
40 
41         public void OnTemperatureChanged(object sender,Thermostat.TemperatureArgs e)
42         {
43             Thermostat thermostat = (Thermostat)sender;
44             if (thermostat.Number==1)
45             {
46                 Console.WriteLine("Thermostat Number : {0}",1);
47             }
48             else if (thermostat.Number==2)
49             {
50                 Console.WriteLine("Thermostat Number : {0}", 2);
51             }
52 
53             float newTemperature = e.NewTemperature;
54 
55             if (newTemperature>Temperature)
56             {
57                 Console.WriteLine("Cooler:ON");
58             }
59             else
60             {
61                 Console.WriteLine("Cooler:OFF");
62             }
63         }
64     }
65 
66     public class Cooler
67     {
68         public Cooler(float temperature)
69         {
70             this.Temperature = temperature;
71         }
72         public float Temperature;
73 
74         public void OnTemperatureChanged(object sender, Thermostat.TemperatureArgs  e)
75         {
76             float newTemperature = e.NewTemperature;
77 
78             if (newTemperature<Temperature)
79             {
80                 Console.WriteLine("Heater:ON");
81             }
82             else
83             {
84                 Console.WriteLine("Heater:OFF");
85             }
86         }
87     }
88 }

基本功能介紹:Thermostat類代表恆溫器,控制水溫保持在一定範圍內。Heater類和Cooler類分別是加熱器和冷卻器,負責調節水溫。恆溫器能夠獲取當前水溫值,加熱器和冷卻器根據當前水溫值和預設水溫值水溫值比較,做出相應動作。

在該例中,Thermostat是“溫度變化”事件的釋出者,Heater類和Cooler中OnTemperatureChanged()是該事件訂閱者,並需要獲取當前溫度值。

首先,在主程式中宣告泛型委託:

public delegate void MyEventHandler<TEventArgs>(object sender, TEventArgs e)

where TEventArgs : EventArgs;

這是一種比較規範的寫法,理論上任何委託型別都可以使用。

第一個引數sender:是呼叫委託的那個類的例項。它有什麼作用呢?

假設有兩個Thermostat的例項,Heater.OnTemperatureChanged()訂閱了這兩個例項中的OnOnTemperatureChange事件。此時,任何一個例項都可能觸發對OnTemperatureChanged()的呼叫。判斷具體是哪個Thermostat例項觸發了事件,需要在Heater.OnTemperatureChanged()內部利用sender引數進行判斷。

我們為Thermostat類增加Number屬性,代表恆溫器編號。在主程式中建立兩個Thermostat物件thermostat1和thermostat2,並將編號分別裝置1,2,初始溫度均為0.

在Heater.OnTemperatureChanged()中,將sender轉換為Thermostat物件。並根據該物件的Number屬性值執行相應的操作。

第二個引數e:它包含了事件的附件資料。資料型別是TEventArgs,從泛型約束中可知,該類繼承自EventArgs類,EventArgs類的定義中只有一個Empty屬性,用來指出不存在事件資料。

我們在TEventArgs類中添加了一個新屬性NewTemperature,用於將溫度從恆溫器傳遞給訂閱者。所以這個e引數就是我們用來傳遞訂閱者感興趣的資料的。

剛才的Number屬性也可以新增在TEventArgs類中作為感興趣資料傳遞出去,作用都是一樣的。

一種最簡單,也最常用的委託宣告,就是前面提到的EventHandler委託:

Public delegate void EventHandler(object sender,EventArgs e)

類比上面的說明,button1_Click()方法訂閱了this.button1.Click事件,我們在該方法中就可以利用sender引數來訪問button1物件的各種資料,這裡我以訪問Text屬性為例。

Button button1 = (Button)sender;

string text = button1.Text;

PS:這並不是最簡單的方法,這裡只是說明下可以這麼用。直接用this.button1.Text簡單。

這麼宣告委託就是一種簡便方法,我把所有資料都宣告在呼叫委託的類中,使用sender就能訪問到所有資料,e變數中不放任何資料。