目錄
1.7.6 修改DllUnregisterServer 14
1.7.7 修改Fire_ClickIn、Fire_ClickOut 14
第1章 VC++6.0創建
本章內容根據MSDN98的ATL Tutorial翻譯、整理而成。
1.1 目標
本章的目標是使用ATL創建一個下圖所示的ActiveX控件。

圖1.1
這個控件只有一個屬性 short Sides;用於指定多邊形的邊數。
這個控件有兩個事件:
void ClickIn([in]long x, [in] long y);
void ClickOut([in] long x, [in] long y);
當鼠標左鍵在多邊形內部按下時將觸發ClickIn事件;當鼠標左鍵在多邊形外部按下時將觸發ClickOut事件。
1.2 創建項目
運行VC++6.0,新建"ATL COM AppWizard"項目,如下圖所示。配置好項目名稱、項目目錄后,單擊"OK"按鈕。

圖1.2

采用默認設置,直接單擊"Finish"按鈕。

圖1.3
單擊"OK"按鈕,完成項目創建。

圖1.4
1.3 增加COM類
單擊【Insert】【New ATL Object...】

圖1.5
選中Controls里的Full Control,單擊"Next"按鈕

圖1.6
在Names頁面,請輸入COM類的名稱。

圖1.7
在Attributes頁面,請勾中Support ISupportErrorInfo(錯誤信息)和 Support Connection Points(連接點)這兩個復選框。其中,連接點非常重要:ActiveX控件通過連接點把事件傳遞給客戶程序。

圖1.8
Miscellaneous頁面的設置保持不變

圖1.9
在Stock Properties頁面,增加庫存屬性——Fill Color

圖1.10
上圖中,單擊"確定"按鈕完成COM類的添加。
1.4 屬性
鼠標右鍵單擊接口IPolyCtl,彈出菜單中單擊【Add Property】。注意:不要混淆IPolyCtl和_IPolyCtlEvents,后者只是用來產生事件。

圖1.11
多邊形邊數的Property Type(屬性類型)為short,Property Name(屬性名稱)為Sides。單擊"OK"按鈕完成屬性Sides的添加。

圖1.12
1.5 事件
現在添加兩個事件:
void ClickIn([in]long x, [in] long y);
void ClickOut([in] long x, [in] long y);
當鼠標左鍵在多邊形內部按下時將觸發ClickIn事件;當鼠標左鍵在多邊形外部按下時將觸發ClickOut事件。
鼠標右鍵單擊_IPolyCtlEvents,彈出菜單中單擊【Add Method...】

圖1.13
依下圖顯示進行配置,單擊"OK"按鈕完成ClickIn的添加。

圖1.14
用同樣的方法添加ClickOut事件。
1.6 實現連接點
鼠標右鍵單擊"Polygon.idl"文件,彈出菜單中單擊【Compile Polygon.idl】,編譯此文件。

圖1.15
鼠標右鍵單擊"CPolyCtl",彈出菜單中單擊【Implement Connection Point...】

圖1.16
勾中"_IPolyCtlEvents",然后單擊"OK"按鈕。

圖1.17
1.7 編碼
1.7.1 增加成員變量
給CPolyCtl增加成員變量m_nSides和m_arrPoint。可以增加到庫存屬性m_clrFillColor的下方:
| OLE_COLOR m_clrFillColor; //填充色(庫存屬性) short m_nSides; //多邊形邊數 POINT m_arrPoint[100]; //多邊形頂點坐標 |
1.7.2 初始化成員變量
CPolyCtl的構造函數里對成員變量進行初始化:
| CPolyCtl() { m_nSides = 3; //初始化為三角形 m_clrFillColor = RGB(0, 0xFF, 0); //填充顏色默認為綠色 memset(m_arrPoint,0,sizeof(m_arrPoint)); //多邊形頂點初始化為零 } |
1.7.3 完成屬性賦值代碼
| STDMETHODIMP CPolyCtl::get_Sides(short *pVal) { *pVal = m_nSides; return S_OK; } STDMETHODIMP CPolyCtl::put_Sides(short newVal) { if (newVal > 2 && newVal < 101) { m_nSides = newVal; FireViewChange(); return S_OK; } return Error(_T("Shape must have between 3 and 100 sides")); } |
獲取Sides屬性時,將調用get_Sides函數;修改Sides屬性時,將調用put_Sides函數。FireViewChange()將通知控件重新繪制。CComCoClass::Error函數將產生一個錯誤信息。
1.7.4 完成控件繪制代碼
控件的繪制由CPolyCtl::OnDraw負責。
| HRESULT OnDraw(ATL_DRAWINFO& di) { RECT& rc = *(RECT*)di.prcBounds; HDC hdc = di.hdcDraw; COLORREF colFore; HBRUSH hOldBrush, hBrush; HPEN hOldPen, hPen; //Translate m_colFore into a COLORREF type OleTranslateColor(m_clrFillColor, NULL, &colFore); //Create and select the colors to draw the circle hPen = (HPEN)GetStockObject(BLACK_PEN); hOldPen = (HPEN)SelectObject(hdc, hPen); hBrush = (HBRUSH)GetStockObject(WHITE_BRUSH); hOldBrush = (HBRUSH)SelectObject(hdc, hBrush); Ellipse(hdc, rc.left, rc.top, rc.right, rc.bottom); // Create and select the brush that will be used to fill the polygon hBrush = CreateSolidBrush(colFore); SelectObject(hdc, hBrush); CalcPoints(rc); Polygon(hdc, &m_arrPoint[0], m_nSides); // Select back the old pen and brush and delete the brush we created SelectObject(hdc, hOldPen); SelectObject(hdc, hOldBrush); DeleteObject(hBrush); return S_OK; } |
上面的OleTranslateColor用於將OLE_COLOR轉換為COLORREF。CalcPoints函數用於計算多邊形頂點坐標至m_arrPoint,其代碼如下:
| void CPolyCtl::CalcPoints(const RECT& rc) { const double pi = 3.14159265358979; POINT ptCenter; double dblRadiusx = (rc.right - rc.left) / 2; double dblRadiusy = (rc.bottom - rc.top) / 2; double dblAngle = 3 * pi / 2; // Start at the top double dblDiff = 2 * pi / m_nSides; // Angle each side will make ptCenter.x = (rc.left + rc.right) / 2; ptCenter.y = (rc.top + rc.bottom) / 2; // Calculate the points for each side for (int i = 0; i < m_nSides; i++) { m_arrPoint[i].x = (long)(dblRadiusx*cos(dblAngle) + ptCenter.x + 0.5); m_arrPoint[i].y = (long)(dblRadiusy*sin(dblAngle) + ptCenter.y + 0.5); dblAngle += dblDiff; } } |
1.7.5 響應鼠標左鍵按下消息
鼠標右鍵單擊"CPolyCtl",彈出菜單中單擊【Add Windows Message Handler...】。

圖1.18
選中"WM_LBUTTONDOWN"消息,然后依次單擊"Add Handler"和"OK"按鈕或直接單擊"Add and Edit"按鈕。

圖1.19
編輯CPolyCtl::OnLButtonDown函數如下:
| LRESULT OnLButtonDown(UINT uMsg ,WPARAM wParam, LPARAM lParam, BOOL& bHandled) { HRGN hRgn; WORD xPos = LOWORD(lParam); // horizontal position of cursor WORD yPos = HIWORD(lParam); // vertical position of cursor CalcPoints(m_rcPos); // Create a region from our list of points hRgn = CreatePolygonRgn(&m_arrPoint[0], m_nSides, WINDING); // If the clicked point is in our polygon then fire the ClickIn // event otherwise we fire the ClickOut event if (PtInRegion(hRgn, xPos, yPos)) Fire_ClickIn(xPos, yPos); else Fire_ClickOut(xPos, yPos); // Delete the region that we created DeleteObject(hRgn); return 0; } |
Fire_ClickIn將觸發ClickIn事件給客戶程序;Fire_ClickOut將觸發ClickOut事件給客戶程序。
1.7.6 修改DllUnregisterServer
| STDAPI DllUnregisterServer(void) { HRESULT hr = _Module.UnregisterServer(); #if _WIN32_WINNT >= 0x0400 if (FAILED(hr)) return hr; // Following assumes that the type library version is 1.0 hr = UnRegisterTypeLib(LIBID_POLYGONLib, 1, 0 , LOCALE_NEUTRAL, SYS_WIN32); #endif return hr; } |
1.7.7 修改Fire_ClickIn、Fire_ClickOut
CProxy_IPolyCtlEvents的函數Fire_ClickIn和Fire_ClickOut中的如下代碼有問題:
| pvars[1] = x; pvars[0] = y; |
請更改為:
| pvars[1].vt = VT_I4; pvars[1].lVal= x; pvars[0].vt = VT_I4; pvars[0].lVal= y; |
1.7.8 保存、恢復屬性值
完成上述步驟,即可編譯本項目,生成的Polygon.dll將自動注冊。VB6.0里也可以使用這個控件了。但是,兩個屬性里FillColor可以保存,Sides卻不能保存。也就是說:VB6.0里增加本控件,修改FillColor和Sides屬性,下次再打開此項目時FillColor是上次修改的值,而Sides將恢復成構造函數里的數值3。
為此,需要增加下代碼PROP_ENTRY("Sides",1,CLSID_NULL),如下所示:
| BEGIN_PROP_MAP(CPolyCtl) PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4) PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4) PROP_ENTRY("FillColor", DISPID_FILLCOLOR, CLSID_StockColorPage) PROP_ENTRY("Sides", 1, CLSID_NULL) END_PROP_MAP() |
PROP_ENTRY("Sides",1,CLSID_NULL)中的"Sides"是屬性名稱。1是該屬性在odl文件里的順序號。CLSID_NULL表示該屬性不在任何屬性頁面內。
1.7.9 屬性頁
1.7.9.1 增加屬性頁面類
單擊【Insert】【New ATL Object...】

圖1.20
選中"Controls"里的"Property Page",單擊"Next"按鈕

圖1.21
頁面Names里輸入名稱

圖1.22
頁面Attributes采用默認設置

圖1.23
頁面Strings中的Title是屬性頁面的名稱。

圖1.24
上圖中,單擊"確定"按鈕,完成屬性頁面類的添加。
1.7.9.2 編輯屬性頁面
鼠標右鍵單擊"CPolyProp"(屬性頁面類),彈出菜單中單擊【Go To Dialog Editor】。

圖1.25
顯示頁面設計界面如下。增加一個文本框(IDC_SIDES)

圖1.26
1.7.9.3 響應Apply
單擊屬性頁上的Apply按鈕,會調用CPolyProp::Apply函數。在這里,把屬性頁面上的輸入值賦給屬性值,代碼如下:
| STDMETHOD(Apply)(void) { USES_CONVERSION; ATLTRACE(_T("CPolyProp::Apply\n")); for (UINT i = 0; i < m_nObjects; i++) { CComQIPtr<IPolyCtl, &IID_IPolyCtl> pPoly(m_ppUnk[i]); short nSides = (short)GetDlgItemInt(IDC_SIDES); if FAILED(pPoly->put_Sides(nSides)) { CComPtr<IErrorInfo> pError; CComBSTR strError; GetErrorInfo(0, &pError); pError->GetDescription(&strError); MessageBox(OLE2T(strError),_T("Error") ,MB_ICONEXCLAMATION); return E_FAIL; } } m_bDirty = FALSE; return S_OK; } |
1.7.9.4 使Apply按鈕可用
默認情況下,Apply按鈕是不可用的。在圖1.26中,修改Sides屬性值后,應該讓Apply按鈕可用,為此需要響應文本框的消息。
鼠標右鍵單擊"CPolyProp"(屬性頁面類),彈出菜單中單擊【Add Windows Message Handler...】。

圖1.27
先選中IDC_SIDES,然后再選中EN_CHANGE消息。最后單擊"Add and Edit"按鈕。

圖1.28
修改CPolyProp::OnChangeSides如下:
| LRESULT OnChangeSides(WORD wNotifyCode,WORD wID ,HWND hWndCtl, BOOL& bHandled) { SetDirty(TRUE); return 0; } |
SetDirty(TRUE);說明屬性值改變了,按鈕Apply就可使用了。
1.7.9.5 增加屬性頁面
修改PROP_ENTRY("Sides", 1, CLSID_NULL)中的CLSID_NULL為CLSID_PolyProp。
| BEGIN_PROP_MAP(CPolyCtl) PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4) PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4) PROP_ENTRY("FillColor", DISPID_FILLCOLOR, CLSID_StockColorPage) PROP_ENTRY("Sides", 1, CLSID_PolyProp) END_PROP_MAP() |
1.7.10 實現IObjectSafety接口
在IE瀏覽器里使用此控件,會提示如下對話框:

圖1.29
為了消除上面的對話框,需要實現IObjectSafety接口。其步驟如下:
1、給CPolyCtl增加基類
| class ATL_NO_VTABLE CPolyCtl : public CComObjectRootEx<CComSingleThreadModel> ... ... ... ,public IObjectSafetyImpl<CPolyCtl,INTERFACESAFE_FOR_UNTRUSTED_CALLER> { public: CPolyCtl() |
2、BEGIN_COM_MAP(CPolyCtl)與END_COM_MAP()之間增加代碼:
| BEGIN_COM_MAP(CPolyCtl) ... ... ... COM_INTERFACE_ENTRY(IObjectSafety) END_COM_MAP() |
1.8 注冊
ATL3.0 編寫的組件在注冊時,如果組件所在目錄包含中文,則注冊后注冊表中的路徑會有亂碼,導致無法正常使用組件。
解決方法一:使用 UNICODE,即定義宏 _UNICODE
解決方法二:
1、編譯時預定義宏 _ATL_STATIC_REGISTRY
_ATL_DLL 表示動態鏈接 ATL.DLL
_ATL_STATIC_REGISTRY 表示注冊組件時,不再使用 ATL.DLL。
不能定義 _ATL_DLL,必須定義 _ATL_STATIC_REGISTRY。這樣就不會使用 ATL.DLL,也就不會產生路徑亂碼。
2、修改 ATL\Include\STAREG.H 文件里的 AddChar 和 AddString函數:
| BOOL AddChar(const TCHAR* pch) { //if (nPos == nSize) // realloc //fix register bug with chinese path if (nPos == nSize - 1 ) { nSize *= 2; p = (LPTSTR) CoTaskMemRealloc(p, nSize*sizeof(TCHAR)); } p[nPos++] = *pch; #ifndef _UNICODE if(IsDBCSLeadByte(*pch)) { p[nPos++] = *(pch + 1); } #endif return TRUE; }
BOOL AddString(LPCOLESTR lpsz) { USES_CONVERSION; LPCTSTR lpszT = OLE2CT(lpsz); while (*lpszT) { AddChar(lpszT); #ifndef _UNICODE //fix bug with chinese path if (IsDBCSLeadByte(*lpszT)) { lpszT++; } #endif lpszT++; } return TRUE; } |
1.9 BUG
使用VC++2005、2008、2010的ATL創建而成的ActiveX控件無法被VB6.0使用。解決方法:使用VC++6.0創建項目,然后使用高版本的VC++編譯。
注意:高版本的VC++需要定義_WIN32_WINNT為0x0501
