摘要目前MFC和ATL代表了兩種框架,分別面向不同類型的基於Windows的開發。MFC代表了創建獨立的Windows應用的一種簡單、一致的方法;ATL提供了一種框架來實現創建COM客戶機和服務器所必須的樣板文件代碼。這兩種框架在它們對於開發ActiveX的用途上會合了。我們將看看這兩種框架是如何適用於創建ActiveX控件的——突出其優缺點,親自經歷創建一個控件的過程——以便你能夠決定何時使用一種框架,何時使用另一種。
如果你希望用C++來寫ActiveX®控件,有兩個流行的框架,一個是Microsoft® Foundation Classes (MFC),另一個是ATL。我將深入的解釋這兩種框架對開發ActiveX控件所提供的支持,幫助你更好的決定哪種模型最適合你的開發環境和需求。
ActiveX控件的完全形態
ActiveX控件基於構件對象模型COM,使得ActiveX控件成為可能的COM的基本原則是一個對象的接口和其實現能夠而且應該分開對待。只要COM的對象和它的客戶方代碼就接口細節達成了一致,如何實現就不成其問題。ActiveX控件展示了大量ActiveX控件包容器理解的接口。因為客戶方代碼和控件認可這些接口的外在表現,你可以編寫一個ActiveX控件然後簡單的將它放入包容器中。包容器將通過良好定義的接口來驅動控件,而這些控件將以自己的方式做出合適的響應。在更高的層次上,一個ActiveX控件是實現了幾個主要ActiveX技術的一個COM對象,包括通常的引入COM接口,OLE嵌入協議,連接點和屬性頁。在較低的編程層次上,ActiveX控件只是實現了某些類型接口的COM類。當一些客戶方代碼成功的查詢到這些接口之一時,它就知道如何使用一個ActiveX控件了。
一個ActiveX控件暴露的接口主要分為3類。第一,ActiveX控件是可嵌入的對象;就是說,它們實現了大多數的OLE文檔、in-place激活和嵌入協議。ActiveX控件實現了如下的接口:
IOleObject, IPersistStorage, IDataObject, IOleInPlaceActiveObject, IOleInPlaceObject, IViewObject2和IRunnableObject (這一個很少用到). 第二ActiveX控件通常都支持屬性頁,這樣客戶方就可以修改控件的屬性了。最後,ActiveX控件通常都利用COM的連接點技術,實現了客戶方能發現的外出接口。
為了幫助比較ATL和MFC框架,我們來看一下寫在每一種框架中的相同的控件。此控件監視創建它的線程上傳遞的消息流。消息流控件是一個很不錯的例子,因為它演示了一個ActiveX控件所有主要的方面,包括送入接口、外出接口,屬性,永久性以及屬性頁。讓我們從研究這兩個框架提供的標准的COM支持開始吧。
MFC的基本COM支持
Microsoft建立MFC使得開發Windows®應用程序比使用SDK容易多了。有了MFC,Microsoft接著增加了對即存框架的COM支持。這意味著MFC的開發者在增加越來越多的函數時必須保持框架的完整。同時,Visual C++®編譯器那時還不支持模板,因此,它們不得不借助非模板的其它手段來將COM功能摻入它們的類中。Microsoft通過加入一些虛函數到CcmdTarget類和一些宏中解決了這個問題,使得在MFC中實現COM接口有了可能。
MFC內部的COM支持從CcmdTarget開始,CcmdTarget類實現了Iunknown接口,還包括了一個用於引用計數的成員變量(m_dwRef)以及用於實現IUnknown 的6個函數:: InternalAddRef, InternalRelease, InternalQueryInterface, ExternalAddRef, ExternalRelease, 和 ExternalQueryInterface.。QueryInterface的兩個版本——AddRef和Release支持COM聚合。InternalAddRef, InternalRelease和InternalQueryInterface完成引用計數和QueryInterface操作,而ExternalAddRef, ExternalRelease和 ExternalQueryInterface代理控制聚合的對象(如果此對象參與聚合的話)。
MFC使用嵌套的類復合策略來實現COM接口。在MFC中,想實現COM 接口的類是從CcmdTarget中派生的。每個由CcmdTarget派生出的類實現的接口得到它自己的嵌套類。MFC使用宏BEGIN_INTERFACE_PART和END_INTERFACE_PART來產生嵌套類。
最後,MFC實現了表驅動的QueryInterface。MFC的接口映射的工作機理同它的消息映射基本相同:MFC的消息映射把一個Windows消息和一個C++類中的函數相聯系;MFC的接口映射把一個接口的GUID和一個表示此接口的特定的vptr的地址相聯系。每個基於CcmdTarget的類實現COM接口通過更多的宏:DECLARE_INTERFACE_MAP, BEGIN_INTERFACE_MAP, INTERFACE_PART,和 END_INTERFACE_MAP來增加一個接口映射。
為了理解這些宏在實際中是什麼樣子的,請看圖1,它說明了實現ActiveX控件,COleControl 的MFC類。當你細讀代碼時,注意ColeControl帶有夾在一對BEGIN_INTERFACE_PART 和 END_INTERFACE_PART宏之間的每個接口的簽名,還要注意ColeControl的接口映射表有22個條目。
除了實現了Iunknown接口,MFC還包括IclassFactory的一個標准實現。再一次,MFC通過若干宏提供了此支持。MFC有兩個宏來提供類對象:DECLARE_OLECREATE_EX 和 IMPLEMENT_OLECREATE_EX.。在一個基於CcmdTarget的類中使用這些宏增加一個ColeObjectFactory類型的靜態成員到該類中。如果你看一下AFXDISP.H中 ColeObjectFactory的定義,你將會看到用在COleObjectFactory 中的MFC的嵌套類宏為實現IClassFactory2定義了一個嵌套類。IClassFactory::CreateInstance的MFC版本使用MFC的動態創建機制(DECLARE_DYNCREATE 和 IMPLEMENT_DYNCREATE宏打開此功能)來實例化COM類,因此買入MFC的COM支持同樣意味著買入它的動態創建機制。
最後幾個由MFC提供的在ActiveX控件內的基本COM支持是對Idispatch的支持。用Visual C++ 和 MFC實現一個分發接口幾乎是微不足道的。在MFC中實現一個分發接口,只需要使用ClassWizard就可以了。ClassWizard中的自動創建板有一個按鈕用於添加屬性,另一個用於添加方法。在MFC中,Idispatch支持來自CcmdTarget類。IDispatch 的MFC的實際實現在一個叫做COleDispatchImpl 的類中,ColeDispatchImpl派生自Idispatch,實現了所有4個Idispatch函數:GetTypeInfoCount, GetTypeInfo, GetIDsOfNames, 和 Invoke.。由CcmdTarget派生的類通過調用EnableAutomation,將IDispatch vptr加入到它們的接口映射中。當客戶在基於MFC的ActiveX控件上調用IDispatch 的QueryInterface時,CcmdTarget交出鏈接在ColeDispatchImpl上的vptr。
每次你使用ClassWizard將一個自動屬性或者方法加入到一個類中時,你同時也在該類的分發映射表中加入了一項。一個分發映射表是一個將DISPIDs(用來調用分發成員的符號)和它們的供人讀的名字以及和實際完成這個工作的某些C++代碼聯系起來的簡單的表格。ColeDispatchImpl的調用以及GetIDsOfNames函數通過在類的分發映射表中查找分發成員並分發DISPID相對應的函數來工作。MFC能為某些基於COM的高級技術如OLE文檔、OLE拖放和自動操作提供非常好的支持,然而,如果你想更改框架——比如說,你想將分發接口編程雙接口的——你就得大動手腳了。另一方面,ATL更加是COM中心的。
ATL的基本的COM支持
ATL的目標是使開發者不必重寫IUnknown, IDispatch, IclassFactory和其它的分支以將常規的DLL和EXE變成基於COM的DLL和EXE。從這個角度講,ATL是一個比MFC精簡的多的框架,它設計和生成時就考慮了COM支持。它使用基於模板的方法,通過繼承ATL提供的模板,開發者可以加入各種COM功能片斷。
ATL的原始COM支持是從對Iunknown的支持開始的。ATL的Iunknown實現分成兩個部分:CcomObjectRootEx類,用來處理Iunknown部分的引用計數;CcomObjectRootBase類,用來處理QueryInterface。
CcomObjectRootEx是一個基於模板的類,將線性模型作為其唯一參數。這是一個真正有趣的說明ATL怎樣使用模板將算法作為模板參數傳遞的例子。ATL有兩個處理引用計數的類,用於處理不同的線性模型: CComSingleThreadModel 和 CcomMultiThreadModel。這些類每個都有一個遞增和一個遞減函數。它們之間的區別是CcomSingleThreadModel用標准C++操作符(++和——)實現遞增和遞減;而CcomMultiThreadModel使用線程安全的InterlockedIncrement 和 InterlockedDecrement函數來實現這兩個功能。根據用來實例化CcomObjectRootEx的模板參數,它能正確的運行給定的組件類型。你很快將會看到它的用法的一個例子。象MFC,ATL使用基於表的查找機制實現QueryInterface.。CComObjectRootBase 通過一個接口映射處理類的QueryInterface函數。BEGIN_ COM_MAP 和 END_COM_MAP 宏定義了一個接口映射的開始和結束。然而,不像MFC,ATL提供了17種途徑來組成一個接口映射,例如使用從ATL的基於模板的接口實現類如IOleObjectImpl 來的vptrs。這包括了那些從tear-off 的類或者由聚合提供的類來的vptrs。
在ATL裡,C++類通過繼承CcomObjectRootEx,指定它們想用的組件模型(記住,MFC的Iunknown支持是內建在CcmdTarget中的)變成了COM類。
ATL的類對象(以及IClassFactory)支持也來自模板,而MFC的類對象支持通過ColeObjectFactory和一些宏而有效。ATL的類對象支持來自CComCoClass/CcomClassFactory類家族和CcomCreator類家族。CcomCoClass包含了類的GUID,定義了COM類的錯誤處理設施。CcomCreator類提供了CreateInstance的實現,供CcomClassFactory使用。對於MFC,你可以通過若干宏,使所有這種支持有效。ATL包括 DECLARE_CLASS_FACTORY, DECLARE_CLASS_ FACTORY2, DECLARE_CLASS_FACTORY_AUTO_THREAD, 以及 DECLARE_CLASS_FACTORY_SINGLETON等宏用來使各種具體的類工廠支持有效。
最後,ATL 對IDispatch的支持還來自模板類,——其名字是IDispatchImpl.。比起MFC的Idispatch支持來,ATL對Idispatch的支持更加是COM中心的。MFC使用了一種hand-rolled 的IDispatch實現,而ATL使用更加標准的方法來加載一個接口的類型信息並代表標准的類型庫編譯器。
圖2顯示了一個標准的基於ATL的控件。最值得注意的一點是MFC和ATL各是怎樣引入實現一個控件所需的必要的各種接口的。MFC對標准控件接口的支持是內建在ColeControl類中的。你從ColeControl中派生出你的控件並且一次性繼承所有的函數調用。注意ATL通過模板繼承以零碎的方式逐個引入每個功能片斷。這是一個非常重要的差異,因為這意味著用ATL你可以忽略一些接口實現模板(例如,使你的控件更為精簡)剝掉不希望的功能。對MFC,你不能完成同樣的動作——不管你想不想,你將獲得所有接口。
關於例子應用
這裡我將使用的例子是一個通過一個分支過程監控消息流的ActiveX控件,它實時的顯示消息流圖。這兩個控件實際上有著相同的功能。它們都把圖表提交到屏幕。它們都帶流入接口以便包容器能通知控件開始和停止該圖表。它們都支持圖表線的顏色和消息間隔長度作為屬性而可以永久存在。最後,它們都支持缺省事件集,將關於在一個特定時間段裡處理的消息的數量通知包容器。圖3顯示了這兩個控件。
Figure 3 監視 ActiveX 控件消息
用MFC開發一個控件
用MFC開發一個ActiveX控件涉及到在Visual Studio®.中使用ActiveX ControlWizard。為了開始一個新的控件,從File菜單中選擇New,然後從工程類型列表中選擇MFC ActiveX控件Wizard。首先,ControlWizard要求你決定在DLL中包括多少個控件。接著你就可以選擇你打算怎樣實現你的控件。
ControlWizard提供的第一批選項總體上適用於控件的DLL。它們包括了許可支持、源碼注釋和在線幫助。選擇許可使得ControlWizard使用BEGIN_OLEFACTORY和END_OLEFACTORY (而非DECLARE__OLECREATE).。BEGIN_OLEFACTORY 和 END_OLEFACTORY宏覆蓋了VerifyUserLicense和GetLicenseKey,因而為你的控件提供許可支持。請求ControlWizard包括注釋將所有的TODO注釋加入代碼中。最後,請求ControlWizard包括在線幫助將為DLL創建樣板HELP文件源代碼。
一旦你通過了第一個對話框,ControlWizard就顯示一個對話框用來配置DLL中的控件。這些配置選擇包括使控件在運行時可見的選項,使得控件在可見時激活的選項,使得對象可以被插入的選項,給控件一個About框的選項,使得控件像一個簡單的框架控件那樣行為的選項。圖4解釋了不同的選項是如何影響ControlWizard生成的代碼的。
ControlWizard還有一個將控件實現為一個標准的Windows控件的選項,就像一個編輯框或者一個按鈕。這是一個有趣的選項。例如,如果你選擇按照一個按鈕將你的控件分成子類,控件的窗口實際上是一個按鈕。此時,PreCreateWindow攔截控件的窗口創建,當創建控件的窗口時使用BUTTON窗口類。ControlWizard使你可以選擇一些高級的選項,包括無窗口的激活,使你的控件具有完整的設備上下文,實現無抖動的激活,使你的控件在非激活狀態也接受鼠標消息,使你的控件異步的加載自己的屬性。這裡有一個每個選項如何影響ControlWizard生成的代碼的綱要。
無窗口的激活此選項覆蓋COleControl::GetControlFlags,將windowlessActivate標志附加到控件標志中。一旦使此選項有效,包容器就將輸入消息送交到控件的IoleInPlaceObjectWindowless接口。此接口ColeControl的實現通過你的消息映射分發消息。你就能通過簡單的添加相應的入口到消息映射表,像處理一般windows消息那樣處理消息了。
無省略的設備上下文選擇了此選項覆蓋COleControl::GetControlFlags,關閉clipPaintDC位,從而在ColeControl的 OnPaint函數中去掉了IntersectClipRect調用。如果你確定你的控件並不需要在客戶區外部繪圖,這個選項就有用了,因為使對IntersectClipRect的調用失效後,有一個明顯的速度的提高。
無抖動的激活選擇此選項覆蓋COleControl::GetControlFlags,將缺省控件標志與noFlickerActivate逐位相或。控件在激活的時候檢查此標志以阻止控件在激活和非激活狀態轉換時被重畫。如果你的控件在激活和非激活狀態外觀一樣,那麼這個選項就是有用的。
非激活時的鼠標指針通知這個選項覆蓋COleControl::GetControlFlags,附加了pointerInactive位。IpointerInactive接口使得一個對象大多數時間保持非激活,然而仍然參加與鼠標的某些操作的交互,例如拖放。
優化的繪圖碼 這個選項覆蓋COleControl::GetControlFlags,打開canOptimizeDraw位,具有優化繪圖代碼的控件檢查這個標志(通過COleControl’的IsOptimizedDraw函數)來確定控件是否需要在完成繪畫後將舊的對象復原回設備上下文。
異步加載屬性此選項將stock ReadyState屬性和stock ReadyStateChange事件加入到控件中去。這將使得控件異步的加載其屬性。例如,一個加載大量的數據作為其屬性之一的控件會需要很長的時間來加載,而鎖住了控件。這個stock屬性和事件使得此控件立刻開始加載過程。包容器使用此事件和屬性判斷控件何時完成加載。
當ControlWizard完成這些事情後,你就得到了編譯到一個包含此控件的DLL的源代碼(擴展名是.OLX)。由wizard產生的源代碼包括一個從ColeControlModule(它是從CwinApp中派生的)中派生的類。這個類包含整個控件模塊的初始化代碼。接著,wizard為基於ColeControl的表示每個控件的類生成源代碼。最後,wizard生成一些ODL代碼用來建立類型信息。
一旦wizard產生了控件DLL,你就面臨完善這個控件的任務了。這意味著添加渲染代碼,開發一個引入接口(方法和屬性),rigging up屬性頁,展示某些事件。但是,在我向你說明所有這些都是如何工作之前,我們先來看一下使用ATL創建一個控件都需要什麼。
用ATL開發一個控件
有了基於MFC的控件,你就可以用ATL COM App Wizard得到一個開發基於ATL的控件的觸發器。使用ATL來創建控件可以分為兩步。雖然MFC的Control Wizard要求你預先確定你希望在DLL中包含多少個控件,ATL COM Wizard簡單的創建DLL——你可以以後使用ATL對象選項從Insert菜單添加控件。當創建一個新的基於ATL的DLL時,你可以選擇混用MFC支持。你還可以選擇在控件的DLL中合並任何proxy/stub代碼。這使得如果有人希望遠程控制你的控件實現的代碼,你只要發布一個文件。
一旦生成了基於ATL的DLL,你就可以開始添加COM類了。Insert New ATL Object菜單條使得這項工作變得十分容易。選擇此菜單項顯示一個用來創建任何一個COM類的對話框,包括無格式的COM對象,ActiveX控件以及Microsoft事務服務器組件(Windows NT Server的一部分)。
當添加基於ATL的控件到你的工程的時候,ATL Object Wizard比MFC Object Wizard提供了更大范圍的選項。對於新手來說,ATL使得你可以選擇使用任何現有的線性模型實現你的控件。你可以將你的類標記為或者單線程或者單元線程的。ATL Object Wizard限制你創建一個自由的或者混合線程的控件,因為控件通常是面向UI的。
如果你創建了一個單線程的控件,一個包容的控件的客戶將總是將它加載到主、單線程的單元(STA)中。結果,只有在客戶進程空間運行的單主線程才會接觸到你的對象,這樣就免除了你保護你的控件狀態不受並發訪問干擾的責任。另外,因為你的對象的所有實例將只會被一個線程接觸,你將不必擔心DLL中的全局數據。如果你的控件是單元線程的,你還免除了保護你的控件的內部狀態的大部分負擔。然而,你仍然不得不保護DLL中的全局數據。為什麼呢?首先,設想你的控件是由客戶的單線程創建的。現在假定客戶試圖創建該控件的另一份拷貝——但是是從一個雲向在當前進程的多線程的單元中。通過將你的對象標記為單元線程的,COM被告知你希望你的控件保護免遭並發訪問。COM在它加載時為你的控件創建一個新的STA。現在當線程調用到你的對象時,它們只能通過單元邊界來訪問它,遠程層將同步對此對象的調用。然而,當某個控件的狀態被保護不被並發訪問,作為在一個STA中的副產品,由控件的實例所共享的數據(象在DLL中的全局數據一樣)是脆弱的。這是因為你的全局DLL數據(同時為幾個對象服務,分別運行在獨立的線程中)會被那些多線程同時接觸到。
雖然基於MFC的COM類總是可聚合的(內置了對它的支持),ATL ObjectWizard使得你可以指定你的控件支持聚合,只是可聚合的,或者是獨立的對象。根據你選擇的聚合選項,ATL ObjectWizard使用一個宏來執行聚合策略。例如,缺省的COM類的實現是可聚合的——對象將既運行在獨立的模式,又作為一個聚合的一部分。如果你使你的COM對象不可聚合,ObjectWizard把DECLARE_NOT_ AGGREGATABLE宏加到你的類定義中。如果你選擇了僅是可聚合的,ObjectWizard把DECLARE_ ONLY_AGGREGATABLE宏加入到類定義中。
這裡是宏如何工作的。缺省的對象創建在一個名為_CreatorClass的類中發生。_CreatorClass當被加入到服務器范圍的對象映射後(這是OBJECT_ENTRY宏所做的工作的一部分),就成為你的COM類的創建機制。_CreatorClass其實只是一個名為CComCreator2類的別名,此類將兩個從CcomCreator類中定制的類作為參數。此宏根據選擇的聚合模式來特制CcomCreator類,分別使用CComObject, CComAggObject, CComFailCreator, 或者CcomPolyObject:
#define DECLARE_NOT_AGGREGATABLE(x) public: \ typedef CComCreator2< CComCreator< CComObject< x > >, \ CComFailCreator > _CreatorClass; #define DECLARE_AGGREGATABLE(x) public: \ typedef CComCreator2< CComCreator< CComObject< x > >, \ CComCreator< CComAggObject< x > > > _CreatorClass; #define DECLARE_ONLY_AGGREGATABLE(x) public: \ typedef CComCreator2< CComFailCreator, \ CComCreator< CComAggObject< x > > > _CreatorClass; #define DECLARE_POLY_AGGREGATABLE(x) public: \ typedef CComCreator< CComPolyObject< x > > _CreatorClass;
ATL ObjectWizard Attributes頁中最後三個檢查框包括對COM例外處理的支持(例如,IsupportErrorInfo接口),連接點以及自由線程集(FTM)。你也可以添加IsupportErrorInfo到控件的繼承列表中,提供ISupportErrorInfo::InterfaceSupportsErrorInfo的一個實現。打開連接點將添加IConnectionPointImpl 模板類到控件的繼承列表中。
將你的控件聚合到FTM使得單元間(以及Windows 2000的上下文間)的調用更為頻繁的發生,如果兩個對象正好位於同一個進程中。然而,你在編寫控件時不應該檢查這一點,因為當你使用FTM的時候,你多少都違反了單元(以及Windows 2000的上下文)規則。關於FTM的更多細節,請參見Don Box的Effective COM (Addison-Wesley Longman, 1998)一書。
除了你可以應用到所有COM對象的一般選項,ATL ObjectWizard還提供了幾個控件創建特定的選項。首先,ATL ObjectWizard讓你從一個常規控件(例如一個按鈕或是一個編輯控件)中進行子類劃分。你可以為你的空間指定其它幾個選項使得它更加不透明,給它一個更實心的背景,在運行時不可見,或者是你的控件象一個按鈕那樣的工作。下面是控件屬性頁提供的選項的一個大綱:
不透明和實心背景如果你希望使所有的包容器都不顯示在控件邊界之後,選擇"opaque"檢查框,這是控件傳給它的包容器的狀態信息。結果是,控件在說明它將畫出它的完整矩形。選擇此選項設置VIEWSTATUS_OPAQUE位以便IViewObjectExImpl::GetViewStatus向包容器指示一個不透明的控件。你可能還想選擇一個實心的背景。這個選項設置VIEWSTATUS_ SOLIDBKGND 位以便GetViewStatus指示控件有一個實心的背景。
運行時不可見此選項使你的控件在運行時不可見。你可以使用不可見控件在後台完成某些操作,例如周期性的激發事件。此選項在它加入到注冊表中後使得控件翻轉OLEMISC_INVISIBLEATRUNTIME 位。
仿按鈕此選項使你的控件象一個按鈕那樣工作。此時,控件將在包容器周圍屬性DisplayAsDefault的基礎上顯示為缺省的按鈕。如果控件的位置標記為缺省按鈕,控件將顯示為一個較厚的框架。選擇此選項在它加入到注冊表中後使得控件翻轉OLEMISC_ACTSLIKEBUTTON 。
仿標簽選擇此選項使得你的控件取代包容器的內部標簽。這使得控件在它加入到注冊表中後標記OLEMISC_ACTSLIKELABEL。
在超類基礎上添加控件選擇此選項使得你的控件根據一種標准window類進行子類劃分。下拉列表包含了Windows定義的window類。當你選擇這些類名中的一個時,向導添加一個CcontainedWindow成員變量到你的控件類中。CContainedWindow::Create將你指定的window類超類化。
規格化DC選擇此選項使得你的控件在被調用來畫自己時創建一個規格化的設備上下文。這標准化了控件的外觀,但是效率降低了。此選項生成的代碼覆蓋了OnDrawAdvanced方法(而不是常規的OnDraw方法)。
可插入的選擇此選項使得你的控件顯示在象Microsoft Excel 和Word 這樣的應用的Insert Object對話框中。你的控件就能夠被插入到任何支持嵌入對象的應用中了。選擇此選項在注冊表項中增加了Insertable鍵。
僅為窗口化的選擇此選項迫使你的控件窗口化,即使在支持無窗口對象的包容器中。如果你不選擇此選項,你的控件將會自動的適應包容器:在支持無窗口對象的包容器中是無窗口的,在不支持無窗口對象的包容器中是有窗口的。這將使CComControlBase::m_bWindowOnly標志設置為TRUE。ATL使用此標志來決定在控件激活過程中是否要查詢包容器的IoleInPlaceSiteWindowless接口。
ATL要求你預先在Stock Properties頁中決定你的對象的stock屬性,你可以選擇Caption或者 Border Color這樣的屬性,或者通過點擊>>按鈕一次性選擇所有的stock屬性。這將向控件的屬性映射中添加屬性。
在運行ATL COM App Wizard 和 ObjectWizard之後,你就得到了一個完整的DLL,它具有一個COM DLL所必需的所有分支。此控件顯示的眾所周知的出口包括DllGetClassObject, DllCanUnloadNow, DllRegisterServer,和 DllUnregisterServer。另外,你得到了一個滿足COM主要需求的對象——包括一個主流入接口和一個類對象。
一旦你已經使用一個向導開始了一個工程,下一步就是使控件做點有趣的事情了。通常出發點是控件的翻譯代碼。你立刻得到一些可視化的反饋。讓我們來看一下一個基於MFC的控件的翻譯是怎樣發生的。
控件翻譯
MFC和ATL在翻譯處理上是相似的。在每一個框架裡,實現控件的類具有一個名為OnDraw的虛函數。你只需將你的翻譯代碼添加到OnDraw函數裡。然而,在各框架裡,OnDraw函數得工作有所不同。
MFC的OnDraw在兩種上下文下調用。第一個上下文發生在控件響應一個WM_PAINT消息時。此時,傳遞給OnDraw函數的設備上下文代表了真實的設備上下文。如果控件正被要求render它自己作為對客戶調用IViewObjectEx::Draw的響應,設備上下文或者是一個元設備上下文,或者是一個常規設備上下文。下面的代碼說明了基於MFC的控件是怎樣被render的:
void CMFCMsgTrafficCtrl::OnDraw(CDC* pdc, const CRect& rcBounds,
const CRect& rcInvalid)
{
// TODO: 用你自己的繪圖代碼代替下面的代碼
pdc->FillRect(rcBounds,
CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH)));
ShowGraph(*pdc, const_cast<CRect&>(rcBounds), nMessagesToShow);
}
COleControl::OnDraw的簽名包括一個代表控件大小的矩形和一個代表控件非法區域的矩形。MFC調用控件的OnDraw函數來響應一個WM_PAINT消息。此時,OnDraw函數接受一個真實的設備上下文來繪圖。MFC還調用控件的OnDraw函數來響應IViewObject::Draw中的一個調用。MFC的實現調用COleControl::OnDrawMetafile,它的缺省OnDrawMetafile調用COleControl::OnDraw。當然,這暗示了控件的實時翻譯是與控件的元文件表示相同的,該元文件在設計時與包容器一塊存儲。你可以使得控件的實時的翻譯與設計時的翻譯不同,這通過重載COleControl::OnDrawMetafile來實現。通過調用你的控件的InvalidateControl方法,你可以強制進行一個重繪。ATL的翻譯機制非常類似於MFC。CComControlBase::OnPaint建立一個ATL_DRAWINFO結構,包括創建一個繪圖設備上下文。然後ATL調用控件的OnDrawAdvanced函數。OnDrawAdvanced生成元文件,接著調用你的控件的OnDraw方法,它使用ATL_DRAWINFO結構中的信息來知道怎樣在屏幕上繪圖。下面是ATL_DRAWINFO結構:
struct ATL_DRAWINFO
{
UINT cbSize;
DWORD dwDrawAspect;
LONG lindex;
DVTARGETDEVICE* ptd;
HDC hicTargetDev;
HDC hdcDraw;
LPCRECTL prcBounds; //在其中繪圖的矩形
LPCRECTL prcWBounds; //WindowOrg and Ext if metafile
BOOL bOptimize;
BOOL bZoomed;
BOOL bRectInHimetric;
SIZEL ZoomNum; //ZoomX = ZoomNum.cx/ZoomNum.cy
SIZEL ZoomDen;
};
ATL為你填寫此結構。當你正在屏幕上繪圖時,你所感興趣的最重要的域是hdcDraw 和 prcBounds。如果你對在一個元文件裡繪圖感興趣,或者你需要注意縮放因子等等,那麼其它域也是重要的。下面的代碼顯示了基於ATL的消息流控件是怎樣處理繪圖的:
HRESULT CATLMsgTrafficCtl::OnDraw(ATL_DRAWINFO& di)
{
RECT& rc = *(RECT*)di.prcBounds;
HBRUSH hBrush = CreateSolidBrush(RGB(255, 255, 255));
FillRect(di.hdcDraw, &rc, hBrush);
DeleteObject(hBrush);
Rectangle(di.hdcDraw, rc.left, rc.top, rc.right, rc.bottom);
ShowGraph(di.hdcDraw, rc, nMessagesToShow);
return S_OK;
}
注意當你使用ATL的時候,你必須處理設備和GDI句柄。在ATL中,你調用你的控件的FireViewChange函數來強制控件的一次重畫。
開發一個流入接口
當開發一個基於MFC的ActiveX控件時,缺省的流入接口是一個分發接口。Visual C++ 和 MFC使得開發一個流入分發接口變得十分簡單——只需使用ClassWizard來生成方法和屬性。每次你使用ClassWizard添加一個新的屬性或方法,它就插入一個入口到你的控件的分發映射中。MFC使用分發映射來滿足客戶的調用請求。
MFC的缺點是在你的控件中增加一個常規的COM接口是一個枯燥無味的過程。此過程包括使用MFC的COM宏來建立實現接口的嵌套的類。
當為你的基於ATL的COM控件開發主流入接口時,類視是添加屬性和方法的最好的手段。一當你為控件生成了代碼,ATL ObjectWizard即添加一個缺省的流入接口。這可以是一個雙端接口,也可以是一個常規的自定義接口,取決於你先前設定的工程選項。
Visual Studio的類視向你顯示了你的工程中包含的所有的類和接口,在類視中右擊一個接口的定義時,即可添加一個屬性或者方法。使用類視來定義接口非常方便,因為每次你添加一個方法或者屬性的時候,類視都會更新IDL,類源代碼以及頭文件。
不象MFC,給控件添加一個常規COM接口是非常容易的。在ATL中,你只要簡單的添加新的接口樣板文件連接(goo)(一個GUID,關鍵字對象和關鍵字接口)。類視將會顯示新的接口,你可以繼續添加新的成員。
添加屬性
一個ActiveX控件經常包含屬性,它們是描述控件的狀態的成員變量。給一個基於MFC的控件添加屬性的最好的手段是利用ClassWizard。ClassWizard的自動為你添加成員變量,將它們映射到缺省的分發接口。ClassWizard給你提供了兩種選擇:你可以添加一個成員變量,包括一個變化通告函數,或者你可以添加一對Get/Set函數,手動的添加成員變量。除了給控件添加你自己的定制屬性,ClassWizard使你象添加背景和標題一樣的添加庫存。ClassWizard甚至自動為你的類添加一個成員變量。
為一個基於ATL的控件添加屬性有一點不同,你為控件中的每個屬性添加單獨的存取程序和變異因子函數(propget 和 propput函數)。然而,類視只是定義了接口函數。你還要手工添加數據成員到類中,然後簡單的實現這些函數。
基於ATL的控件還支持stock屬性,ATL ControlWizard預先要求你確定希望哪些stock屬性包括在你的控件中。添加至少一個stock屬性到控件中使得控件繼承自ATL的CstockPropImpl類。CstockPropImpl是Idispatch的一個實現,優化來顯示ActiveX控件的stock屬性,為每個標准的stock屬性包含了兼容Idispatch的get 和 put函數。
ControlWizard還給控件添加代表stock屬性的數據成員,例如,如果你添加了背景顏色的stock屬性,ControlWizard添加一個名為m_clrBackColor的數據成員到你的類中。CstockPropImple一次性的為所有標准的stock屬性的get 和 put函數添加實現。所有這些函數期望在你的類中看到合適的成員變量(象對應背景顏色的m_clrBackColor)。
編譯器將在stock屬性沒有包括的那些get和put函數上阻塞。實現過程希望在你的類中看到成員變量。為了消除編譯器錯誤,CcomControlBase添加了一個聯合結構,它包括了stock的get 和 put函數希望看到的所有成員的名字。然而,給控件添加數據成員重載了聯合類型中的名字,CstopPropImpl類在它的get 和 put函數中使用控件的成員變量
如果你忘記了使用ControlWizard預先添加stock屬性,你總可以手工添加相關代碼——即,從CstockPropImpl繼承,然後為你想要顯示的屬性添加成員變量。
屬性持續
MFC的屬性持續機制是非常直觀易懂的。從編程的觀點來看,所有你要做的是填寫ControlWizard已經提供的DoPropExchange函數。DoPropExchange將控件屬性的狀態從某些成員變量移動到持續媒體中。
MFC具有3個屬性持續機制,內置於ColeControl:IPersistPropertyBag, IPersistStorage和 IPersistStream[Init]。所有這些持續機制都封裝在MFC的CpropExchange類中,與當你需要serialize 一個文檔時Carchive為你包裝一個文件非常相似。客戶方選擇使用3個接口中的一個保持對象。不管使用了哪種持續機制,執行總落在控件的DoPropExchange函數中。
下面的代碼顯示了MFCMsgTraffic控件是怎樣將它的顏色和時間間隔屬性保存起來的:
void CMFCMsgTrafficCtrl::DoPropExchange(CPropExchange* pPX)
{
ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));
COleControl::DoPropExchange(pPX);
PX_Color(pPX, "GraphLineColor", m_graphLineColor);
PX_Long(pPX, "GraphInterval", m_interval);
}
MFC包括了若干PX_函數在控件和存儲媒體間轉移數據,它們是:
PX_Short
PX_UShort
PX_Long
PX_ULong
PX_Color
PX_Bool
PX_String
PX_Currency
PX_Float
PX_Double
PX_Blob
PX_Font
PX_Picture
PX_IUnknown
PX_VBXFontConvert
PX_DataPath
在ATL中管理控件屬性持續涉及到兩個步驟。第一步是添加你希望客戶能夠使用的持續接口的ATL實現。ATL包括了類IPersistStorageImpl, IPersistStreamInitImpl, 和 IPersistPropertyBagImpl, 它們實現了三個主COM持續機制。
第二步是在控件的屬性映射中插入屬性。當一個客戶請求保存或者加載基於ATL的控件時,ATL檢查控件的屬性映射表,將控件的屬性輸出到存儲媒介,或者從存儲媒介輸入。屬性映射表是屬性名字、DISPIDs的一個表,有時還包括一個屬性頁面GUID。ATL遍歷詞表查找該持續哪個屬性,並將其持續到合適的媒體。圖5顯示了繼承了持續接口實現和一個屬性映射的ATLMsgTraffic控件。
屬性頁
ActiveX控件經常在開發者將控件放到各類容器時提供屬性頁幫助開發者。將消息流控件放入一個對話框的開發者可能想要配置控件的各個方面,象控件的取樣間隔,或者繪圖線條的顏色。例如,當控件放在一個對話框中,你想通過右擊鼠標得到控件的屬性時,Visual Studio顯示了一個突出的對話框。這裡將說明其工作過程。
Visual Studio請求控件在一個對話框框架裡顯示屬性頁(Visual Studio IspecifyPropertyPages接口請求控件提供一個屬性頁的清單),屬性頁顯示在Visual Studio中,但是通過控件提供的一個COM接口,保持與控件的連接。每當你完成了屬性編輯並從Visual Studio中關閉了對話框,它就會要求屬性頁更新控件中的屬性。
當你生成一個MFC的控件時,wizard給你一個對話框模板和一個從ColePropertyPage中派生的代表此控件的缺省屬性頁的類。Visual Studio使得實現一個控件的屬性和此屬性頁中的屬性的連接變得容易了。當你使用ControlWizard的Automation tab添加屬性到你的基於MFC的控件中的時候,你給了屬性一個外部名字。這個名字是外部客戶方(包括屬性頁)用來識別該屬性的。
你按照開發其它任何對話框的方法來開發屬性頁——將控件添加到對話框模板,將對話框成員變量和控件聯系起來。ControlWizard添加DDX/DDV代碼在對話框控件和成員變量之間交換數據。然而,當你將成員變量和對話框控件相關聯時,ControlWizard給你提供了這樣一個機會,你可以將外部屬性名字用於對話框的成員變量。此外部名字是你給控件添加屬性時鍵入的字符串。
當屬性頁需要將變化應用於控件時(例如當按下Apply按鈕時),屬性頁使用控件的Idispatch接口以及外部名字來修改控件的屬性。在MFC中,你可以通過ClassWizard來添加一個新屬性,添加一個新的對話框模板到工程中,讓ClassWizard創建一個類——要確保是從ColePropertyPage中派生出來的類。然後,為了使新的屬性頁可以被外界訪問到,將它的GUID添加到控件的屬性頁映射中(在控件的.CPP文件中查找BEGIN_ PROPPAGEIDS 和 END_PROPPAGEIDS兩個宏)。不象MFC的ActiveX ControlWizard,ATL COM App Wizard並不向DLL中添加缺省的屬性頁。這意味著你要自己完成此工作。幸運的是,又一個wizard可以向屬性頁中添加基於ATL的DLL。只要選擇Insert ATL Object,然後找到屬性頁對象。Wizard將一個對話框模板和一個C++類與所有必要的COM goo一起添加到一個屬性頁中。讓它們完成什麼工作是你的事情。
不幸的是,ATL屬性頁的wizard驅動特性不如基於MFC的屬性頁,你得手工完成應用和顯示操作。這就意味著提供Apply 和 Show的函數實現到你的屬性頁類中。Apply函數只是提取對話框中控件的狀態,遍歷屬性頁擁有的指向控件的接口指針列表,使用接口指針來修改控件屬性。Show函數通常提取控件的狀態,然後以次來組織對話框的控件。下面的代碼顯示了基於ATL的屬性頁是怎樣處理Apply函數的:
STDMETHOD(Apply)(void)
{
long nInterval = GetDlgItemInt(IDC_EDITINTERVAL);
ATLTRACE(_T("CMainPropPage::Apply\n"));
for (UINT i = 0; i < m_nObjects; i++)
{
IATLMsgTrafficCtl* pATLMsgTrafficCtl;
m_ppUnk[i]->QueryInterface(IID_IATLMsgTrafficCtl,
(void**)&pATLMsgTrafficCtl);
if(pATLMsgTrafficCtl) {
pATLMsgTrafficCtl->put_Interval(nInterval);
pATLMsgTrafficCtl->Release();
}
}
m_bDirty = FALSE;
return S_OK;
}
為基於ATL的控件提供一個屬性頁的第二步是確保屬性頁的CLSID出現在控件的屬性映射中,圖5中列出的屬性持續代碼提供了它的一個例子。消息映射表明了控件的圖線顏色,被標准的顏色屬性頁管理。控件的取樣間隔由控件的主屬性頁來管理。
Window 消息
MFC和ATL在它們處理window消息方面有很多共同之處,都使用消息映射,都有wizards來生成代碼處理window消息。在MFC中,消息映射可以添加到任何一個CcmdTarget派生的類中,然後你就可以用ClassWizard來建立你的控件的事件處理器了。圖6顯示了基於MFC的控件怎樣處理WM_ TIMER消息。另外,MFC提供了處理命令和控件通告的宏。象MFC一樣,ATL通過消息映射來處理window消息,只要你的類是從CwindowImpl派生的,而且包含ATL的消息映射宏,你就可以使用類視來建立事件處理器。圖7顯示了ATL消息流控制是怎樣處理WM_TIMER消息的。
ATL使用MESSAGE_HANDLER宏將標准的window消息映射到一個C++類。此宏簡單的產生一個將window消息和類的成員函數關聯的表。除了常規消息,消息映射還可以處理其它類型的事件。圖8顯示了能參與消息映射的各種宏。
連接和事件
最後要進行的比較是MFC和ATL各是怎樣處理連接點和事件集的。為了管理連接點和事件集,需要一個COM類來實現IconnectionPointContainer,然後創建一種提供指向IconnectionPoint的指針給客戶的方法。MFC的主控件類,ColeControl,已經有了內置的IconnectionPointContainer,MFC通過連接映射提供了連接點。MFC已經為IPropertyNotifySink定義了連接點和控件的缺省事件集。
為了完善一個基於MFC的控件的缺省事件集,你只要簡單的使用ClassWizard’s ActiveX Event tab。在你使用ClassWizard添加事件的時候,Visual Studio更新你的控件的.ODL文件,為潛在的包容器描述外出事件。另外,Visual Studio添加一個函數到你的類中,你可以調用它反過來向包容器激發事件。圖9演示了基於MFC的控件的事件觸發機制。基於MFC的控件的事件觸發函數只是由包容器在它和控件建立連接點時提供的一個Idispatch指針的一些簡單的包裹器。
在基於ATL的控件中建立事件則有所不同。在基於ATL的控件中,你從定義控件的.IDL文件中的事件開始。接著你建立了類型庫的編譯工程文件。圖10顯示了在IDL中描述的基於ATL的控件的事件集。
一但類型庫編譯通過,你就可以通過在類視中選擇控件的類,在類上右擊,然後選擇Implement Connection Point,讓類視來為你創建一個回調代理了。Visual Studio彈出一個對話框,列出控件類型庫中所有可訪問的事件接口。你選擇那些你希望回調代理做的,Visual Studio就為你寫一個代理。圖11顯示了基於ATL的消息流控制的回調代理。Visual Studio產生的回調代理代表了一個C++友好的函數集,被客戶實現的接口所調用。
MFC的IconnectionPointContainer實現是硬分布到ColeControl中,並且每個連接點是由一個連接映射處理的,而ATL的實現是用多重繼承處理的。你的控件類繼承IconnectionPointContainerImpl和類視生成的代理。如果你開始一個工程的時候,選擇了"Supports connection points",ObjectWizard就為你添加IconnectionPointContainerImpl。如果你忘了標記檢查框,你可以寫進去。此代碼顯示了連接點機制是怎樣加入一個控件中的。類ATL_NO_VTABLE CATLMsgTrafficCtl :
{
•••
public IConnectionPointContainerImpl<CATLMsgTrafficCtl>,
public CProxy_DATLMsgTrafficEvents<CATLMsgTrafficCtl>
•••
{
LRESULT OnTimer(UINT msg, WPARAM wParam,
LPARAM lParam,
BOOL& bHandled) {
//•••
if(nMessagesToShow > m_threshold)
{
Fire_ExceededThreshold(nMessagesToShow, m_threshold);
}
//•••
}
};
作為一個應用框架ATL和MFC的比較
最近,許多開發者開始對使用ATL作為框架來開發應用和控件感興趣了。當然,MFC已經使用了很長時間了,是一個能夠開發可雙擊的基於Windows的應用的非常成熟的框架。例如,MFC包括了這樣的特性作為一個總體文檔/視體系結構:Object Linking 和 Embedding支持, 以及工具條和狀態欄。
然而,所有這些功能都是有代價的。一些更加普遍的抱怨是MFC的比較深的足跡(無論是在DLL中,或是在靜態連接的版本中),以及自身的某種相互依存性。例如,買入MFC的一種特性意味著買入MFC的對象連接和嵌入,這意味著買入MFC的文檔/視結構。另一方面,ATL是一個原始的框架,沒有任何應用框架goodies。
正象你已經看到的,兩個框架都提供創建控件的可行途徑。然而,兩者都各有千秋,也各有弊病。用MFC編寫控件通常更加容易——尤其是如果你不是在開發COM集中的應用並且你需要windowing和drawing支持。ATL的體系架構更加靠近COM的核心,你還會經常發現自己在編寫很多的SDK類型的代碼——就是說,你在回過頭來用window和設備上下文句柄。ATL為更廣范圍的控件類型提供了很大的支持,象復合控件,基於HTML的控件,沒有design-time接口的輕量級的控件等等。MFC僅提供完全成熟的控件。
對ATL進行分支的實現是非常直接的。例如,增加一個接口通常是一件添加接口到繼承表,在COM映射中添加一個入口然後實現接口函數的工作。分支MFC的實現通常是一種折磨。例如,添加一個接口到基於MFC的控件意味著處理所有那些接口映射宏。
最後,ATL提供了大量的調試支持,包括接口引用計數以及QueryInterface調試支持,這在MFC中是沒有的。
這兩種體系架構的區別是非常明顯的。通常,MFC使得你很快完成你的工程並更快的運行起來,但是犧牲了靈活性。ATL沒有那樣快,那樣容易使用,但是它是COM友好的。而且,好像隨著ATL的成熟,它將會越來越容易使用。