從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變數中不放任何資料。