C++ 事件编程
++ 事件編程
| 在微軟 .NET 框架中可以定義托管類事件并用委托和 += 操作符處理這些事件。這種機制似乎很有用,那么在本機 C++ 中有沒有辦法做同樣的事情? Several Readers ?確實如此!Visual C++ .NET 具備所謂統(tǒng)一事件模型(Unified Event Model),它可以像托管類一樣實現(xiàn)本機事件(用 __event 關(guān)鍵字),但是由于本機事件存在一些不明顯的技術(shù)問題,而微軟的老大不打算解決這些問題,所以他們要我正式奉勸你不要使用它們。那么這是不是就是說 C++ 程序員與事件無緣了呢?當(dāng)然不是!可以通過別的方法實現(xiàn)。本文我將向你展示如何輕松實現(xiàn)自己漂亮的事件系統(tǒng)。但是在動手之前,讓我先大體上介紹一下事件和事件編程。它是個重要的主題,當(dāng)今對事件沒有堅實的理解,你是無法編寫程序的——什么是事件以及什么時候使用事件。 成功的編程完全在于對復(fù)雜性的掌控。很久以前,函數(shù)被稱為“子程序”(我知道,我這樣說證明我已經(jīng)老了!)管理復(fù)雜性的主要方式之一是自頂向下的編程模式。高層實現(xiàn)類似“宇宙模型”,然后將它劃分為更小的任務(wù)如:“銀河系模型”以及“太陽系模型”等等,直到任務(wù)被劃分為可以用單個函數(shù)實現(xiàn)為止。目前自頂向下的編程模型仍被用于過程化的任務(wù)實現(xiàn)當(dāng)中,但它不適用于發(fā)生順序不確定的實時事件響應(yīng)系統(tǒng)。經(jīng)典的例子便是 GUI,程序必須響應(yīng)用戶的某些行為,比如按鍵或是鼠標移動。實際上,事件編程很到程度上源于圖形用戶界面的出現(xiàn)。 在自頂向下的模型中,在頂部的高級部分對低級的實現(xiàn)各種不同任務(wù)的函數(shù)——如 DoThis,DoThat 進行食物鏈式的調(diào)用。但不久以后,低層部分需要回調(diào)(talk back),在 Windows 中,可以調(diào)用 Rectangle 或 Ellipse 繪制一個矩形或橢圓,但最終 Windows 需要調(diào)用你的應(yīng)用程序來畫窗口。但應(yīng)用程序都還不存在,它仍然處于被調(diào)用度狀態(tài)!那么 Windows 如何知道要調(diào)用哪個函數(shù)呢?這就是事件用處之所在。
在每個 Windows 程序的核心——不論是直接用 C 語言編寫的還是使用 MFC 或 .NET 框架類編寫——都是一個處理消息的窗口過程,這些消息如:WM_PAINT, WM_SETFOCUS 和 WM_ACTIVATE。你(MFC 或 .NET)實現(xiàn)窗口過程并將它傳遞給 Windows。到了該畫窗口,改變輸入焦點以及激活窗口的時候,Windows 用相應(yīng)的消息代碼調(diào)用你的過程。這個消息就是事件。窗口過程就是事件處理器。
那么到底什么叫事件?其實,事件就是回調(diào)。而不是在編譯時就已知名字的函數(shù)調(diào)用,組件調(diào)用在運行時調(diào)用你提供的函數(shù)。在 Windows 中,它是一個窗口過程。在 .NET 框架中,它叫做委托。不管術(shù)語怎么叫,事件提供了一種軟件組件調(diào)用函數(shù)的方式,這種調(diào)用方式直到運行時才知道要調(diào)用什么函數(shù)。回調(diào)被稱為事件處理器。發(fā)生或觸發(fā)一個事件意味調(diào)用這個事件處理器。為此,事件接收部分首先得給事件源提供一個事件處理器的指針,這個過程叫注冊。
一些讀者問:異常和事件之間有什么差別?主要差別是:異常表示不應(yīng)該發(fā)生的意外情況。例如,你的程序運行耗盡內(nèi)存,或者遇到被零除。這些都是你并不希望發(fā)生的異常情況,并且一旦出現(xiàn)這些情況,你的程序必須要做出相應(yīng)的處理。另一方面,事件則是每天常規(guī)操作的部分并且完全是預(yù)期的。用戶移動鼠標或按下某個鍵。瀏覽器導(dǎo)航到一個新頁面。從控制流的角度看,事件是一次函數(shù)調(diào)用,而異常則是堆棧的突然跳躍,用展開的語義銷毀丟失的對象。 控制立即傳到事件處理器,并且不會返回,除非處理完成。某些系統(tǒng)提供某種以異步觸發(fā)事件的方式,例如,在 Windows 中,你可以用 PostMessage 代替 SendMessage。控制會從 PostMessage 立即返回,該消息是后來才處理的。但是 .NET 框架中的事件以及我在這里討論的事件是在觸發(fā)時被立即處理的。當(dāng)然,你總是可以觸發(fā)來自運行在單獨的線程中的消息代碼事件,或者使用異步委托調(diào)用在線程池中執(zhí)行每個事件處理器,在這種情況下,相對于主線程來說,事件是異步發(fā)生的。 這里 EventHandler 是某個函數(shù)的委托,該函數(shù)帶有參數(shù):Object (也就是 sender) 和 EventArgs: public __delegate void EventHandler(Object* sender,EventArgs* e);為了接收事件,你必須用正確的簽名實現(xiàn)處理器成員函數(shù)并創(chuàng)建一個委托來包裝該函數(shù),然后調(diào)用事件的 += 操作符注冊你的處理器/委托。對于上面的 Click 事件,代碼應(yīng)該像這樣: // event handlervoid CMyForm::OnAbort(Object* sender, EventArgs *e){...}// register my handlerm_abortButton->Click += new EventHandler(this, OnAbort); 注意該處理器函數(shù)必須具備由委托定義的簽名。這是托管擴展的基本原則。但是你的問題涉及的不是托管事件,你問的是本機事件——如何實現(xiàn)本機 C++ 事件?C++ 本身沒有內(nèi)建的事件機制,那么該怎么實現(xiàn)呢?你可以用 typedef 來定義一個回調(diào)并讓客戶機來提供這個回調(diào),這種做法有些類似 qsort——但那樣太老土了。更不用說處理多個事件時的繁瑣。相對于靜態(tài)外部函數(shù)來說,用成員函數(shù)作為事件處理器是最丑陋的做法。 CPrimeCalculator 調(diào)用內(nèi)部輔助函數(shù) NotifyProgress 和 NotifyDone 來觸發(fā)事件。這些函數(shù)遍歷客戶機對象列表,為每個客戶機調(diào)用相應(yīng)的事件處理器。代碼如下: void CPrimeCalculator::NotifyProgress(UINT nFound){list<IPrimeEvents*>::iterator it;for (it=m_clients.begin(); it!=m_clients.end(); it++) {(*it)->OnProgress(nFound);}}如果你對 STL 不熟悉,去看看有關(guān)迭代器反引用操作符的內(nèi)容,它返回當(dāng)前指向的對象,上面代碼段中,for 循環(huán)里的代碼等同于: IPrimeEvents* obj = *it;obj->OnProgress(nFound);觸發(fā) Done 事件的 NotifyDone 函數(shù)做法類似,它沒有參數(shù),如 Figure 3 所示。你也許覺得 Done 事件是多余的,因為當(dāng) FindPrimes 返回控制時,客戶機已經(jīng)知道 CPrimeCalculator 完成了工作。沒錯——但有一種情況除外,那就是多個客戶機注冊接收的事件,并且調(diào)用 CPrimeCalculator::FindPrimes 的對象可能不是同一個。Figure 4 是我的測試程序 PrimeCalc。該程序為素數(shù)事件實現(xiàn)了兩個不同的事件處理器。第一個處理器是主對話框本身,CMyDlg,它利用多繼承實現(xiàn) IPrimeEvents。該對話框處理 OnProgress 和 OnDone,并在對話窗口顯示進度,完成后發(fā)出蜂鳴聲。其它的事件處理器,如 CTracePrimeEvents 也實現(xiàn)了 IPrimeEvents,這個實現(xiàn)顯示診斷(TRACE)流中的信息。如 Figure 6 所示,在我的 TraceWin 程序(參見 2004 年三月的專欄)中顯示的范例輸出。我寫的 CTracePrimeEvents 展示了多個客戶機如何注冊相同的事件。
從使用 CPrimeCalculator 來編寫應(yīng)用的程序員角度看,處理事件簡單而直白。從 IPrimeEvents 派生,實現(xiàn)處理器函數(shù),然后調(diào)用 Register。從編寫觸發(fā)事件的類的程序員看來,這個過程有些冗長乏味。首先你得定義事件接口。這并沒有什么不好。但接著你得編寫 Register 和 Unregister 函數(shù),每個 Foo 事件都得有一個相應(yīng)的 NotifyFoo 函數(shù)。如果有 15 個事件的話,那就十分令人不爽了,尤其是每個 NotifyFoo 函數(shù)的模式都相同: void CMyClass::NotifyFoo(/* args */){list<IPrimeEvents*>::iterator it;for (it=m_clients.begin(); it!=m_clients.end(); it++) {(*it)->OnFoo(/* args */);}}
NotifyFoo 迭代客戶機列表,為每個注冊的客戶機調(diào)用相應(yīng)的 OnFoo 處理器,并傳遞任何需要的參數(shù)。有沒有什么方法實現(xiàn)這個一般過程,比如用宏或者模板來封裝這種繁瑣而固定的樣板代碼,將自己從重復(fù)性勞動中解放出來呢?實際上是有的。下個月的專欄文章我們將討論這個問題。記住在同一時間,同一頻道,咱們再見——順祝編程愉快! |
總結(jié)
- 上一篇: FFPLAY的原理(一)
- 下一篇: C++中extern关键字的作用