WinForm事件與消息
消息概述以及在C#下的封裝
Windows下應用程序的執行是通過消息驅動的。所有的外部事件,如鍵盤輸入、鼠標移動、按動鼠標都由OS系統轉換成相應的“消息”,進入到應用程序的消息隊列中,由應用程序引擎輪詢處理。在C#中,消息被應用程序的工作引擎通過輪詢等方式遍歷獲取並按照消息的類型逐個分發到對應的組件(例如窗體、按鈕等),最后調用對應組件所注冊的事件進行處理。
在.NET框架類庫中的System.Windows.Forms命名空間中微軟采用面對對象的方式重新定義了Message。該消息主要有一下的幾個公共屬性:
System.Windows.Forms.Message
HWnd 獲取或設定消息的處理函數
Msg 獲取或設定消息的ID號
Lparam 指定消息的LParam字段
Wparam 指定消息的WParam字段
Result 指定為響應消息處理函數而向OS系統返回的值
System.Windows.Forms.Application
System.Windows.Forms.Application類具有用於啟動和停止應用程序和線程以及處理Windows消息的方法。例如,調用Run以啟動當前線程上的應用程序消息循環,並可以選擇使其窗體可見;調用Exit或ExitThread來停止消息循環。所以我們經常使用vs初始化一個基本的WinForm程序,顯示的下列模板代碼:
/// <summary>
/// 應用程序的主入口點。
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1()); // 調用Run以啟動當前線程上的應用程序消息循環
}
因為Application是在單線程中運行的,所以在Application.Run開始后,Application本身不斷輪詢檢查消息隊列,然后根據消息類型進行數據分發。例如,當我們為這個Form1增加一個鼠標的點擊事件后,我們運行該打開Form1:
Form1 form1 = new Form1();
form1.MouseClick += (sender, e) => MessageBox.Show("1");
form1.MouseClick += (sender, e) => MessageBox.Show("2");
Application.Run(form1);
運行后點擊Form,可以看到首先出現一個MessageBox,展示“1”,我們點擊確定后,又會出現MessageBox,展示“2”。實際上整個過程應該如下:
當我們按下鼠標左鍵后,消息形成並送往應用程序消息隊列中,然后被Application類從應用程序消息隊列中取出,然后分發到相應的窗體。窗體使用MouseClick事件中的函數指針調用已經添加的響應函數。所以C#中的事件字段實質上是一個函數指針列表,用來維護一些消息到達時的響應函數的地址。
到目前為止我們可以看到,消息其實在我們進行事件調用的時候,已經被提取加工了,它已經由Application進行了預處理,形成了所謂的“事件調用”。那么,我們還能更加自定義的干預消息嗎?答案是可以的。
WndProc
//
// 摘要:
// 處理 Windows 消息。
//
// 參數:
// m:
// 要處理的 Windows System.Windows.Forms.Message。
protected override void WndProc(ref System.Windows.Forms.Message e);
對於每個Form來說,我們都可以重寫該方法,該方法的參數就是上面提到的Message類的實例,所有的消息在被獲取后,正常情況下都會被封裝為Message對象,然后由Application工作引擎調用對用的Form.WndProc傳入該Messsage,由於Form子類重寫了該方法,所以如果希望底層能處理相關的消息,需要通過base.WndProc傳遞到父類繼續調用。下面就是一個代碼示例來展示控制如果當前的消息是鼠標左鍵點擊,則彈出MessageBox展示“WndProc MouseClick”:
protected override void WndProc(ref Message m)
{
const int WM_LBUTTONDOWN = 0x0201;// 鼠標左鍵點擊
if (m.Msg == WM_LBUTTONDOWN)
{
MessageBox.Show("WndProc MouseClick");
return;
}
base.WndProc(ref m);
}
IMessageFilter
除了上述的WndProc之外,其實更加便於處理應該的實現IMessageFilter接口,然后讓Application將實現該接口的消息過濾器添加到Application中:
public class MyMessageFilter : IMessageFilter
{
public bool PreFilterMessage(ref Message m)
{
//返回值為true, 表示消息已被處理,不要再往后傳遞,因此消息被截獲
//返回值為false,表示消息未被處理,需要再往后傳遞,因此消息未被截獲
const int WM_LBUTTONDOWN = 0x0201;// 鼠標左鍵點擊
if (m.Msg == WM_LBUTTONDOWN)
{
MessageBox.Show("MyMessageFilter MouseClick");
return true;
}
return false;
}
}
編寫完成后,在應用程序初始化的過程中,添加該過濾器:
Application.AddMessageFilter(new MyMessageFilter());
同樣的,我們啟動應用程序並點擊實驗,可以看到正常的MessageBox輸出。