本文描述了使用ATL開發一個ActiveX控件的完整過程。
一、創建項目
單擊起始頁中的“New Project…”,選擇“ATL”分類下的“ATL Project”項目,項目名稱爲“Calculator”。在隨後出現的項目嚮導中,使用默認配置即可。
二、添加控件
在解決方案管理器中的項目上右擊,依次選擇“Add”、“Class”,在添加類對話框中選擇ATL分類下的ATL Control類型。單擊“Add”按鈕,將會出現添加ATL Control嚮導。
在嚮導的第二步中,將接口類型選擇爲“Dual”,爲控件支持事件做爲準備,在Support選項中,選中“Connection points”複選框。
隨後出現選擇控件要實現的接口的界面,除VS默認添加的實現外,再添加IObjectSafety接口,實現該接口可以避免控件在IE中使用時IE彈出運行的腳本不安全的提示。
三、爲控件添加並實現方法
在Class View窗口中右擊ICalc接口,依次選擇“Add”、“Add Method…”,此處假定我們實現一個加法運算,將方法命名爲“Add”,然後添加參數:
需要注意的是對返回值的處理。應將參數類型選定爲DOUBLE*,並選中“retval”複選框。
嚮導結束後,VS自動在Calc.cpp中添加了該方法的空實現,略加修改後的方法代碼爲:
STDMETHODIMP CCalc::Add(DOUBLE a, DOUBLE b, DOUBLE* result) { *result = a + b; return S_OK; }
測試該方法:
由於只是調用該控件進行加法運算,並不需要該控件的界面展示,因此在測試控件之前,可以將VS自動生成的OnDraw方法中的其他代碼刪除,直接返回 S_OK 即可。
對VS自動生成的用於測試的htm略做修改來測試添加的方法。修改後的完整htm代碼如下:
<HTML> <HEAD> <TITLE>ATL 8.0 test page for object Calc</TITLE> </HEAD> <BODY> <OBJECT ID="Calc" CLASSID="CLSID:59443E6F-7B99-4F75-A7AF-6FEE5B8208CD"></OBJECT> <input type="button" value="Add" οnclick="add();" /> <script type="text/javascript"> function add() { var calc = document.getElementById('Calc'); var result = calc.Add(2, 3); alert(result); } </script> </BODY> </HTML>
點擊“Add”按鈕後的運行效果:
四、爲控件添加事件
假定控件進行的是一個非常複雜的運算,爲了在調用運算時不阻塞調用者線程,可以使用異步方式完成運算。控件在完成運算時需要通知調用者,這時便需要事件。
首先按照步驟三中的方法,添加一個異步調用加法運算的方法AddAsync,然後爲控件添加運算完成的事件AddCompleted。
在Class View窗口中右擊_ICalcEvents接口,依次選擇“Add”、“Add Method…”,根據添加方法嚮導添加AddCompleted方法,如下圖所示:
然後在Class View窗口中右擊CCalc類,依次選擇“Add”、“Add Connection Point…”,在彈出的實現連接點窗口中實現_ICalcEvents接口。
完成嚮導後,VS會自動爲我們生成基本框架,包括引發事件的方法Fire_AddCompleted。我們只需在AddAsync方法中添加運算並在運算結束時調用Fire_AddCompleted的代碼:
STDMETHODIMP CCalc::AddAsync(DOUBLE a, DOUBLE b) { double result; result = a + b; Fire_AddCompleted(&result); return S_OK; }
在網頁中添加異步計算的代碼進行測試(添加的javascript代碼如下),應該能夠得到我們想要的效果。
<script type="text/javascript"> function addAsync() { var calc = document.getElementById('Calc'); calc.attachEvent("AddCompleted", OnAddCompleted); calc.AddAsync(3, 4); } function OnAddCompleted(result) { alert(result); } </script>
五、ActiveX控件的事件與多線程
細心的讀者一定會發現步驟四中所謂的“異步”是假的:雖然在運算結束後使用事件對調用者進行通知,但由於運算是在主線程上進行的,所以調用過程仍是同步的。步驟四其實只是展示了一下事件的簡單用法,如果真正實現異步,則需要在控件中使用多線程。
在ActiveX控件中使用多線程時需要注意的是:引發事件(即調用Fire_XXXX)必須在窗口線程中進行,否則會導致事件不能被ActiveX控件的容器處理。如果事件發生時執行代碼的線程不是窗口線程。那麼應該使用PostMessage或SendMessage來通知窗口線程,並在消息處理函數中執行Fire_XXXX。爲了使用控件的消息機制,還應該在CCalc的構造函數中將m_bWindowOnly字段設置爲TRUE。
以下是改爲多線程後的部分示例代碼:
至此,一個簡單的ActiveX控件就開發完成了。關於ActiveX控件的打包部署等問題,可以參考以下內容:
本文示例所使用的開發環境爲Visual Studio 2010。
附:使用VC6開發時的注意事項
在今天看來,VC6顯得有些古老,但由於目前能見到的大多數版本的Windows操作系統已經內置了運行VC6開發的應用程序所需要的庫,因此從方便發佈的角度看,使用VC6來開發ActiveX控件不失爲一個好的選擇。
使用VC6開發ActiveX控件與上文所述步驟大同小異,但是需要注意微軟給開發者留下的兩道試題--使用VC6嚮導生成的代碼中包含兩處錯誤:
第一處錯誤位於連接點映射,DIID__IXXXXEvents中的第一個字符‘D’需手動添加。
示例代碼:
BEGIN_CONNECTION_POINT_MAP(CCalc)
CONNECTION_POINT_ENTRY(DIID__ICalcEvents) // 修改 IID_XXXX 爲 DIID_XXXX
END_CONNECTION_POINT_MAP()
該錯誤會造成生成失敗,比較容易發現。
第二處錯誤位於Fire_XXXX方法內,不會造成生成失敗但會造成運行結果莫名其妙,因此該錯誤更隱蔽一些。
示例代碼:
if (pConnection) { CComVariant avarParams[1]; avarParams[0].byref = result; //此處自動生成的代碼有錯誤,應去掉原代碼中的取址運算 avarParams[0].vt = VT_R8|VT_BYREF; CComVariant varResult; //... }