[.net 物件導向程式設計基礎] (22) 事件
事件(Event)是學習.net物件導向程式設計很重要的一部分,在學習事件之前,我們實際上已經在很多地方使用了事件,比如控制元件的click事件等,這些都是.net設計控制元件的時候已經定義好的事件。除此之外,我們同樣可以自己定義事件。
事件實際上是一種訊息機制,當然點選控制元件時,click就通知處理他的方法去處理,實際上就是前面說的委託。因此我們可以說:事件是一種具有特殊簽名的委託。而事件/訊息機制是windows的核心,因此我們必須掌握他。
為了更加容易理解事件,我們還是使用前面的動物的示例來說明,有兩三隻動物,貓(名叫Tom),還有兩隻老鼠(Jerry和Jack),當貓叫的時候,觸發事件(CatShout),然後兩隻老鼠開始逃跑(MouseRun)。在使用程式碼實現這個示例之前,我們先看一下事件的書面定義.
1.什麼是事件(Event)?
事件(Event)是類或物件向其他類或物件通知發生的事情的一種特殊簽名的委託.
2.事件的宣告
public event 委託型別 事件名;
事件使用event關鍵詞來宣告,他的返回類值是一個委託型別。
通常事件的命名,以名字+Event 作為他的名稱,在編碼中儘量使用規範命名,增加程式碼可讀性。
3.事件示例
下面我們實現本篇開始的貓捉老鼠的示例
首先看一下UML圖
如上UML類圖,有貓(Cat)和老鼠(Mouse)兩個類,裡面包含其成員.當貓叫(CatShout)時,觸發事件(CatShoutEvent),事件通知老鼠,然後老鼠跑路(MouseRun).
兩個類的程式碼如下:
1 class Cat 2 { 3 string catName; 4 string catColor { get; set; } 5 public Cat(string name, string color) 6 { 7 this.catName = name; 8 catColor = color; 9 } 10 11 public void CatShout() 12 { 13 Console.WriteLine(catColor+" 的貓 "+catName+" 過來了,喵!喵!喵!\n"); 14 15 //貓叫時觸發事件 16 //貓叫時,如果CatShoutEvent中有登記事件,則執行該事件 17 if (CatShoutEvent != null) 18 CatShoutEvent(); 19 } 20 21 public delegate void CatShoutEventHandler(); 22 23 public event CatShoutEventHandler CatShoutEvent; 24 25 } 26 class Mouse 27 { 28 string mouseName; 29 string mouseColor { get; set; } 30 public Mouse(string name, string color) 31 { 32 this.mouseName = name; 33 this.mouseColor = color; 34 } 35 36 public void MouseRun() 37 { 38 Console.WriteLine(mouseColor + " 的老鼠 " + mouseName + " 說:\"老貓來了,快跑!\" \n我跑!!\n我使勁跑!!\n我加速使勁跑!!!\n"); 39 } 40 }
呼叫如下:
1 Console.WriteLine("[場景說明]: 一個月明星稀的午夜,有兩隻老鼠在偷油吃\n"); 2 Mouse Jerry = new Mouse("Jerry", "白色"); 3 Mouse Jack = new Mouse("Jack", "黃色"); 4 5 6 Console.WriteLine("[場景說明]: 一隻黑貓躡手躡腳的走了過來\n"); 7 Cat Tom = new Cat("Tom", "黑色"); 8 9 Console.WriteLine("[場景說明]: 為了安全的偷油,登記了一個貓叫的事件\n"); 10 Tom.CatShoutEvent += new Cat.CatShoutEventHandler(Jerry.MouseRun); 11 Tom.CatShoutEvent += new Cat.CatShoutEventHandler(Jack.MouseRun); 12 13 Console.WriteLine("[場景說明]: 貓叫了三聲\n"); 14 Tom.CatShout(); 15 16 17 Console.ReadKey();
執行結果如下:
4.事件引數
上面的事件是最簡單的事件,通過我們看到的事件,都會帶兩個引數,比如c# winform中的button點選事件的委託方法如下:
private void button1_Click(object sender, EventArgs e)
帶有兩個引數,不熟悉事件引數的小夥伴肯定要問,這兩個引數sender和e到底有什麼用呢?
第一個引數 sender,其中object型別的引數 sender表示的是傳送訊息的物件,為什麼要使用object型別呢,這是因為不同型別的物件呼叫時使用object能很好的相容。
第二個引數 e,他的型別為EventArgs.EventArgs這個類沒有實際的用途,只是作為一個基類讓其他物件繼承。很多物件不需要傳遞額外的資訊,例如按鈕事件,只是呼叫一個回撥方法就夠了。當我們定義的事件不需要傳遞額外的資訊時,這時呼叫EventArgs.Empty就行了,不需要重新構建一個EventArgs物件。
我們可以看到在Button事件登記時,只傳了一個引數sender
為了更好的理解帶引數的事件,我們改寫一下上面貓捉老鼠的示例:
先看UML圖:
實現程式碼如下:
1 class Cat 2 { 3 string catName; 4 string catColor { get; set; } 5 public Cat(string name, string color) 6 { 7 this.catName = name; 8 catColor = color; 9 } 10 11 public void CatShout() 12 { 13 Console.WriteLine(catColor+" 的貓 "+catName+" 過來了,喵!喵!喵!\n"); 14 15 //貓叫時觸發事件 16 //貓叫時,如果CatShoutEvent中有登記事件,則執行該事件 17 if (CatShoutEvent != null) 18 CatShoutEvent(this, new CatShoutEventArgs() {catName=this.catName, catColor=this.catColor}); 19 } 20 21 public delegate void CatShoutEventHandler(object sender,CatShoutEventArgs e); 22 23 public event CatShoutEventHandler CatShoutEvent; 24 25 } 26 27 /// <summary> 28 /// EventArgs類的作用就是讓事件傳遞引數用的 29 /// 我們定義一個類CatShout包含兩個成員屬性,以方便傳遞 30 /// </summary> 31 class CatShoutEventArgs:EventArgs 32 { 33 public string catColor { get; set; } 34 public string catName { get; set; } 35 } 36 37 class Mouse 38 { 39 string mouseName; 40 string mouseColor { get; set; } 41 public Mouse(string name, string color) 42 { 43 this.mouseName = name; 44 this.mouseColor = color; 45 } 46 47 public void MouseRun(object sender, CatShoutEventArgs e) 48 { 49 if (e.catColor == "黑色") 50 Console.WriteLine(mouseColor + " 的老鼠 " + mouseName + " 說:\" " + e.catColor + " 貓 " + e.catName + " 來了,快跑!\" \n我跑!!\n我使勁跑!!\n我加速使勁跑!!!\n"); 51 else 52 Console.WriteLine(mouseColor + " 的老鼠 " + mouseName + " 說:\" " + e.catColor + " 貓 " + e.catName + " 來了,慢跑!\" \n我跑!!\n我慢慢跑!!\n我慢慢悠悠跑!!!\n"); 53 54 } 55 }
呼叫如下:
1 Console.WriteLine("[場景說明]: 一個月明星稀的午夜,有兩隻老鼠在偷油吃\n\n\n"); 2 Mouse Jerry = new Mouse("Jerry", "白色"); 3 Mouse Jack = new Mouse("Jack", "黃色"); 4 5 6 Console.WriteLine("[場景說明]: 一隻黑貓躡手躡腳的走了過來"); 7 Cat Tom = new Cat("Tom", "黑色"); 8 Console.WriteLine("[場景說明]: 為了安全的偷油,登記了一個貓叫的事件"); 9 Tom.CatShoutEvent += new Cat.CatShoutEventHandler(Jerry.MouseRun); 10 Tom.CatShoutEvent += new Cat.CatShoutEventHandler(Jack.MouseRun); 11 Console.WriteLine("[場景說明]: 貓叫了三聲\n"); 12 Tom.CatShout(); 13 14 Console.WriteLine("\n\n\n"); 15 16 //當其他顏色的貓過來時 17 Console.WriteLine("[場景說明]: 一隻藍色的貓躡手躡腳的走了過來"); 18 Cat BlueCat = new Cat("BlueCat", "藍色"); 19 Console.WriteLine("[場景說明]: 為了安全的偷油,登記了一個貓叫的事件"); 20 BlueCat.CatShoutEvent += new Cat.CatShoutEventHandler(Jerry.MouseRun); 21 BlueCat.CatShoutEvent += new Cat.CatShoutEventHandler(Jack.MouseRun); 22 Console.WriteLine("[場景說明]: 貓叫了三聲"); 23 BlueCat.CatShout();
執行結果如下:
也可以使用前面學過的Lamda表示式來簡潔的寫以上的事件註冊
Cat Doraemon = new Cat("哆啦A夢", "彩色"); Doraemon.CatShoutEvent += (sender, e) => Jerry.MouseRun(sender, e); Doraemon.CatShoutEvent += (sender, e) => Jack.MouseRun(sender, e); Doraemon.CatShout();
呼叫後結果一樣.
5. 事件應用例項
如果上面的簡單例項不夠過癮,我下面列舉幾個日常開發過程中應用事件解決實際問題的例子,加深對事件的理解。
示例一:我們使用一個事件來監控一個資料夾下檔案的變更情況
先看一下UML類圖
程式碼如下:
1 /// <summary> 2 /// 資料夾監控 3 /// </summary> 4 public class FolderWatch 5 { 6 public class Files 7 { 8 string _fileName; 9 public string FileName 10 { 11 get { return _fileName; } 12 set { _fileName = value; } 13 } 14 DateTime _fileLastDate; 15 public DateTime FileLastDate 16 { 17 get { return _fileLastDate; } 18 set { _fileLastDate = value; } 19 } 20 21 } 22 /// <summary> 23 /// 資料夾路徑 24 /// </summary> 25 public string folderPath; 26 27 /// <summary> 28 /// 檔案集合 29 /// </summary> 30 public List<Files> fileList=new List<Files>(); 31 32 /// <summary> 33 /// 資料夾監控事件 34 /// </summary> 35 public event FolderWatchEventHandler FolderWatchEvent; 36 37 /// <summary> 38 /// 建構函式 39 /// </summary> 40 public FolderWatch(string path) 41 { 42 folderPath = path; 43 44 //獲取目錄下所有檔案 45 foreach (var file in new System.IO.DirectoryInfo(path).GetFiles()) 46 { 47 fileList.Add( 48 new Files() 49 { 50 FileName = file.Name, 51 FileLastDate=file.LastWriteTime 52 } 53 54 ); 55 } 56 } 57 public void OnFieldChange() 58 { 59 if (FolderWatchEvent != null) 60 FolderWatchEvent(this, new MonitorFileEventArgs { Files = fileList }); 61 } 62 63 /// <summary> 64 /// 委託實現方法 65 /// </summary> 66 /// <param name="sender"></param> 67 /// <param name="e"></param> 68 public void MonitorFiles(object sender, MonitorFileEventArgs e) 69 { 70 71 while(true) 72 { 73 //遍歷fileList檔案列表,檢測是否有變更(刪除或修改) 74 if(e.Files!=null) 75 foreach(var file in this.fileList) 76 { 77 string fileFullName=folderPath + "\\" + file.FileName; 78 //檢測是否存在 79 if (!System.IO.File.Exists(fileFullName)) 80 Console.WriteLine("檔案\"" + file.FileName + "\"已被刪除或更名;\n"); 81 else if (file.FileLastDate != (new System.IO.FileInfo(fileFullName)).LastWriteTime) 82 { 83 Console.WriteLine("檔案\"" + file.FileName + "\"已被修改過(上次修改日期:" + file.FileLastDate + ",本次檢測到日期為:" + (new System.IO.FileInfo(fileFullName)).LastWriteTime + ");\n"); 84 85 } 86 87 } 88 //重新獲取目錄下所有檔案 89 List<Files> newFiles = new List<Files>(); 90 foreach (var newFile in new System.IO.DirectoryInfo(this.folderPath).GetFiles()) 91 { 92 newFiles.Add( 93 new Files() 94 { 95 FileName = newFile.Name, 96 FileLastDate = newFile.LastWriteTime 97 } 98 99 ); 100 if(!(fileList.Any(m=>m.FileName==newFile.Name))) 101 Console.WriteLine("新建檔案\"" + newFile.Name+"\"\n"); 102 } 103 fileList.Clear(); 104 this.fileList = newFiles; 105 } 106 } 107 } 108 109 /// <summary> 110 /// 資料夾監控委託 111 /// </summary> 112 public delegate void FolderWatchEventHandler(object sender, MonitorFileEventArgs e); 113 114 /// <summary> 115 /// 事件傳遞引數類 116 /// </summary> 117 public class MonitorFileEventArgs : EventArgs 118 { 119 /// <summary> 120 /// 檔案 121 /// </summary> 122 public List<FolderWatch.Files> Files { get; set; } 123 124 }
呼叫如下:
string MyFolder = "MyFolder"; FolderWatch folder = new FolderWatch(System.IO.Directory.GetCurrentDirectory() +"\\"+ MyFolder); folder.FolderWatchEvent += new FolderWatchEventHandler(folder.MonitorFiles); folder.OnFieldChange();
執行結果如下:
可以看到當我們增加,修改,刪除檔案時,就會返回資料夾內檔案更改的提示資訊。
實際上對於檔案更改的監控.NET提供了專門的類FileSystemWatcher來完成。上面的示例只是為了加深理解事件,在實際應用中對檔案的變更還是有缺陷的,比如同一檔案更名、通過時間判斷檔案變更也是不科學的。
下面我們就使用.net提供的FileSystemWatcher類來完成資料夾監控,程式碼非常簡單
1 static void watcher_Renamed(object sender,System.IO.RenamedEventArgs e) 2 { 3 Console.WriteLine("檔案\"" + e.OldName + "\"更名為:"+e.Name+";\n"); 4 } 5 static void watcher_Deleted(object sender, System.IO.FileSystemEventArgs e) 6 { 7 Console.WriteLine("檔案\"" + e.Name + "\"已被刪除;\n"); 8 } 9 static void watcher_Changed(object sender, System.IO.FileSystemEventArgs e) 10 { 11 Console.WriteLine("檔案\"" + e.Name + "\"已被修改;\n"); 12 } 13 static void watcher_Created(object sender, System.IO.FileSystemEventArgs e) 14 { 15 Console.WriteLine("新建立了檔案\"" + e.Name + "\";\n"); 16 }
呼叫如下:
string MyFolder = "MyFolder"; System.IO.FileSystemWatcher watcher = new System.IO.FileSystemWatcher(System.IO.Directory.GetCurrentDirectory() + "\\" + MyFolder); watcher.Renamed+= watcher_Renamed; watcher.Deleted+=watcher_Deleted; watcher.Changed+=watcher_Changed; watcher.Created +=watcher_Created; watcher.EnableRaisingEvents = true;
執行結果如下:
示例二:使用事件完成了一個檔案下載進度條的示例,平時我們看到很多進度條程式設計師為了偷懶都是載入完成直接跳到100%,這個示例就是傳說中的真進度條。
UML類圖如下:
程式碼如下:
1 public partial class Form1 : Form 2 { 3 System.Threading.Thread thread; 4 public Form1() 5 { 6 InitializeComponent(); 7 } 8 9 10 private void downButton_Click(object sender, EventArgs e) 11 { 12 13 if(thread==null) 14 thread = new System.Threading.Thread(new System.Threading.ThreadStart(StartDown)); 15 16 //使用子執行緒工作 17 if (this.downButton.Text == "開始下載檔案") 18 { 19 this.downButton.Text = "停止下載檔案"; 20 if (thread.ThreadState.ToString() == "Unstarted") 21 { 22 thread.Start(); 23 } 24 else if (thread.ThreadState.ToString() == "Suspended") 25 thread.Resume(); 26 } 27 else 28 { 29 this.downButton.Text = "開始下載檔案"; 30 thread.Suspend(); 31 } 32 } 33 34 //開始載入進度 35 public void StartDown() 36 { 37 //註冊事件 38 DownLoad down = new DownLoad(); 39 down.onDownLoadProgress+=down_onDownLoadProgress; 40 down.onDownLoadProgress += down_ShowResult; 41 down.Start(); 42 43 } 44 45 public void down_onDownLoadProgress(long total,long current) 46 { 47 48 49 if (this.InvokeRequired) 50 { 51 this.Invoke(new DownLoad.DownLoadProgress(down_onDownLoadProgress), new object[] { total, current }); 52 } 53 else 54 { 55 this.myProgressBar.Maximum = (int)total; 56 this.myProgressBar.Value = (int)current; 57 } 58 59 } 60 61 public void down_ShowResult(long total,long current) 62 { 63 Action<long, long> ac = (c, t) => { this.resultShow.Text = ((double)current / total).ToString("P"); ; }; 64 this.Invoke(ac, new object[] { current, total }); 65 } 66 67 68 69 //下載處理類 70 class DownLoad 71 { 72 //委託 73 public delegate void DownLoadProgress(long total, long current); 74 75 //事件 76 public event DownLoadProgress onDownLoadProgress; 77 78 //事件 79 public event DownLoadProgress down_ShowResult; 80 81 public void Start() 82 { 83 //下載模擬 84 for (int i = 0; i <= 100; i++) 85 { 86 if (onDownLoadProgress != null) 87 onDownLoadProgress(100, i); 88 if (down_ShowResult != null) 89 down_ShowResult(100, i); 90 System.Threading.Thread.Sleep(100); 91 } 92 } 93 94 95 } 96 }
執行結果如下:
上面示例使用winform應用程式,實現了一個進度條即時計算進度的例子。在檔案下載子類(DownLoad)中有兩個事件,一個是進度條事件,一個是進度百分比顯示事件,在初始化呼叫時,採用了執行緒。啟用執行緒時,註冊了兩個事件。隨著模擬進度的載入,觸發了進度條事件和顯示百分比事件。做到了即時顯示。
關於執行緒相關知識,在後面有時間了會詳細說明。
6 要點
6.1 事件:事件是物件傳送的訊息,傳送訊號通知客戶發生了操作。這個操作可能是由滑鼠單擊引起的,也可能是由某些其他的程式邏輯觸發的。事件的傳送方不需要知道哪個物件或者方法接收它引發的事件,傳送方只需知道它和接收方之間的中介(delegate)。
6.2 事件處理程式總是返回void,它不能有返回值。
6.3 只要使用EventHandler委託,引數就應是object和EventArgs。第一個引數是引發事件的物件。第二個引數EventArgs是包含有關事件的其他有用資訊的物件;這個引數可以是任意型別,只要它派生自EventArgs即可。
6.4 方法的命名也應注意,按照約定,事件處理程式應遵循“object_event”的命名約定。
6.5 事件具有以下特點:
(1)發行者確定何時引發事件,訂戶確定執行何種操作來響應該事件。
(2)一個事件可以有多個訂戶。 一個訂戶可處理來自多個發行者的多個事件。
(3)沒有訂戶的事件永遠也不會引發。
(4)事件通常用於通知使用者操作,例如,圖形使用者介面中的按鈕單擊或選單選擇操作。
(5)如果一個事件有多個訂戶,當引發該事件時,會同步呼叫多個事件處理程式。 要非同步呼叫事件,請參見使用非同步方式呼叫同步方法。
(6)在 .NET Framework 類庫中,事件是基於 EventHandler 委託和 EventArgs 基類的。
6.6 事件的建立步驟
(1)、定義delegate物件型別,它有兩個引數,第一個引數是事件傳送者物件,第二個引數是事件引數類物件。
(2)、定義事件引數類,此類應當從System.EventArgs類派生。如果事件不帶引數,這一步可以省略。
(3)、定義"事件處理方法,它應當與delegate物件具有相同的引數和返回值型別"。
(4)、用event關鍵字定義事件物件,它同時也是一個delegate物件。
(5)、用+=操作符新增事件到事件佇列中(-=操作符能夠將事件從佇列中刪除)。
(6)、在需要觸發事件的地方用呼叫delegate的方式寫事件觸發方法。一般來說,此方法應為protected訪問限制,既不能以public方式呼叫,但可以被子類繼承。名字是OnEventName。
(7)、在適當的地方呼叫事件觸發方法觸發事件。
==============================================================================================
返回目錄
<如果對你有幫助,記得點一下推薦哦,有不明白的地方或寫的不對的地方,請多交流>
QQ群:467189533
==============================================================================================