Windows Form 中的事件與委派

事件(Event)觸發與委派(Delegate)是多線程、多視窗應用程式中不可或缺的一部分,不同 Thread 之間的函式呼叫、或者不同視窗 Form 之間的資料傳輸,都可以透過 Event 來實作。以下將以簡單的範例及基本程式碼來簡單說明,如何完成這樣的功能。

本篇以 C++/CLI 為主要語言解釋概念,使用 C# 也別緊張,原理相同僅表示式有些許差異。

基本概念

欲觸發一個事件,其實跟執行一個函式一樣,就是讓程式去呼叫一個函式,只是此函式比較特別,在宣告的時候需要多加幾個關鍵字:delegate、及 event

  1. 首先 delegate 一個事件需要傳送的參數類別,例如命名為 EventHandler
  2. 宣告一個 event 使用 EventHandler 參數內容,然後取名例如 Send
  3. 如此一來就可以用 += 來綁定、用 -= 來解除我們自訂的 Send 事件。

多視窗間的資料傳遞

現在舉例有一個應用程式使用兩個視窗:FormA、及 FormB,而 FormB 要把一些字串回傳給 FormA。因此我們為 FormB 實作一個事件 Send,當按鈕按下觸發,另一方面 FormA 收到事件後就會觸發並執行相應處理,例如顯示在 TextBox 裡。

Delegate 委派

首先來處理 FormB,加入以下片段,並請記得為 UI 介面的按鈕綁定 button1_Clicked 函式(不知道怎麼綁就直接在 GUI 設計頁面的按鈕上點兩下就可以了)。

// 宣告時加入delegate關鍵字來聲明接收event的方法所需要的參數
public: delegate void EventHandler(System::String^ str);
public: event EventHandler^ Send;

public: System::Void buttonSubmit_Clicked()
{
    this->Send(textboxB->Text); // Issue the "Send" event
    // do whatever you want else
    this->Close(); // for example, close the window
}

+= 綁定

當從 FormA 開啟一個 FormB 新視窗後,將其 onReceived 函式綁定到 Send 事件,注意函式的傳入參數要符合該事件。

private: System::Void buttonOpenB_Clicked()
{
    FormB^ f = gcnew FormB();
    f->Send += gcnew FormB::EventHandler(this, &FormA::onReceived);
}
private: System:Void onReceived(System::String^ str)
{
    // do what you want
    textboxA->Text = str;
}
注意在使用 FormB 之前記得引入 FormB.h 標頭檔。

成果

沒意外的話可以獲得以下效果,在 FormA 開啟 FormB 之後,可以成功利用事件將 FormB 文字框中的文字回傳 FormA。

多線程間的 UI 問題

Windows Form 的程式一開始都會跑在 GUI 的 Thread 裡,此時若執行了一個需要長時間完成的函式,會造成介面被卡住的情形,對使用者來說就像是程式當掉一般,為避免這種事情發生一般會將該函式另外開一個 Thread 去執行。

例如我們欲使用 ThreadPool 做一個多執行緒的應用,以呼叫一個 funcT 函式在獨立的 Thread 裡,並製作一個進度條監看進度。然而若直接在 funcT 函式中更新介面上的 ProgressBar,結果就會發生跨執行緒的錯誤:

跨執行緒作業無效: 存取控制項 'progressBar1' 時所使用的執行緒與建立控制項的執行緒不同。

這就是因為執行 funcT 的 Thread 和維護 GUI 進度條的 Thread 並不相同而導致,我們無法跨線程做寫值。

Invoke 調用

此時的安全解法就是讓 funcT 執行緒做委派及調用讓 GUI 執行緒來收:

  1. 將「更新進度條」動作本身寫成一個函式,例如:setProgressbar
  2. 檢查 UI 上的 InvokeRequired 屬性(即是否需要委派方法來調用的意思)。
  3. 若為 TRUE 則委派自己(即 setProgressbar),並直接利用 Invoke 調用。
// 宣告時加入delegate關鍵字來聲明將被Invoke的方法所需要的參數
private: delegate System::Void SafeCallDelegateInt(UINT64 number);

private: System::Void setProgressbar(UINT64 number) {
	if (progressBar1->InvokeRequired) {
		try {
			// 指定自己來被Invoke
			SafeCallDelegateInt^ d = gcnew SafeCallDelegateInt(this, &FormA::setProgressbar);
			// 實際Invoke的時候,帶入的變數是寫在這邊
			Invoke(d, gcnew array<Object^>{ number });
		}
		catch (Exception^ ex) {}
	}
	else {
		// 其實改個進度條只需要這行而已
		progressBar1->Value = number;
	}
}

跨執行緒就是這麼的麻煩。另外多寫一個 try…catch 是為了避免各種突發狀況導致 Invoke 失敗(例如:執行到一半視窗被關掉)。

利用類似的方法,我們也可以在 funcT 結束前呼叫另一個函式,來告訴使用者執行完畢並更新相應的 UI 唷!祝大家寫程式愉快~

參考資料

  1. 作法:對 Windows Forms 控制項進行安全線程呼叫 | Microsoft Docs