事件(Event)觸發與委派(Delegate)是多線程、多視窗應用程式中不可或缺的一部分,不同 Thread 之間的函式呼叫、或者不同視窗 Form 之間的資料傳輸,都可以透過 Event 來實作。以下將以簡單的範例及基本程式碼來簡單說明,如何完成這樣的功能。
本篇以 C++/CLI 為主要語言解釋概念,使用 C# 也別緊張,原理相同僅表示式有些許差異。
基本概念
欲觸發一個事件,其實跟執行一個函式一樣,就是讓程式去呼叫一個函式,只是此函式比較特別,在宣告的時候需要多加幾個關鍵字:delegate
、及 event
。
- 首先
delegate
一個事件需要傳送的參數類別,例如命名為EventHandler
。 - 宣告一個
event
使用EventHandler
參數內容,然後取名例如Send
。 - 如此一來就可以用
+=
來綁定、用-=
來解除我們自訂的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;
}
成果
沒意外的話可以獲得以下效果,在 FormA 開啟 FormB 之後,可以成功利用事件將 FormB 文字框中的文字回傳 FormA。
多線程間的 UI 問題
Windows Form 的程式一開始都會跑在 GUI 的 Thread 裡,此時若執行了一個需要長時間完成的函式,會造成介面被卡住的情形,對使用者來說就像是程式當掉一般,為避免這種事情發生一般會將該函式另外開一個 Thread 去執行。
例如我們欲使用 ThreadPool 做一個多執行緒的應用,以呼叫一個 funcT
函式在獨立的 Thread 裡,並製作一個進度條監看進度。然而若直接在 funcT
函式中更新介面上的 ProgressBar,結果就會發生跨執行緒的錯誤:
跨執行緒作業無效: 存取控制項 'progressBar1' 時所使用的執行緒與建立控制項的執行緒不同。
這就是因為執行 funcT
的 Thread 和維護 GUI 進度條的 Thread 並不相同而導致,我們無法跨線程做寫值。
Invoke 調用
此時的安全解法就是讓 funcT
執行緒做委派及調用讓 GUI 執行緒來收:
- 將「更新進度條」動作本身寫成一個函式,例如:
setProgressbar
。 - 檢查 UI 上的
InvokeRequired
屬性(即是否需要委派方法來調用的意思)。 - 若為 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 唷!祝大家寫程式愉快~