介紹
在第六章,我將介紹ATL對在對話框中使用ActiveX控件的支持,由於ActiveX控件就是ATL的專業,所以WTL沒有添加其他的輔助類。不過,在ATL中使用ActiveX控件與在MFC中有很大的不同,所以需要重點介紹。我將介紹如何包容一個控件並處理控件的事件,開發ATL應用程序相對於MFC的類向導來說有點不方便。在WTL程序中自然可以使用ATL對包容ActiveX控件的支持。
例子工程演示如何使用IE的浏覽器控件,我選擇浏覽器控件有兩個好處:
我當然無法與那些花了大量時間編寫基於IE浏覽器控件的定制浏覽器的人相比,不過,當你讀完本篇文章之後,你就知道如何開始編寫自己定制的浏覽器!
從使用向導開始 創建工程WTL的向導可以創建一個支持包容ActiveX控件的程序,我將開始一個名為IEHoster的新工程。我們像上一章一樣使用無模式對話框,只是這次要選上支持ActiveX控件包容(Enable ActiveX Control Hosting),如下圖:
選上這個check box將使我們的對話框從CAxDialogImpl派生,這樣就可以包容ActiveX控件。在向導的第二頁還有一個名為包容ActiveX控件的check box,但是選擇這個好像對最後的結果沒有影響,所以在第一頁就可以點擊“Finish”結束向導。
向導生成的代碼在這一節我將介紹一些以前沒有見過的新代碼(由向導生成的),下一節介紹ActiveX包容類的細節。
首先要看的文件是stdafx.h,它包含了這些文件:
#include <atlbase.h>
#include <atlapp.h>
extern CAppModule _Module;
#include <atlcom.h>
#include <atlhost.h>
#include <atlwin.h>
#include <atlctl.h>
// .. other WTL headers ...
atlcom.h和atlhost.h是很重要的兩個,它們含有一些COM相關類的定義(比如智能指針CComPtr),還有可以包容控件的窗口類。
接下來看看maindlg.h中聲明的CMainDlg類:
class CMainDlg : public CAxDialogImpl<CMainDlg>,
public CUpdateUI<CMainDlg>,
public CMessageFilter, public CIdleHandler
CMainDlg現在是從CAxDialogImpl類派生的,這是使對話框支持包容ActiveX控件的第一步。
最後,看看WinMain()中新加的一行代碼:
int WINAPI _tWinMain(...)
{
//...
_Module.Init(NULL, hInstance);
AtlAxWinInit();
int nRet = Run(lpstrCmdLine, nCmdShow);
_Module.Term();
return nRet;
}
AtlAxWinInit()注冊了一個類名未AtlAxWin的窗口類,ATL用它創建ActiveX控件的包容窗口。
使用資源編輯器添加控件和MFC的程序一樣,ATL也可以使用資源編輯器向對話框添加控件。首先,在對話框編輯器上點擊鼠標右鍵,在彈出的菜單中選擇“Insert ActiveX control”:
VC將系統安裝的控件顯示在一個列表中,滾動列表選擇“Microsoft Web Browser”,單擊Insert按鈕將控件加入到對話框中。查看控件的屬性,將ID設為IDC_IE。對話框中的控件顯示應該是這個樣子的:
如果現在編譯運行程序,你會看到對話框中的浏覽器控件,它將顯示一個空白頁,因為我們還沒有告訴它到哪裡去。
在下一節,我將介紹與創建和包容ActiveX控件有關的ATL類,同時我們也會明白這些類是如何與浏覽器交換信息的。
ATL中使用控件的類
在對話框中使用ActiveX控件需要兩個類協同工作:CAxDialogImpl和CAxWindow。它們處理所有控件容器必須實現的接口方法,提供通用的功能函數,例如查詢控件的某個特殊的COM接口。
CAxDialogImpl第一個類是CAxDialogImpl,你的對話框要能夠包容控件就必須從CAxDialogImpl類派生而不是從CDialogImpl類派生。CAxDialogImpl類重載了Create()和DoModal()函數,這兩個函數分別被全局函數AtlAxCreateDialog()和AtlAxDialogBox()調用。既然IEHoster對話框是由Create()創建的,我們看看AtlAxCreateDialog()到底做了什麼工作。
AtlAxCreateDialog()使用輔助類_DialogSplitHelper裝載對話框資源,這個輔助類遍歷所以對話框的控件,查找由資源編輯器創建的特殊的入口,這些特殊的入口表示這是一個ActiveX控件。例如,下面是IEHoster.rc文件中浏覽器控件的入口:
CONTROL "",IDC_IE,"{8856F961-340A-11D0-A96B-00C04FD705A2}",
WS_TABSTOP,7,7,116,85
第一個參數是窗口文字(空字符串),第二個是控件的ID,第三個是窗口的類名。_DialogSplitHelper::SplitDialogTemplate()函數找到以''{''開始的窗口類名時就知道這是一個ActiveX控件的入口。它在內存中創建了一個臨時對話框模板,在這個新模板中這些特殊的控件入口被創建的AtlAxWin窗口代替,新的入口是在內存中的等價體:
CONTROL "{8856F961-340A-11D0-A96B-00C04FD705A2}",IDC_IE,"AtlAxWin",
WS_TABSTOP,7,7,116,85
結果就是創建了一個相同ID的AtlAxWin窗口,窗口的標題是ActiveX控件的GUID。所以你調用GetDlgItem(IDC_IE)返回的值是AtlAxWin窗口的句柄而不是ActiveX控件本身。
SplitDialogTemplate()函數完成工作後,AtlAxCreateDialog()接著調用CreateDialogIndirectParam()函數使用修改後的模板創建對話框。
AtlAxWin and CAxWindow正如上面講到的,AtlAxWin實際上是ActiveX控件的宿主窗口,AtlAxWin還會用到一個特殊的窗口接口類:CAxWindow,當AtlAxWin從模板創建一個對話框後,AtlAxWin的窗口處理過程,AtlAxWindowProc(),就會處理WM_CREATE消息並創建相應的ActiveX控件。ActiveX控件還可以在運行其間動態創建,不需要對話框模板,我會在後面介紹這種方法。
WM_CREATE的消息處理函數調用全局函數AtlAxCreateControl(),將AtlAxWin窗口的窗口標題作為參數傳遞給該函數,大家應該記得那實際就是浏覽器控件的GUID。AtlAxCreateControl()有會調用一堆其他函數,不過最終會用到CreateNormalizedObject()函數,這個函數將窗口標題轉換成GUID,並最終調用CoCreateInstance()創建ActiveX控件。
由於ActiveX控件是AtlAxWin的子窗口,所以對話框不能直接訪問控件,當然CAxWindow提供了這些方法通控件通信,最常用的一個是QueryControl(),這個方法調用控件的QueryInterface()方法。例如,你可以使用QueryControl()從浏覽器控件得到IWebBrowser2接口,然後使用這個接口將浏覽器引導到指定的URL。
調用控件的方法
既然我們的對話框有一個浏覽器控件,我們可以使用COM接口與之交互。我們做得第一件事情就是使用IWebBrowser2接口將其引導到一個新URL處。在OnInitDialog()函數中,我們將一個CAxWindow變量與包容控件的AtlAxWin聯系起來。
CAxWindow wndIE = GetDlgItem(IDC_IE);
然後聲明一個IWebBrowser2的接口指針並查詢浏覽器控件的這個接口,使用CAxWindow::QueryControl():
CComPtr<IWebBrowser2> pWB2;
HRESULT hr;
hr = wndIE.QueryControl ( &pWB2 );
QueryControl()調用浏覽器控件的QueryInterface()方法,如果成功就會返回IWebBrowser2接口,我們可以調用Navigate():
if ( pWB2 )
{
CComVariant v; // empty variant
pWB2->Navigate ( CComBSTR("http://www.codeproject.com/"),
&v, &v, &v, &v );
}
響應控件觸發的事件
從浏覽器控件得到接口非常簡單,通過它可以單向的與控件通信。通常控件也會以事件的形式與外界通信,ATL有專用的類包裝連接點和事件相應,所以我們可以從控件接收到這些事件。為使用對事件的支持需要做四件事:
將CMainDlg轉變成COM對象的原因是事件相應是基於IDispatch的,為了讓CMainDlg暴露這個接口,它必須是個COM對象。IDispEventSimpleImpl提供了IDispatch接口的實現和建立連接點所需的處理函數,當事件發生時IDispEventSimpleImpl還調用我們想要接收的事件的處理函數。
以下的類需要添加到CMainDlg的集成列表中,同時COM_MAP列出了CMainDlg暴露的接口:
#include <exdisp.h> // browser control definitions
#include <exdispid.h> // browser event dispatch IDs
class CMainDlg : public CAxDialogImpl<CMainDlg>,
public CUpdateUI<CMainDlg>,
public CMessageFilter, public CIdleHandler,
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CMainDlg>,
public IDispEventSimpleImpl<37, CMainDlg, &DIID_DWebBrowserEvents2>
{
...
BEGIN_COM_MAP(CMainDlg)
COM_INTERFACE_ENTRY2(IDispatch, IDispEventSimpleImpl)
END_COM_MAP()
};
CComObjectRootEx類CComCoClass共同使CMainDlg成為一個COM對象,IDispEventSimpleImpl的模板參數是事件的ID,我們的類名和連接點接口的IID。事件ID可以是任意正數,連接點對象的IID是DIID_DWebBrowserEvents2,可以在浏覽器控件的相關文檔中找到這些參數,也可以查看exdisp.h。
填寫事件映射鏈
下一步是給CMainDlg添加事件映射鏈,這個映射鏈將我們感興趣的事件和我們的處理函數聯系起來。我們要看的第一個事件是DownloadBegin,當浏覽器開始下載一個頁面時就會觸發這個事件,我們響應這個事件顯示“please wait”信息給用戶,讓用戶知道浏覽器正在忙。在MSDN中可以查到DWebBrowserEvents2::DownloadBegin事件的原型
void DownloadBegin();
這個事件沒有參數,也不需要返回值。為了將這個事件的原型轉換成事件響應鏈,我們需要寫一個_ATL_FUNC_INFO結構,它包含返回值,參數的個數和參數類型。由於事件是基於IDispatch的,所以所有的參數都用VARIANT表示,這個數據結構的描述相當長(支持很多個數據類型),以下是常用的幾個:
VT_EMPTY: void
VT_BSTR: BSTR 格式的字符串
VT_I4: 4字節有符號整數,用於long類型的參數
VT_DISPATCH: IDispatch*
VT_VARIANT>: VARIANT
VT_BOOL: VARIANT_BOOL (允許的取值是VARIANT_TRUE和VARIANT_FALSE)
另外,標志VT_BYREF表示將一個參數轉換成相應的指針。例如,VT_VARIANT|VT_BYREF表示VARIANT*類型。下面是_ATL_FUNC_INFO的定義:
#define _ATL_MAX_VARTYPES 8
struct _ATL_FUNC_INFO
{
CALLCONV cc;
VARTYPE vtReturn;
SHORT nParams;
VARTYPE pVarTypes[_ATL_MAX_VARTYPES];
};
參數:
cc 我們的事件響應函數的調用方式約定,這個參數必須是CC_STDCALL,表示是__stdcall方式 vtReturn 事件響應函數的返回值類型 nParams 事件帶的參數個數 pVarTypes 相應的參數類型,按從左到右的順序
了解這些之後,我們就可以填寫DownloadBegin事件處理的_ATL_FUNC_INFO結構:
_ATL_FUNC_INFO DownloadInfo = { CC_STDCALL, VT_EMPTY, 0 };
現在,回到事件響應鏈,我們為每一個我們想要處理的事件添加一個SINK_ENTRY_INFO宏,下面是處理DownloadBegin事件的宏:
class CMainDlg : public ...
{
...
BEGIN_SINK_MAP(CMainDlg)
SINK_ENTRY_INFO(37, DIID_DWebBrowserEvents2, DISPID_DOWNLOADBEGIN,
OnDownloadBegin, &DownloadInfo)
END_SINK_MAP()
};
這個宏的參數是事件的ID(37,與我們在IDispEventSimpleImpl的繼承列表中使用的ID一樣),事件接口的IID,事件的dispatch ID(可以在MSDN或exdispid.h頭文件中查到),事件處理函數的名字和指向描述這個事件處理的_ATL_FUNC_INFO結構的指針。
編寫事件處理函數好了,等了這麼長時間(吹個口哨!),我們可以寫事件處理函數了:
void __stdcall CMainDlg::OnDownloadBegin()
{
// show "Please wait" here...
}
現在來看一個復雜一點的事件,比如BeforeNavigate2,這個事件的原型是:
void BeforeNavigate2 (
IDispatch* pDisp, VARIANT* URL, VARIANT* Flags,
VARIANT* TargetFrameName, VARIANT* PostData,
VARIANT* Headers, VARIANT_BOOL* Cancel );
此方法有7個參數,對於VARIANT類型參數可以從MSDN查到它到底傳遞的是什麼類型的數據,我們感興趣的是URL,是一個BSTR類型的字符串。
描述BeforeNavigate2事件的_ATL_FUNC_INFO結構是這樣的:
_ATL_FUNC_INFO BeforeNavigate2Info =
{ CC_STDCALL, VT_EMPTY, 7,
{ VT_DISPATCH, VT_VARIANT|VT_BYREF, VT_VARIANT|VT_BYREF,
VT_VARIANT|VT_BYREF, VT_VARIANT|VT_BYREF, VT_VARIANT|VT_BYREF,
VT_BOOL|VT_BYREF }
};
和前面一樣,返回值類型是VT_EMPTY表示沒有返回值,nParams是7,表示有7個參數。接著是參數類型數組,這些類型前面介紹過了,例如VT_DISPATCH表示IDispatch*。
事件響應鏈的入口與前面的例子很相似:
BEGIN_SINK_MAP(CMainDlg)
SINK_ENTRY_INFO(37, DIID_DWebBrowserEvents2, DISPID_DOWNLOADBEGIN,
OnDownloadBegin, &DownloadInfo)
SINK_ENTRY_INFO(37, DIID_DWebBrowserEvents2, DISPID_BEFORENAVIGATE2,
OnBeforeNavigate2, &BeforeNavigate2Info)
END_SINK_MAP()
事件處理函數是這個樣子:
void __stdcall CMainDlg::OnBeforeNavigate2 (
IDispatch* pDisp, VARIANT* URL, VARIANT* Flags,
VARIANT* TargetFrameName, VARIANT* PostData,
VARIANT* Headers, VARIANT_BOOL* Cancel )
{
CString sURL = URL->bstrVal;
// ... log the URL, or whatever you''d like ...
}
我打賭你現在是越來越喜歡ClassWizard了,因為當你向MFC的對話框插入一個ActiveX控件時ClassWizard自動為你完成了所有工作。
將CMainDlg轉換成對象需要注意幾件事情,首先必須修改全局函數Run(),現在CMainDlg是個COM對象,我們必須使用CComObject創建CMainDlg:
int Run(LPTSTR /*lpstrCmdLine*/ = NULL, int nCmdShow = SW_SHOWDEFAULT)
{
CMessageLoop theLoop;
_Module.AddMessageLoop(&theLoop);
CComObject<CMainDlg> dlgMain;
dlgMain.AddRef();
if ( dlgMain.Create(NULL) == NULL )
{
ATLTRACE(_T("Main dialog creation failed!\n"));
return 0;
}
dlgMain.ShowWindow(nCmdShow);
int nRet = theLoop.Run();
_Module.RemoveMessageLoop();
return nRet;
}
另一個可替代的方法是不使用CComObject,而使用CComObjectStack類,並刪除dlgMain.AddRef()這一行代碼,CComObjectStack對IUnknown的三個方法的實現有些微不足道(它們只是簡單的從函數返回),因為它們不是必需的--這樣的COM對象可以忽略對引用的計數,因為它們僅僅是創建在棧中的臨時對象。
當然這並不是完美的解決方案,CComObjectStack用於短命的臨時對象,不幸的是只要調用它的任何一個IUnknown方法都會引發斷言錯誤。因為CMainDlg對象在開始監聽事件時會調用AddRef,所以CComObjectStack不適用於這種情況。
解決這個問題要麼堅持使用CComObject,要麼從CComObjectStack派生一個CComObjectStack2類,允許對IUnknow方法調用。CComObject的那個不必要的引用計數並無大礙--人們不會注意到它的發生--但是如果你必須節省那個CPU時鐘周期的話,你可以使用本章的例子工程代碼中的CComObjectStack2類。
回顧例子工程現在我們已經看到事件響應如何工作了,再來看看完整的IEHoster工程,它包容了一個浏覽器控件並響應了6個事件,它還顯示了一個事件列表,你會對浏覽器如何使用它們提供帶進度條的界面有個感性的認識,程序處理了以下幾個事件:
程序有四個按鈕控制浏覽器工作:向後,向前,停止和刷新,它們分別調用IWebBrowser2相應的方法。
事件和伴隨事件發送的數據都被記錄在列表控件中,你可以看到事件的觸發,你還可以關閉一些事件記錄而僅僅觀察其中的一輛個事件。為了演示事件處理的重要作用,我們在BeforeNavigate2事件處理函數中檢查URL,如果發現“doubleclick.net”就取消導航。廣告和彈出窗口過濾器等一些IE的插件使用的就是這個方法而不是HTTP代理,下面就是做這些檢查的代碼。
void __stdcall CMainDlg::OnBeforeNavigate2 (
IDispatch* pDisp, VARIANT* URL, VARIANT* Flags,
VARIANT* TargetFrameName, VARIANT* PostData,
VARIANT* Headers, VARIANT_BOOL* Cancel )
{
USES_CONVERSION;
CString sURL;
sURL = URL->bstrVal;
// You can set *Cancel to VARIANT_TRUE to stop the
// navigation from happening. For example, to stop
// navigates to evil tracking companies like doubleclick.net:
if ( sURL.Find ( _T("doubleclick.net") ) > 0 )
*Cancel = VARIANT_TRUE;
}
下面就是我們的程序工作起來的樣子:
IEHoster還使用了前幾章介紹過得類:CBitmapButton(用於浏覽器控制按鈕),CListViewCtrl(用於事件記錄),DDX (跟蹤checkbox的狀態)和CDialogResize.
運行時創建ActiveX控件
出了使用資源編輯器,還可以在運行其間動態創建ActiveX控件。About對話框演示了這種技術。對話框編輯器預先放置了一個group box用於浏覽器控件的定位:
在OnInitDialog()函數中我們使用 CAxWindow創建了一個新AtlAxWin,它定位於我們預先放置好的group box的位置上(這個group box隨後被銷毀):
LRESULT CAboutDlg::OnInitDialog(...)
{
CWindow wndPlaceholder = GetDlgItem ( IDC_IE_PLACEHOLDER );
CRect rc;
CAxWindow wndIE;
// Get the rect of the placeholder group box, then destroy
// that window because we don''t need it anymore.
wndPlaceholder.GetWindowRect ( rc );
ScreenToClient ( rc );
wndPlaceholder.DestroyWindow();
// Create the AX host window.
wndIE.Create ( *this, rc, _T(""),
WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN );
接下來我們用CAxWindow方法創建一個ActiveX控件,有兩個方法可以選擇:CreateControl()和CreateControlEx()。CreateControlEx()用一個額外的參數返回接口指針,這樣就不需要再調用QueryControl()函數。我們感興趣的兩個參數是第一個和第四個參數,第一個參數是字符串形式的浏覽器控件的GUID,第四個參數是一個IUnknown*類型的指針,這個指針指向ActiveX控件的IUnknown接口。創建控件後就可以查詢IWebBrowser2接口,然後就可以像前面一樣控制它導航到某個URL。
CComPtr<IUnknown> punkCtrl;
CComQIPtr<IWebBrowser2> pWB2;
CComVariant v;
// Create the browser control using its GUID.
wndIE.CreateControlEx ( L"{8856F961-340A-11D0-A96B-00C04FD705A2}",
NULL, NULL, &punkCtrl );
// Get an IWebBrowser2 interface on the control and navigate to a page.
pWB2 = punkCtrl;
pWB2->Navigate ( CComBSTR("about:mozilla"), &v, &v, &v, &v );
}
對於有ProgID的ActiveX控件可以傳遞ProgID給CreateControlEx(),代替GUID。例如,我們可以這樣創建浏覽器控件:
// 使用控件的ProgID: 創建Shell.Explorer:
wndIE.CreateControlEx ( L"Shell.Explorer", NULL,
NULL, &punkCtrl );
CreateControl()和CreateControlEx()還有一些重載函數用於一些使用浏覽器的特殊情況,如果你的應用程序使用WEb頁面作為HTML資源,你可以將資源ID作為第一個參數,ATL會創建浏覽器控件並導航到這個資源。IEHoster包含一個ID為IDR_ABOUTPAGE的WEB頁面資源,我們在About對話框中使用這些代碼顯示這個頁面:
wndIE.CreateControl ( IDR_ABOUTPAGE );
這是顯示結果:
例子代碼對上面提到的三個方法都用到了,你可以查看CAboutDlg::OnInitDialog()中的注釋和未注釋的代碼,看看它們分別是如何工作的。
鍵盤事件處理最後一個但是非常重要的細節是鍵盤消息。ActiveX控件的鍵盤處理非常復雜,因為控件和它的宿主程序必須協同工作以確保控件能夠看到它感興趣的消息。例如,浏覽器控件允許你使用TAB鍵在鏈接之間切換。MFC自己處理了所有工作,所以你永遠不會意識到讓鍵盤完美並正確的工作需要多麼大的工作量。
不幸的是向導沒有為基於對話框的程序生成鍵盤處理代碼,當然,如果你使用Form View作為視圖類的SDI程序,你會看到必要的代碼已經被添加到PreTranslateMessage()中。當程序從消息隊列中得到鼠標或鍵盤消息時,就使用ATL的WM_FORWARDMSG消息將此消息傳遞給當前擁有焦點的控件。它們通常不作什麼事情,但是如果是ActiveX控件,WM_FORWARDMSG消息最終被送到包容這個控件的AtlAxWin,AtlAxWin識別WM_FORWARDMSG消息並采取必要的措施看看是否控件需要親自處理這個消息。
如果擁有焦點的窗口沒有識別WM_FORWARDMSG消息,PreTranslateMessage()就會接著調用IsDialogMessage()函數,使得像TAB這樣的標准對話框的導航鍵能正常工作。
例子工程的PreTranslateMessage()函數中含有這些必需的代碼,由於PreTranslateMessage()只在無模式對話框中有效,所以如果你想在基於對話框的應用程序中正確使用鍵盤就必須使用無模式對話框。 繼續在下一章,我們將回到框架窗口並介紹如何使用分隔窗口。
修改記錄May 20, 2003: 文章第一次發布。