browser icon
You are using an insecure version of your web browser. Please update your browser!
Using an outdated browser makes your computer unsafe. For a safer, faster, more enjoyable user experience, please update your browser today or try a newer browser.

用SDK玩转ActiveX

Posted by on 2005 年 08 月 10 日

你可以任意转载本文,但请在转载后的文章中注明作者和原始链接。
媒体约稿请联系 titilima_AT_163.com(把“_AT_”换成“@”)。

对源代码在ATL 7.0下不能正常运行的补遗

调用ActiveX控件?呃,这实在不是一件容易的事情:用各种封装精良的Framework(MFC、VCL等等)的话,最后成品EXE的体积难免偏大;用SDK虽然可以有效地减小这个体积,但是往往又无从下手——总之,这似乎是一件鱼与熊掌不能兼得的憾事。还好,“不容易”并不代表“不可能”,李马在本文中要介绍给诸位的,就是“玩转”ActiveX的一种方法,这种方法包括了从ActiveX控件调用到ActiveX控件事件处理的一切必要细节。当然,题目所说的“SDK”也并不是纯粹的SDK,而是借助了ATL的OLE支持,毕竟用SDK实现OLE容器太繁琐了。
在开始正文之前,我还想说明一下本文所面向的读者群。首先,你必须对SDK的编程方式和COM组件的调用方式有所了解,因为本文中的绝大部分示例代码都与之相关,涉及到这方面的知识我也不会再加以解释;其次,你可以不了解ATL,因为本文中对ATL的使用仅限于ActiveX的OLE容器,我也只是在适当的地方给予简要的说明;再次,你可以不了解COM连接点的知识,我在文中会给予详细的介绍。
那么闲话毋庸赘叙,让我们开始吧。

准备工作

现在让我们来完成代码之外的事情,请按照以下步骤建立我们的工程:

  1. 打开Visual C++,新建一个Win32 Application(我名之为ActiveX)。
  2. 新建一个Resource Script(资源脚本),在其中添加一个对话框(我名之为IDD_MAIN_DLG)。
  3. 在对话框上单击右键,选择“Insert ActiveX Control…”(如下图)。在本文中,我以Microsoft Agent Control为例,所以在之后的列表之中选择“Microsoft Agent Control 2.0”。

     

  4. 完成后的对话框如下图。

     

骨架代码

现在就可以编写代码了。建立一个C++ Source File(C++源文件),在其中输入下面的程序骨架:

C++代码
  1. #include <atlbase.h>   
  2. CComModule _Module;   
  3. #include <atlwin.h>   
  4.   
  5. #import "C:WINNTmsagentagentctl.dll"   
  6. using namespace AgentObjects;   
  7.   
  8. #include "resource.h"   
  9.   
  10. int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nShowCmd)   
  11. {   
  12.     _Module.Init(NULL, hInstance);   
  13.     _Module.Term();   
  14.     return 0;   
  15. }  

然后,在工程设置中加入atl.lib,如下图:

让我们再回过头来看看上面的代码。程序的头三行就是我在本文开头时所说到的“ATL的支持”,其中预处理的部分你大可以略去不管,你只需要了解的就是_Module这个全局变量,它保存了程序模块的一些相关信息。并且,在WinMain之中的Init和Term已经包括了CoInitialize、OleInitialize、CoUninitialize、OleUninitialize的初始化和卸载工作。
#import的一行表示导入Agent控件的类型库,并且由于Agent控件的各个接口被封装在了library AgentObjects之中(这些东西可以使用Visual Studio自带的工具“OLE/COM Object Viewer”从agentctl.dll的类型库接口定义之中看到),所以要使用AgentObjects的命名空间——当然不用也无所谓,只不过是以后的使用会稍稍麻烦一些。
现在你可以编译链接这段代码了。在编译链接完成之后,你就可以在工程目录下的Debug或Release目录下(取决于你的工程设置)发现名为agentctl.tlh和agentctl.tli的两个文件。你可以用文本方式打开它们看看,你会发现agentctl.tlh中是agentctl.dll类型库中各接口的C/C++支持以及各接口的智能指针定义;至于agentctl.tli之中,则是一些更有趣的东西,在这里我就不多介绍了。

使用ActiveX

骨架完成后,就可以使用Agent这个ActiveX控件了。不过在使用之前,你需要把你曾经用来显示对话框的代码写成类似下面这个样子:

C++代码
  1. g_hDlgMain = AtlAxCreateDialog(hInstance, MAKEINTRESOURCE(IDD_MAIN_DLG), NULL, (DLGPROC)MainDlgProc, 0);  

对于这行代码我需要解释三点。第一,由于我们的对话框中含有ActiveX控件,所以不能使用普通的CreateDialog;第二,g_hDlgMain是一个全局变量,我需要在另一个类中使用它;第三,由于我们需要显示Agent助手而不显示对话框,所以在此使用了无模式对话框——这样就可以创建一个不可见的对话框了。
现在可以在对话框的回调函数中使用ActiveX控件了。以Agent控件为例,通常使用ActiveX是类似这个样子:

C++代码
  1. CAxWindow wndAgent = GetDlgItem(hDlg, IDC_AGENT);   
  2. IAgentCtlExPtr pAgent; // IAgentCtlExPtr的定义来自于agentctl.tlh   
  3. HRESULT hr = wndAgent.QueryControl(__uuidof(IAgentCtlEx), (LPVOID *)&pAgent);  

然后,就可以利用pAgent指针对Agent控件进行操作了。你可以在对话框回调函数中的WM_INITDIALOG中加入下面的代码来测试效果:

C++代码
  1. case WM_INITDIALOG:   
  2.     {   
  3.         CAxWindow wndAgent;   
  4.         IAgentCtlExPtr pAgent;   
  5.         IAgentCtlCharactersPtr pChars;   
  6.         IAgentCtlCharacterExPtr pMerlin;   
  7.         IAgentCtlRequestPtr pRequest;   
  8.         HRESULT hr;   
  9.   
  10.         wndAgent = GetDlgItem(hDlg, IDC_AGENT);   
  11.         hr = wndAgent.QueryControl(__uuidof(IAgentCtlEx), (LPVOID *)&pAgent);   
  12.   
  13.         // 获取角色文件路径   
  14.         TCHAR szPath[MAX_PATH];   
  15.         GetWindowsDirectory(szPath, MAX_PATH);   
  16.         lstrcat(szPath, _T("\msagent\chars\merlin.acs"));   
  17.   
  18.         // 进行连接   
  19.         hr = pAgent->put_Connected((VARIANT_BOOL)-1);   
  20.   
  21.         // 获得角色列表   
  22.         hr = pAgent->get_Characters(&pChars);   
  23.         // 装载角色   
  24.         pRequest = pChars->Load(_bstr_t("merlin"), CComVariant(szPath));   
  25.         pMerlin = pChars->Character(_bstr_t("merlin"));   
  26.         // 显示角色   
  27.         pMerlin->Show();   
  28.         // 计算屏幕中央坐标,并移动   
  29.         short x = (GetSystemMetrics(SM_CXFULLSCREEN) – pMerlin->GetWidth()) / 2;   
  30.         short y = (GetSystemMetrics(SM_CYFULLSCREEN) – pMerlin->GetHeight()) / 2;   
  31.         pRequest = pMerlin->MoveTo(x, y);   
  32.         pRequest = pMerlin->Speak(CComVariant("右键单击我,选择“隐藏”以结束程序。"));   
  33.     }   
  34.     break;  

这里我有两点需要说明。第一,事实上对COM接口的调用需要非常严谨地判断每个方法返回值的成功与否,而在这里出于篇幅考虑我便将其一概略去,你可以在配套源代码中看到这些容错处理;第二,在使用Agent控件之前必须将它的Connected状态置为真(-1)——也就是pAgent->put_Connected的一句,否则以下的方法都会失败,而在MFC的封装下倒可以略去这一句,可能MFC有个自动连接的过程。
到现在为止,你应该已经可以把这个助手显示在屏幕上了,运行看看效果吧。

连接点

可能你注意到了,在WM_INITDIALOG的最后一句,我让Agent助手说了一句话:“右键单击我,选择‘隐藏’以结束程序。”但事实上如果你运行这段代码的话,你在助手身上右击鼠标并选择“隐藏”,助手虽然隐藏了起来,程序却并没有退出,这是怎么回事呢?答案是显而易见的——我并没有处理Agent控件的Hide(隐藏)事件。
那么,又如何处理这个事件呢?——在MFC中,ActiveX控件的事件是通过一张宏映射表来实现的,类似下面这个样子:

C++代码
  1. BEGIN_EVENTSINK_MAP(CActiveXDlg, CDialog)   
  2. //{{AFX_EVENTSINK_MAP(CActiveX)   
  3. ON_EVENT(CActiveXDlg, IDC_AGENT, 7 /* Hide */, OnHideAgent, VTS_BSTR VTS_I2)   
  4. //}}AFX_EVENTSINK_MAP   
  5. END_EVENTSINK_MAP()  

对于SDK来说,就没有那么简单了。我们必须从COM最底层的机制入手,也就是连接点。
那么,什么又是连接点呢?

在有的时候——比如我们这里的ActiveX的事件处理,COM服务器需要将一个接口开放给客户端,然后由客户端实现这个接口供服务器进行回调,这就是COM的连接点事件。下面,我用两个C++类来模拟连接点和COM服务器之间的关系。

 

C++代码
  1. class CSink   
  2. {   
  3. public:   
  4.     void DoSomeOtherThings()   
  5.     {   
  6.         cout << "I still want to do some other things, so this sentence is from sink." << endl;   
  7.     }   
  8. };   
  9.   
  10. class ISomeInterface   
  11. {   
  12.     CSink *m_pSink;   
  13. public:   
  14.     ISomeInterface() : m_pSink(NULL) {}   
  15.     void SetSink(CSink *pSink)   
  16.     {   
  17.         m_pSink = pSink;   
  18.     }   
  19.     void DoSomething()   
  20.     {   
  21.         cout << "Do something…." << endl;   
  22.         if (NULL != m_pSink)   
  23.         {   
  24.             m_pSink->DoSomeOtherThings();   
  25.         }   
  26.     }   
  27. };  

然后,在程序中这样调用:

C++代码
  1. CSink sink;   
  2. ISomeInterface *p = new ISomeInterface;   
  3. p->SetSink(&sink);   
  4. p->DoSomething();   
  5. delete p;  

如你所见,ISomeInterface并不是一个严格意义上的“接口”,而是一个实实在在的类,不过既然这是段模拟代码,所以我也没有必要做得惟妙惟肖了——同样,你也可以把new和delete的过程看作一个接口CoCreateInstance和Release的过程。CSink类则是用来处理ISomeInterface类回调事件的,在COM中我们称之为“接收器”。那么,ISomeInterface::SetSink就是设置连接点的“连接”过程,CSink::DoSomeOtherThings()则是接收器的事件处理实现。

接收器的实现

现在,我们就要开始本文最核心的部分了。通常对于COM接收器来讲,我们可以将它理解为一个没有CLSID的COM组件类,也就是说,我们只需要给出它的实现,并提供它的指针给服务器就足够了。而且对于我们的实现语言——C++来说,这个实现过程其实和编写一个C++类没有什么两样。那么,首先让我把这个实现完整地呈现给你,然后再逐一解释吧。

C++代码
  1. // 处理连接点事件的接收器实现   
  2. class CSink : public IDispatch   
  3. {   
  4. public:   
  5.     // 构造/析构函数   
  6.     CSink() : m_uRef(0) {}   
  7.     virtual ~CSink() {}   
  8.     // IUnknown接口实现   
  9.     STDMETHODIMP QueryInterface(REFIID iid, void **ppvObject)   
  10.     {   
  11.         if (iid == __uuidof(_AgentEvents))   
  12.         {   
  13.             *ppvObject = (_AgentEvents *)this;   
  14.             AddRef();   
  15.             return S_OK;   
  16.         }   
  17.         if (iid == IID_IUnknown)   
  18.         {   
  19.             *ppvObject = (IUnknown *)this;   
  20.             AddRef();   
  21.             return S_OK;   
  22.         }   
  23.         return E_NOINTERFACE;   
  24.     }   
  25.     ULONG STDMETHODCALLTYPE AddRef()   
  26.     {   
  27.         m_uRef++;   
  28.         return m_uRef;   
  29.     }   
  30.     ULONG STDMETHODCALLTYPE Release()   
  31.     {   
  32.         ULONG u = m_uRef–;   
  33.         if (0 == m_uRef)   
  34.         {   
  35.             delete this;   
  36.         }   
  37.         return u;   
  38.     }   
  39.     // IDispatch接口实现   
  40.     STDMETHODIMP GetTypeInfoCount(UINT *pctinfo)   
  41.     {   
  42.         return E_NOTIMPL;   
  43.     }   
  44.     STDMETHODIMP GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo)   
  45.     {   
  46.         return E_NOTIMPL;   
  47.     }   
  48.     STDMETHODIMP GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId)   
  49.     {   
  50.         return E_NOTIMPL;   
  51.     }   
  52.     STDMETHODIMP Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams,   
  53.         VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)   
  54.     {   
  55.         HRESULT hr = S_OK;   
  56.         if (NULL != pDispParams && 7 == dispIdMember)   
  57.         {   
  58.             if (2 == pDispParams->cArgs )   
  59.             {   
  60.                 if (VT_I2 == pDispParams->rgvarg[0].vt && VT_BSTR == pDispParams->rgvarg[1].vt)   
  61.                 {   
  62.                     OnHide(pDispParams->rgvarg[1].bstrVal, pDispParams->rgvarg[0].iVal);   
  63.                 }   
  64.                 else // 类型错误   
  65.                 {   
  66.                     hr = DISP_E_TYPEMISMATCH;   
  67.                 }   
  68.             }   
  69.             else // 参数个数错误   
  70.             {   
  71.                 hr = DISP_E_BADPARAMCOUNT;   
  72.             }   
  73.         }   
  74.         return hr;   
  75.     }   
  76.     // 要处理的_AgentEvents事件   
  77.     STDMETHODIMP OnHide(_bstr_t CharacterID, short Cause)   
  78.     {   
  79.         PostMessage(g_hDlgMain, WM_CLOSE, 0, 0);   
  80.         return S_OK;   
  81.     }   
  82. private:   
  83.     ULONG m_uRef;   
  84. };  
  1. 我们可以用OLE/COM Object Viewer从agentctl.dll的类型库接口定义之中看到以下的内容:
    C++代码
    1. dispinterface _AgentEvents {   
    2. …  

    这样我们可以很容易猜到,这个接口就是Agent控件开放给我们处理连接点事件用的。这一句用C++的语法来表示,就是:

    C++代码
    1. // 摘自agentctl.tlh   
    2. struct __declspec(uuid("f5be8bd4-7de6-11d0-91fe-00c04fd701a5"))   
    3. _AgentEvents : IDispatch   
    4. {   
    5.     // …  

    也就是说,这个接口继承自IDispatch。IDispatch接口称作“调度”接口,通常用来实现对一些符号解释型语言(如Visual Basic)调用COM接口的支持。关于这个接口的详细情况你可以参考MSDN,里面有非常详尽的介绍。在这里我使接收器亦继承自IDispatch,是因为我只需要处理Hide一个事件,而若将接收器继承自_AgentEvent,那么我必须完全实现_AgentEvent接口的全部方法,这将会是一个非常浩大的工程——即使是将除Hide之外的所有方法都返回E_NOTIMPL。

  2. 我是前说过,可以将接收器理解为一个没有CLSID的COM组件类。因此,接收器必须完全按照COM组件的规格来实现,也就是你所看到的AddRef、Release和QueryInterface的部分。不过,接收器终究有着它自己的特定性,所以我们可以简化QueryInterface,并且可以将IDispatch的GetTypeInfoCount、GetTypeInfo和GetIDsOfNames直接返回E_NOTIMPL。
  3. 现在来到CSink::Invoke的部分。既然COM拥有语言无关的特性,那么就意味着像Visual Basic这样的符号解释型语言也可以处理ActiveX控件的事件。从这一点我们可以猜想到,所有事件都是经由IDispatch::Invoke调度的——事实上这一点从MFC的事件映射表就可以看出来。所以,我们也通过Invoke来捕获ID为7的Hide事件,在检验一切条件都符合后便调用我们自己的处理函数OnHide。
  4. 在CSink::Invoke中有着大量的条件判断,这是为了代码的严谨,从调度ID、参数类型、参数个数等几个方面来确定本次调用无误之后才调用OnHide。也就是说,CSink::Invoke和CSink::OnHide完全可以写成这个样子:
    C++代码
    1. STDMETHODIMP CSink::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)   
    2. {   
    3.     if (7 == dispIdMember)   
    4.     {   
    5.         OnHide();   
    6.     }   
    7.     return S_OK;   
    8. }   
    9. void CSink::OnHide()   
    10. {   
    11.     PostMessage(g_hDlgMain, WM_CLOSE, 0, 0 );   
    12. }  

    看起来的确是简单多了,不过我还是建议你使用前面的方法。

连接点的设置

连接点的使用非常简单,很模式化的代码:

C++代码
  1. // 设置连接点的过程开始   
  2. IConnectionPointContainer *pCPC = NULL;   
  3. // 查询连接点容器   
  4. hr = pAgent->QueryInterface(IID_IConnectionPointContainer, (void **)&pCPC);   
  5. // 查找连接点   
  6. hr = pCPC->FindConnectionPoint(__uuidof(_AgentEvents), &pCP);   
  7. // 这时连接点容器已经没用了,释放之   
  8. pCPC->Release();   
  9. pCPC = NULL;   
  10. // 创建通知对象   
  11. CSink *pSink = new CSink;   
  12. hr = pSink->QueryInterface(IID_IUnknown, (void **)&pSinkUnk);   
  13. // 对连接点进行设置   
  14. hr = pCP->Advise(pSinkUnk, &dwCookie);  

需要注意的是,这段代码必须放在pAgent->put_Connected之后,否则连接点的设置就会失败。另外,这段代码中有几个变量是定义在回调函数头部的static变量,如下:

C++代码
  1. static IConnectionPoint *pCP = NULL;   
  2. static IUnknown *pSinkUnk = NULL;   
  3. static DWORD dwCookie = 0;  

在程序结束的时候,这样释放连接点:

C++代码
  1. pCP->Unadvise(dwCookie);   
  2. pCP->Release();   
  3. pSinkUnk->Release();  

需要注意的是pSinkUnk->Release()一句。由于先前进行了接口查询QueryInterface使得引用计数增加,所以必须在结束使用的时候调用Release。
到现在为止,调用ActiveX的大致过程和原理我已经介绍完了。由于示例工程的代码是一段一段地根据文章逻辑而无序引用的,所以这可能会给诸位带来实现上的麻烦,在此李马给大家赔个不是。

附件:activex.zip

订阅本站

13 Comments

  • At 2006.07.28 09:08, 李马 said:

    To MFCORNOT:
    技术是和产业相关的,如果你需要关注经济利益,那么你别无选择。
    To 小马:
    可参见本文《接受器的实现》一节。

    • At 2007.03.29 11:03, xxx said:

      牛人,拜读~~~

      • At 2011.01.05 11:21, 《用SDK玩转ActiveX》补遗 – 马说 said:

        […] Submitted by 李马 on 2008-03-21 12:00. 已经不止一位朋友向我咨询过这个问题——《用SDK玩转ActiveX》的源代码在VS2003、VS2005下无法正常运行,错误原因是GetDlgItem无法取到ActiveX控件的窗口容器句柄,因而也就无法为CAxWindow赋值。于是在此补遗,请诸位周知。 ATL 3.0中的ActiveX控件处理机制是这样的: […]

        (Required)
        (Required, will not be published)