C#委託及事件

 

C#中,委託(delegate)是一種引用類型,在其他語言中,與委託最接近的是函數指針,但委託不僅存儲對方法入口點的引用,還存儲對用於調用方法的對象實例的引用。

簡單的講委託(delegate)是一種類型安全的函數指針,首先,看下面的示例程序,在C++中使用函數指針。

首先,存在兩個方法:分別用於求兩個數的最大值和最小值。

int Max(int x,int y)

{

return x>y?x:y;

}

int Min(int x,int y)

{

return x

}

上面兩個函數的特點是:函數的返回值類型及參數列表都一樣。那麼,我們可以使用函數指針來指代這兩個函數,並且可以將具體的指代過程交給用戶,這樣,可以減少用戶判斷的次數。

下面我們可以建立一個函數指針,將指向任意一個方法,代碼如下所示:

//定義一個函數指針,並聲明該指針可以指向的函數的返回值爲int類型,參數列表中包//括兩個int類型的參數

int (*p)(int,int);

//讓指針p指向Max函數

p=max;

//利用指針調用Max

c=(*p)(5,6);

我們的問題在於,上面的代碼中,爲什麼不直接使用Max函數,而是利用一個指針指向Max之後,再利用指針調用Max函數呢?

實際上,使用指針的方便之處就在於,當前時刻可以讓指針p指向Max,在後面的代碼中,我們還可以利用指針p再指向Min函數,但是不論p指向的是誰,調用p時的形式都一樣,這樣可以很大程度上減少判斷語句的使用,使代碼的可讀性增強!

C#中,我們可以使用委託(delegate)來實現函數指針的功能,也就是說,我們可以像使用函數指針一樣,在運行時利用delegate動態指向具備相同簽名的方法(所謂的方法簽名,是指一個方法的返回值類型及其參數列表的類型)。

6.1 使用委託(delegate

6.1.1 委託的建立

建立委託(delegate),過程有點類似於建立一個函數指針。過程如下:

1. 建立一個委託類型,並聲明該委託可以指向的方法的簽名(函數原型)

delegate void MyDelegate(int a,int b);

2.建立一個委託類的實例,並指向要調用的方法

//利用委託類的構造方法指定,這是最爲常見的一種方式

MyDelegate md=new MyDelegate(Max);

//利用自動推斷方式來指明要調用的方法,該形式更類型於函數指針

MyDelegate md=Max;

3.利用委託類實例調用所指向的方法

int c=md(4,5);

下面通過實例來演示C#中委託的使用。

l         案例操作020601:利用委託實現方法的動態調用

首先,添加如下控件:

Ø  兩個RadioButton,分別用來讓用戶選擇求最大值以及求最小值

Ø  二個TextBox,用來輸入兩個操作數

Ø  一個TextBox,用來顯示運算結果

Ø  一個Button,用來執行運算

界面如下圖所示:

下一步,在窗口中添加兩個方法:MaxMin,這兩方法的代碼如下:

int Max(int x,int y)

{

return x>y?x:y;

}

int Min(int x,int y)

{

return x

}

窗口中的代碼,如下圖所示:

下一步:爲了使用委託來實現動態指向,我們需要建立一個委託類“MyDelegate”,並建立該委託類型的一個實例,如下圖所示:

上面的代碼中,我們可以發現,此時,還沒有讓MyDelegate類型的實例“md”指向任何一個方法(即:md的值爲null),原因是:在編寫代碼的時候,我們還不知道用戶想要調用哪一個方法。

下一步,分別爲兩個RadioButton編寫它們的“CheckedChanged”事件,代碼如下:

private void rbtMax_CheckedChanged(object sender, EventArgs e)

{

    if (this.rbtMax .Checked ==true)

    {

        this.md = new MyDelegate(this.Max );

    }

}

 

private void rbtMin_CheckedChanged(object sender, EventArgs e)

{

    if (this.rbtMin .Checked ==true)

    {

        this.md = new MyDelegate(this.Min );

    }

}

這段代碼是,如果用戶選擇了求最大值的RadioButton,則讓MyDelegate類型的實例“md”指向Max方法,如果用戶選擇了求最小值的RadioButton,則讓MyDelegate類型的實例“md”指向Min方法。這樣作的目的,就是要把選擇的過程交給用戶。

下一步,我們爲界面中的Button編寫Click事件,並利用委託來調用求最值的方法。代碼如下所示:

private void btGetResult_Click(object sender, EventArgs e)

{

    if (this.md ==null )

    {

        MessageBox.Show("委託md沒有指向任何方法!");

        return;

    }

    int a = int.Parse(this.tbxOP1 .Text );

    int b = int.Parse(this.tbxOP2 .Text );

    int c = this.md(a,b);

    this.tbxResult.Text = c.ToString();

}

從上面的代碼中,可以發現,在使用委託之前,先要判斷其值是否爲空,如果不爲空,則可以進行調用,同時,使用者可以看到,在調用md時,我們並沒有關心md到底指向了哪一個方法,總之,md不爲空的時候,就一定會指向MaxMin當中的一個。

爲了讓求最大值的RadioButton在程序開始運行的時候就被選中,在FormLoad事件中添加如下代碼:

private void Form1_Load(object sender, EventArgs e)

{

    this.md = new MyDelegate(this.Max );

}

運行的效果如下圖所示:

求最大值

求最小值

l         委託使用的注意事項

Ø   在C#中,所有的委託都是從System.MulticastDelegate類派生的。

Ø   委託隱含具有sealed屬性,即不能用來派生新的類型。

Ø   委託最大的作用就是爲類的事件綁定事件處理程序。

Ø   在通過委託調用函數前,必須先檢查委託是否爲空(null),若非空,才能調用函數。

Ø   在委託實例中可以封裝靜態的方法也可以封裝實例方法。

Ø   在創建委託實例時,需要傳遞將要映射的方法或其他委託實例以指明委託將要封裝的函數原型(.NET中稱爲方法簽名:signature)。注意,如果映射的是靜態方法,傳遞的參數應該是類名.方法名,如果映射的是實例方法,傳遞的參數應該是實例名.方法名。

Ø   只有當兩個委託實例所映射的方法以及該方法所屬的對象都相同時,才認爲它們是想等的(從函數地址考慮)。

 

6.1.2 討論委託類型

從上面的案例中,我們可以發現,在使用委託之前,先要定義一個委託類型,如下所示:

delegate int MyDelegate(int a, int b);

MyDelegate md = null;

既然叫做委託類型,就說明MyDelegate實際上是一個類,上面的寫法只是一種簡單的縮略寫法,實際上,我們自己定義的委託,都是繼承自System.MulticastDelegate類的,但是我們確不能自己定義一個類去繼承自System.MulticastDelegate類,爲了證明這點,我們可以使用ildasm工具,來查看“MyDelegate”的IL代碼。

首先在Visual Studio控制檯,如下圖所示:

在打開的“Visual Studio2008 Command Prompt”窗口中,輸入ildam,如下圖所示:

運行之後,會出現ildasm的窗口,如下圖所示:

下一步,打開剛纔編譯好的exe程序,如下圖所示:

展開結點,並找到“MyDelegate”類型,將其展開,如下圖所示:

在上圖中,我們可以看到,對“MyDelegate”,存在如下“說明”:

extends [mscorlib]System.MulticastDelegate

與此同時,還存在着四個方法,即:

Ø  .ctor:構造方法

Ø  BeginInvoke

Ø  EndInvoke

Ø  Invoke

l         MulticastDelegate類

MultiDelegate類是一個特殊類(Special Class),和System.Delegate類一樣,該類只能夠被編譯器以及內置的工具類所繼承,我們自定義的類是不能夠顯式的繼承自該類的。

MultiDelegate類當中可以包括一個委託的鏈表,這個表中,可以包括一個或多個元素(每個元素都是一個委託),我們可以將這個表稱爲調用鏈。當我們調用一個MultiDelegate的時候,位於該MultiDelegate調用鏈中的委託就會被串行調用。這樣我們就可以只調用一個方法,而多個相同簽名的方法就會同時被串行調用。關於多播委託的說明,我們會在後面的內容中進行講解。

l         Invoke方法

爲了解釋Invoke方法,我們先來回顧一下,當一個委託指向了一個方法時是如何調用的,代碼如下所示:

int c = this.md(a,b);

我們在調用委託,並執行該委託所指向的方法時,本質上就是調用了其Invoke方法。實際上,我們可以直接調用其Invoke方法,代碼如下所示:

int c = this.md.Invoke(a,b);

另外,與Invoke方法對應的BeginInvoke,是對Invoke方法的一個異步調用,而EndInvoke是異步調用完成後的處理方法,關於異步調用的說明,我們將在多線程的章節中進行說明。

6.1.3 使用多播委託(MulticastDelegate

前面剛剛提及到MulticastDelegate,下面我們來看一下它的應用。

有的時候,我們想要調用一個委託,但同時可以執行多個方法(自定義事件中最爲常見),比如,一個工作文檔生成之後,系統要將生成文檔日誌,而且還要被保存到數據庫中,對於以上個操作,如果只想調用一個委託,就可以順序完成,那麼使用多播委託,就可以實現。

多播委託(MulticastDelegate)提供了一種類似於流水線式的鉤子機制只要加載到這條流水線上的委託,都會被順序執行。因爲所有的委託都繼承自MulticastDelegate,因此所的委託都具備多播特性。

下面能過一個控制檯程序來說明多播委託(MulticastDelegate)的使用方式。

l         案例操作050602:使用多播委託

首先,建立一個控制檯程序。在其中添加兩個具備相同簽名的方法

Ø  void CreateLogFile(string originalPath):用於創建日誌文件

Ø  void WriteToDb(string originalPath):用於將文件寫入數據庫

代碼如下:

方法:void CreateLogFile(string originalPath)

///

/// 用於生成日誌文檔

///

/// 文件的原始路徑

static void CreateLogFile(string originalPath)

{

    if (!Directory .Exists ("log"))

    {

         Directory.CreateDirectory("log");

     }

     StreamWriter sw = new StreamWriter("log/log.txt" ,true);

     sw.WriteLine("新文件已經創建,創建時間:{0},文件路徑:{1}",DateTime .Now .ToLongTimeString (),originalPath );

     sw.Close();

     Console.WriteLine("已經寫入日誌!");

}

方法:void WriteToDb(string originalPath)

///

/// 用於將文件寫入數據庫

///

/// 文件的原始路徑

static void WriteToDb(string originalPath)

{

     FileStream fs = new FileStream(originalPath ,FileMode.Open );

     var buffer=new byte[fs.Length ];

     fs.Read(buffer ,0,buffer.Length );

     fs.Close();

 

     SqlConnection con = new SqlConnection("server=.;database=test;uid=sa;pwd=sa");

     SqlCommand cmd = con.CreateCommand();

     cmd.CommandText = "insert into tb_files values(@ID,@FileName,@CreationTime,@FileBytes)";

     cmd.Parameters.Add("@ID",SqlDbType.UniqueIdentifier).Value=Guid.NewGuid ();

     cmd.Parameters.Add("@CreationTime",SqlDbType.DateTime).Value =DateTime.Now ;

     cmd.Parameters.Add("@FileName",SqlDbType.NText).Value=Path.GetFileName (originalPath);           

     cmd.Parameters.Add("@FileBytes",SqlDbType.Image ).Value=buffer  ;

 

     con.Open();

     cmd.ExecuteNonQuery();

     con.Close();

     Console.WriteLine("已經寫入數據庫");

}

上面兩個方法,具備相同簽名,如果想同時串行調用這兩個方法,還要定義一個委託類型,代碼如下:

///

/// 生成一個委託,用於實現多播操作

///

/// 文件的原始路徑

delegate void MyMulticastDelegate(string path);

主函數代碼如下所示:

static void Main(string[] args)

{

    //創建原始文件

    StreamWriter sw = new StreamWriter("new file.txt",false );

    sw.WriteLine("this is a new file");

    sw.Close();

 

    //創建委託,並指向CreateLogFile方法

    MyMulticastDelegate logDelegate=new MyMulticastDelegate (CreateLogFile);

    //創建委託,並指向WriteToDb方法

    MyMulticastDelegate dbDelagate = new MyMulticastDelegate(WriteToDb );

 

 

    MyMulticastDelegate multicastDelegate = logDelegate;

    //在多播委託的調用鏈中添加新的委託元素

    multicastDelegate = multicastDelegate + dbDelagate;

 

    //調用多播委託,並且序列執行兩個委託所指向的方法

    multicastDelegate("new file.txt");

 

}

在主函數中,首先創建一個原始文件,然後建立兩個委託分別指向CreateLogFile方法以及WriteToDb方法,如下代碼段所示:

    MyMulticastDelegate logDelegate=new MyMulticastDelegate(CreateLogFile);

    MyMulticastDelegate dbDelagate = new MyMulticastDelegate(WriteToDb );

下一步,將這兩個方法合併到一個多播委託中,代碼如下所示:

MyMulticastDelegate multicastDelegate = logDelegate;

    multicastDelegate = multicastDelegate + dbDelagate;

最後,利用多播委託,同時串行執行兩個操作,代碼段如下所示:

multicastDelegate("new file.txt");

從上面的代碼中,我們可以發現,對於兩個委託來講,“+”加操作是有意義的。如下面代碼所示:

MyMulticastDelegate multicastDelegate = logDelegate;

    multicastDelegate = multicastDelegate + dbDelagate;

這一點可以說明,如果想要將兩個委託,放入到一個多播委託的調用鏈中,可以使用“+操作符,換句話說,對於委託的“+”操作,就是在調用鏈中增加一個新的結點,並將一個新委託放置到該結點中。另外,和int類型的自加操作類似,委託的自加操作也進行簡寫,這種寫法在註冊事件的時候較爲常用,代碼如下:

MyMulticastDelegate multicastDelegate = logDelegate;

multicastDelegate += dbDelagate;

 

該案例的完整代碼如下:

///

/// 用於生成日誌文檔

///

/// 文件的原始路徑

static void CreateLogFile(string originalPath)

{

    if (!Directory .Exists ("log"))

    {

         Directory.CreateDirectory("log");

     }

     StreamWriter sw = new StreamWriter("log/log.txt" ,true);

     sw.WriteLine("新文件已經創建,創建時間:{0},文件路徑:{1}",DateTime .Now .ToLongTimeString (),originalPath );

     sw.Close();

     Console.WriteLine("已經寫入日誌!");

}

///

/// 用於將文件寫入數據庫

///

/// 文件的原始路徑

static void WriteToDb(string originalPath)

{

     FileStream fs = new FileStream(originalPath ,FileMode.Open );

     var buffer=new byte[fs.Length ];

     fs.Read(buffer ,0,buffer.Length );

     fs.Close();

 

     SqlConnection con = new SqlConnection("server=.;database=test;uid=sa;pwd=sa");

     SqlCommand cmd = con.CreateCommand();

     cmd.CommandText = "insert into tb_files values(@ID,@FileName,@CreationTime,@FileBytes)";

     cmd.Parameters.Add("@ID",SqlDbType.UniqueIdentifier).Value=Guid.NewGuid ();

     cmd.Parameters.Add("@CreationTime",SqlDbType.DateTime).Value =DateTime.Now ;

     cmd.Parameters.Add("@FileName",SqlDbType.NText).Value=Path.GetFileName (originalPath);           

     cmd.Parameters.Add("@FileBytes",SqlDbType.Image ).Value=buffer  ;

 

     con.Open();

     cmd.ExecuteNonQuery();

     con.Close();

     Console.WriteLine("已經寫入數據庫");

}

///

/// 生成一個委託,用於實現多播操作

///

/// 文件的原始路徑

delegate void MyMulticastDelegate(string path);

 

static void Main(string[] args)

{

    //創建原始文件

    StreamWriter sw = new StreamWriter("new file.txt",false );

    sw.WriteLine("this is a new file");

    sw.Close();

 

    //創建委託,並指向CreateLogFile方法

    MyMulticastDelegate logDelegate=new MyMulticastDelegate (CreateLogFile);

    //創建委託,並指向WriteToDb方法

    MyMulticastDelegate dbDelagate = new MyMulticastDelegate(WriteToDb );

 

 

    MyMulticastDelegate multicastDelegate = logDelegate;

    //在多播委託的調用鏈中添加新的委託元素

    multicastDelegate = multicastDelegate + dbDelagate;

 

    //調用多播委託,並且序列執行兩個委託所指向的方法

    multicastDelegate("new file.txt");

 

}

該案例的運行效果如下:

首先,系統中並不存在日誌文件,如下圖所示:

用於儲存文件的數據庫結構如下:

數據表中的原始數據爲空,如下圖所示:

執行完程序之後,窗口的效果如下圖所示:

日誌文件已經生成,內容如下圖所示:

數據庫的效果如下圖所示:

 

6.1.4 匿名方法

在前面的代碼中,用戶可以發現,在使用委託時,無論該代碼難易,都需要將功能性代碼放置在一個方法中,再利用委託指向該方法。在C#2.0以及C#3.0中這種情況得到了改善,在C#2.0中,我們可以利用匿名方法(Anonymous Method)來簡化委託的使用,在C#3.0中,我們可以Lambda表達式使其得到進一步簡化,關於Lambda表達式的相關內容,請參見C#3.0程序設計。

所謂匿名方法(Anonymous Method),是指在使用委託時,可以不再事先定義一個方法,然後再讓委託指向方法,匿名委託允許開發人員,使用內聯方式,直接讓一委託指向一個功能代碼段。下面代碼對比了傳統方法中委託的會用,以及利用匿名方法的簡化操作:

l         傳統方法使用委託:先定義一個方法,再定義委託,並指向方法

public void Run()

{

    StreamWriter sw = new StreamWriter("e:\ex\log.txt",true );

    for (int i = 0; i < 10000000; i++)

    {

        sw.WriteLine(i.ToString ());

    }

    sw.Close();

}

delegate void MyDelegate();

protected void Button2_Click(object sender, EventArgs e)

{

    MyDelegate md = new MyDelegate(this.Run );

    md();

}

l         利用匿名方法簡化委託的使用

delegate void MyDelegate();

protected void Button2_Click(object sender, EventArgs e)

{

   MyDelegate md = new MyDelegate(

      delegate()

      {

         StreamWriter sw = new StreamWriter("e:\ex\log.txt", true);

         for (int i = 0; i < 10000000; i++)

         {

              sw.WriteLine(i.ToString());

         }

         sw.Close();

       }

       );

   md();

}

從上面代碼的對比中,不難發現,使用匿名方法,省去了定義方法的步驟。實際上,在多線程編程的時候,使用匿名方法可以使得代碼變的簡化,並提高了可讀性。下面代碼是在不使用匿名方法的情況下編寫多線程代碼:

public void Run()

{

    for (int i = 0; i < 1000000; i++)

    {

        this.textBox1.Text = i.ToString();

    }

    thread.Abort();

}

Thread thread = null;

private void button1_Click(object sender, EventArgs e)

{

    CheckForIllegalCrossThreadCalls = false;

    this.thread = new Thread(new ThreadStart (this.Run ));

    this.thread.Start();

}

利用匿名方法,可以將上面的代碼改寫爲:

private void button1_Click(object sender, EventArgs e)

{

 

     CheckForIllegalCrossThreadCalls = false;

     Thread thread = null;

     thread = new Thread(

          delegate()

          {

              for (int i = 0; i < 1000000; i++)

              {

                   this.textBox1.Text = i.ToString();

              }

              thread.Abort();

           }

         );

     thread.Start();

}

使用內聯方式可以讓代碼更好理解!

6.2 自定義事件

自定義事件,是委託的一個典型應用!開發人員在編寫WinForm或者ASP.NET應用程序時,都會使用ButtonClick事件。那麼,這種事件觸機制是如何實現的呢?我們能不能編寫自己定義的事件;另外,我們需要在什麼時候編寫自己定義的事件呢?

6.2.1 爲什麼要使用自定義事件

很多開發人員都會向筆者提出這樣的問題“我們爲什麼要自己編寫事件?控件裏面已經集成了很多的事件,這還不夠我們用嗎?”。

實現上,如果不清楚掌握如果編寫自定義事件,想要開發出真正能用的應用程序是很困難的。其主要原因有以下兩點:

1. 自定義控件

通常情況下,開發人員爲了減少重複界面功能代碼的編寫量,往往會開發出一些用戶控件或者是自定義控件。這時,我們就會產生問題,微軟爲我們提供的控件中包括了大量的事件,那麼我們自己編寫的控件中,是不是也會存在很多的事件呢?

2. 自己編寫的組件及類庫

事件並不是UI層的控件類所專有的,很多底層的類也都包括了事件,比如SqlConnection類,該類的類視圖如下所示:

在這個類中就包括了一個InfoMessage事件,該事件作用是“ SQL Server 返回一個警告或信息性消息時發生”。也就是說,在我們自己編寫一個類的時候,往往會發生一種特殊的數據傳遞情況,即:數據的傳遞的方向是由下向上的。爲了解釋這種“由下向上”的傳遞方式,我們先來看一個應用程序的分層結構圖:

上圖是一個比較常規的Socket通信軟件的三層構架圖,客戶端及服務器的界面是兩個“EXE”程序,但是爲了系統的邏輯更好管理,我們通常是將所的通信邏輯封裝到業務邏輯層的兩個“DLL”文件中。圖中箭頭方向說明整個系統功能的實現,是界面的“EXE”調用底層的“DLL”實現的。

然而,所有通信的功能都是在DLL中所定義的類裏實現的,那麼,當客戶端的DLL向服務器的DLL發送了一個消息,並被服務器的“DLL”所截獲之後,服務器界面的“EXE”又是怎麼得知的呢?我們平時在使用QQMSN的時候有沒有想過這樣的問題呢?

這個時候,又出現了我們剛纔提到的那個問題“由下向上”,即,由下層“DLL”中的類,通知上層“EXE”中的界面。我們總不能再使用DLL調用一次EXE吧!

注意:在一般情況下.NET平臺的DLL是可以引用EXE的,但是如上圖所示,EXE已經對DLL進行了引用,此時IDE就不再允許DLLEXE進行引用,這是爲了防止發生循環引用產生死鎖。即使IDE允許我們再利用DLL引用EXE,引用之後所創建的實例和當前運行的界面的實例也是位於不同內存的,所以任何操作都不會生效。

此時,利用委託實現的事件機制就可以爲我們解決問題!

6.2.2 控件中的事件

爲了理解事件機制,首先從控件中的事件談起。我們以ButtonClick事件爲例進行說明。

當開發人員雙擊了Button之後,IDE會將設計器轉向代碼視圖,併爲我們生成一個用於事件回調的方法,如下圖所示:

注意:這裏的button1_Click並不是事件,它僅僅是事件發生時所調用的方法!

我們的問題是,爲什麼在Button1Click事件發生之後,button1_Click方法就會被調用呢?實際上,在我們雙擊Button1的時候,IDE自動的添加了一段代碼,該段代碼位於“Form1.Designer.cs”中(.NET1.1中並不包括Form1.Designer.cs),“Form1.Designer.cs”的位置如下圖所示:

打開Form1.Designer.cs,並展開“InitializeComponent()”方法,找到第42行,如下圖所示:

我們可以看到如下代碼:

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

實際上這段代碼,也就是所謂的事件註冊代碼。該代碼的意思是:如果this.button1Click事件發生之後,就轉向this. button1_Click方法進行處理。

爲了更好的理解事件的註冊過程,我們先第42行代碼進行修改,如下圖所示:

這裏,我們將原來的

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

修改爲

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

在這個程序裏,這的修改是爲了更好理解,當然這種寫法是語法錯誤的

下面我們對其進行分析:

首先,觀察“=”右面的表達式。

new System.EventHandler(this.button1_Click);

通過6.1.1一節中的說明,大家可以發現,這段代碼實際上是建立了一個委託類型的實例,並讓該委託指向了this.button1_Click方法。也就是說,在程序運行的“某一時刻”,系統會通過這個委託實例間接的調用this.button1_Click方法。

然後,我們再來觀察“=”左面的表達示。在C風格的語言中“=”是賦值表達式,也就是說,“=”兩側表達式的數據類型應該是一樣的。因此,既然“=”右側的表達式是一個委託類型(System.EventHandler)的實例,那麼this.button1.Click也應該是一個委託類型(System.EventHandler)。

通過上面的說明,我們得到一個信息,前面這段事件註冊代碼,是讓this.button1.ClickSystem.EventHandler(this.button1_Click)指向了同一段內存空間,簡單來,就是讓this.button1.Click指向了this.button1_Click方法,調用了this.button1.Click,就相當於調用了this.button1_Click方法。因此,我們說,當this.button1Click事件發生之後,方法this.button1_Click就會被調用。

在程序運行的時候,系統會自己檢測this.button1是否被點擊了,如果被點擊了,就在button1的內部調用button1.Click,這時,Windows窗口中的button1_Click方法就會被執行。

當然,事件註冊代碼完全可以手寫。因爲,除了控件中事件註冊代碼是自動生成以外,其他類中的事件註冊都是手寫的。手工註冊事件的方法如下:

首先,可以在事件發生之前的任何代碼中添加事件(通常是在窗口的構造方法中),下面我們來手工註冊button1MouseMove事件,如下圖所示:

當我們寫完“=”時,會出現一個提示“Press TAB to insert”,這時,我們只需要按2下“TAB”鍵,事件的註冊以及用於回調的方法,就會自己添加到代碼窗口裏,如下圖所示:

自動生成的代碼是將this.button1MouseMove事件指向了button1_MouseMove方法。這樣手寫的代碼和IDE自動生成的代碼是完全一樣的。

當然,作爲控件的事件,我們完全可以自動生成,如果想自動生成button1的其他事件,只需要查看button1的屬性窗口,並點擊“”按鈕,就會出現該控件的事件列表,如下圖所示:

然後雙擊你想要的事件,代碼就會自動生成了。

在前的面代碼中爲了更好理解事件註冊,我們曾將

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

修改爲

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

我們會發現,無論是自己寫的事件註冊代碼,還是自動生成的代碼,都是使用“+=”來實現的,實際上,作爲事件註冊的代碼,我們僅僅能夠使用“+=”來實現註冊,簡單的使用“=”是語法錯誤的!!!

+=”操作符在C風格語言中是常用的操作符,比如

int i=0;

i+=1;

等同於

int i=0;

i=i+1;

因此,

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

在原則上等同於

this.button1.Click = this.button1.Click +

new System.EventHandler(this.button1_Click);

用自然語言來描述上面的代碼就是“一個委託=這個委託本身+另外一個委託”。那麼委託相加意味着什麼呢?

6.1.3一節中,我們討論過MultiDelegate(多播委託),而事件本身也是委託,並且所有委託都是System.MultiDelegate類的派生類,在6.1.3中,我們曾經演示過,多個委託類型實例相加,就是將這些委託實例存放在一個多播委託的調用鏈中,當調用多播委託時,該多播委託的調用鏈中的所有委託都會順序的被調用。

利用多播委託的原理,我們可以將多個方法註冊給一個事件,如下所示:

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

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

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

上面的代碼,就將三個方法註冊到了button1Click事件中,button1Click事件觸發之後,方法button1_Clickbutton1_Click1button1_Click2將會被順序調用這樣作的好處是,我們可以將多個功能以及邏輯完全獨立的操作放在不同的方法中,當事件發生之後,這方法將會順序的被調用,以實現我的需要的級聯操作。

6.2.3 控件中事件的回調方法

說完了事件的註冊,下面我們來談一下事件的回調方法。首先,我們還要再一次回顧事件註冊的代碼:

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

上面代碼中,使用“new System.EventHandler(this.button1_Click)”將一個System.EventHandler委託類型的實例指向了this.button1_Click方法。通過6.1.1一節中所談到的內容,我們知道,如果想讓一個委託指向一個方法,那麼該委託以及所被指向的方法一定要具備相同的簽名(Signature,具備相同的參數列表,相同的返回值)。因此,System.EventHandler類型和this.button1_Click方法具備相同的簽名,下面,我們來看一下System.EventHandler委託的簽名是什麼樣的:

public delegate void EventHandler(

              Object sender,

              EventArgs e

)

System.EventHandler的簽名是:返回值爲void;有兩個參數,Object sender, EventArgs e。因此button1_Click方法也具備相同形式,代碼如下:

private void button1_Click(object sender, EventArgs e)

{

 

}

實際上,我們所能夠看到的事件回調方法的簽名基本上都着不多,只不過第二個參數略有區別,下面,我們對該方法的參數進行說明。

Ø         Object sender

從該參數的命名上,可以看出其作用,sender(發送者)的意思是:誰觸發的這個事件,那麼sender就是誰,由於所有的類型在理論上講都可以包括事件,因此sender的類型被定義成Object類型,當多個事件同時指向一個事件回調方法的時候,通過該參數可以區分出是哪一個類觸發的事件,以便做出不同的處理,此時,需要對參數sender作出類型轉化。

l         案例操作020603:多個事件指向同一個回調方法

首先,添加三個Button,一個TextBox

界面如下:

然後,在主窗口中添加一個方法ButtonClick,這三個按鈕的Click事件將調用該方法。

代碼如下:

protected void ButtonClick(object sender, EventArgs e)

{

     Button bt = sender as Button;

     this.textBox1.Text ="我是:"+ bt.Text;

}

上面代碼中,爲了知道點擊的是哪個按鈕,我們將sender轉化成了Button類型。

下面來指定這三個按鈕的Click事件回調方法

首先,切換到button1的屬性窗口(F4),點擊“”按鈕,找到“Click”事件,並設置所調用的方法名爲ButtonClick ,如下圖所示。

然後,以相同的方法設置button2,button3Click事件,並它們都指向ButtonClick方法。

最後,運行程序,下面是運行情況:

點擊button1:

點擊button2:

點擊button3:

 

Ø         EventArgs e

EventArgs類型是事件參數,當事件發生時,可以通過該參數來傳遞一些數據。當然EventArgs類本身是傳遞不了什麼數據的,該類的類視圖如下:

從類視圖中不難發現,該類中的成員很少,通常情況下,如果想傳遞數據,那麼事件參數類一般會是一個EventArgs類的派生類。比如TextBox類的KeyDown事件中事件參數就是一個KeyEventArgs類型的參數,事件回調方法的原型如下:

private void textBox1_KeyDown(object sender, KeyEventArgs e)

{

 

}

KeyEventArgs類的類視圖如下所示:

該類中有三個屬性:“Alt”,“Control”,“Shift”分另用來表示按下鍵盤某一個鍵的同時,有沒有按下這三個功能鍵。

另外“KeyCode”屬性可以用來表示當前用戶所按的鍵位名。

在下一節中,我們會說明如何編寫自定義事件參數。

l         案例操作020604:利用TextBox的KeyDown事件來模擬QQ聊天窗口

新建一個Windows窗口,包括以下控件

一個RichTextBox控件rtbMessage:用來顯示聊天信息

一個TextBox控件(tbxInput):用來輸入聊天信息

一個Button控件( btSubmit):用來提交

界面如下所示:

功能如下:

點擊button可以讓消息傳遞到上面的RichTextBox中。當然,如果按“Ctrl+Enter”也可以使文字傳遞到RichTextBox中。

首先,我們在Windows窗口中添加一個方法,代碼如下:

public void Display()

{

     this.rtbMessage.AppendText(this.tbxInput .Text +"n");

     this.tbxInput.Text = "";

}

該方法的功能就是將文本框中的文字添加到RichTextBox中。

下一步,編寫ButtonClick事件,代碼如下:

private void btSubmit_Click(object sender, EventArgs e)

{

     this.Display();

}

下一步編寫TextBoxKeyDown事件,代碼如下:

private void tbxInput_KeyDown(object sender, KeyEventArgs e)

{

     if (e.Control ==true&&e.KeyCode .ToString ()=="Return")

     {

           this.Display();

      }

           

}

該方法在執行前先檢測用戶是否同時按下的Control和回車鍵,然後再進行顯示操作。

程序運行的效果如下:

按下Control+回車之後,文字上屏,效果如下:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章