65.9K
CodeProject 正在变化。 阅读更多。
Home

一个用 C++ 编写的、可以触发 COM 事件的 ATL 组件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.19/5 (10投票s)

2004年6月7日

8分钟阅读

viewsIcon

90112

downloadIcon

2974

一个实现了进程间通信的 COM 组件,并演示了如何向 Visual Basic 等 COM 容器触发事件。

引言

Visual Basic 是一个出色的开发环境,但 VC++ 能够生成依赖项最少或没有依赖项的小型可分发程序,这具有优势。这促使我重写了一些 VB 代码,同时学习了一些 VB/VC/COM 交互的知识。在这里,我将向您展示如何开发一个 COM 组件来实现进程内通信。

背景

不久前,我需要让两个 Visual Basic 应用程序之间进行通信,于是我编写了一个可以包含在 VB 项目中的 IPC 类和模块。然而,我正在转向部署纯 C++ 应用程序,因此需要一个类似的组件,它不依赖于 VB 运行时 DLL,并且我还想学习一些 COM / C++ / VB 交互技术。由于我仍然希望这个组件能与我原来的 VB 应用程序一起工作,所以我选择通过 COM 来实现。这就是最终结果。

使用代码

进程内通信的基础是使用 WM_COPYDATA 窗口消息。这项技术并不新鲜,并且被广泛描述为实现进程内通信的一种方法。该代码为每个 IPC 类实例创建一个顶级的隐藏窗口,并对这个隐藏窗口进行子类化。在接收到 WM_COPYDATA 消息后,数据会从调用中检索出来,在我提供的示例中,它会在客户端应用程序中触发一个事件,告知消息已收到。

在 VB 中,这相对容易实现,只要你理解子类化。对我来说,挑战在于用 C++ 编写一个 COM 组件,它能够处理 COM 中的 BSTR 和事件,并进行接口交互。对于从 VB 过来的开发者,我包含了原始的 VB 源代码,以便您进行交叉引用和学习。

我使用的开发环境是 VC++ 7 和 VB6(用于测试应用程序),主要是因为我还没来得及研究 .NET,并且还没有弄清楚如何在没有 .NET 的情况下编写 VB7 应用程序(如果可能的话)。答案请邮寄给我!我假设您对您喜欢的任何 IDE 都有一定程度的熟悉,并且具备向类添加函数/变量的基本技能。

即便如此,还有几个需要注意的陷阱。

  • 添加由控件公开的方法:诸如 SendData 方法等方法需要公开给客户端,因此必须出现在 IDL 文件中。这是通过右键单击实现类(例如 IIPC)并添加方法来实现的,而不是在类视图中的 CIPC 类上操作。
  • 连接点:在设置连接点之前,必须先编译 IDL 文件。

创建项目

好了,首先快速介绍一下项目是如何启动的。使用正确的向导似乎是成功的一半。我们需要使用新的项目向导创建一个新的 ATL COM 项目。我将其命名为 **InterProcessComms**。在 VC7 中,移除“属性”选项,然后选择“合并代理/存根代码”。在 VC6 中,选择 ATL COM 应用程序向导,同样选择“合并代理/存根代码”。不需要 MFC 或 COM+ 支持。

New ATL Project

New Project in VC6

现在需要添加一个新的 ATL 类。

  • VC7 - 转到“项目”菜单 -> “添加类”,然后选择“ATL 控件”。
  • VC6 - 转到“插入”菜单 -> “新建 ATL 对象...”。从向导中选择“控件”类别,然后选择“完整控件”。
在这两种情况下,都选择一个简短的名称:IPC。然后需要指定该控件使用连接点,并且在运行时是不可见的。我还选择创建一个最小化控件。VC7 的选项如下图所示。

New Atl Control in VC7

选择“完整控件”非常重要,因为这样会启用连接点支持,而我们将要实现事件。

我将不一步一步地讲解代码,而是讨论其中一些更有趣的部分。

SendData 方法

[id(2), helpstring("method SendData")] HRESULT SendData([in] LONG lData, 
[in] BSTR sData, [in, optional, defaultvalue(0)] LONG hWnd);

STDMETHODIMP CIPC::SendData(LONG lData, BSTR sData, LONG hWnd)
{
int c;
HWND hWnd2Send;
COPYDATASTRUCT MyCDS;       // The WM_COPYDATA sturcture
CComBSTR localStr(sData);   // Make a new local BSTR with the passed data
                            // We can now get the lenght etc of this string
    
    // Fill the copydata structure
    MyCDS.dwData = lData;                // arbitatry long data to send
    MyCDS.lpData = localStr.m_str;       // a pointer to the buffer containing 
                                         // data to send
    MyCDS.cbData = localStr.ByteLength()+1;  // the size of the buffer  
                                             // NB: + 1 for the end NULL

    // If a window to send to has been specified, then use this
    // otherwise send to all, except ourself
    if ( hWnd != 0 ) {
        hWnd2Send = (HWND) hWnd;
        ::SendMessage(hWnd2Send, WM_COPYDATA, (WPARAM) 0, 
                                  (LPARAM) (LPVOID) &MyCDS);
    }
    else {
        // Get all the windows we need to send to, this will fill and array
        // of hWnds returns the number of windows found. Note excludes self
        if ( this->EnumerateWindows() >= 1 ) {
            for(c=m_hWndArray.GetSize();c>0;c--) {
                hWnd2Send = m_hWndArray[c-1];
                ::SendMessage(hWnd2Send, WM_COPYDATA, (WPARAM) 0, 
                            (LPARAM) (LPVOID) &MyCDS);
            } // end for
        } // end inner if
    } // end else

    return S_OK;
}

IDL 定义值得解释。基本结构与 C 声明相同,但在方括号中指定了附加属性。Callista 网站提供了一个精彩的解释:Callista 网站。不过,我修改了属性,在末尾包含了一个可选的 hWnd 参数。[in, optional, defaultvalue(0)] LONG hWnd。可选参数必须提供一个默认值。根据 MS 文档,[in, optional, defaultvalue("Hello World")] BSTR hWnd 在你想用字符串做同样的事情时是完全有效的。

COM 和 VB 使用 BSTR 数据类型来表示字符串。它是一个指向以 dword 长度信息为前缀的 null 终止字符串缓冲区的指针。ATL 提供了一个名为 CComBSTR 的类,它对我来说最实用。它避免了 MFC,并且拥有大多数用于简单字符串操作的函数。我们在这里使用它来获取长度和可以打包到 COPYDATA 结构中的 char * 缓冲区。

顺带一提,我还没有找到一个好的资源来明确说明如何在 C++ 中使用 COM 操作字符串并返回它,以便可以编写供 VB 使用的函数。这里有一些代码可以展示如何做到这一点。

// This appears in the IDL file
HRESULT MessWithString([in] BSTR stringIn, [out, retval] BSTR* stringOut);

// This is the function implementation
STDMETHODIMP CIPC::MessWithString(BSTR stringIn, BSTR* stringOut)
{
    // ATL macro needed to use converstion macros!
    USES_CONVERSION;

    // make a local copy using the CComBSTR class
    CComBSTR localStr(stringIn);

    // Reverse the string, as an example. Must convert to a char * for _strrev
    localStr = _strrev( COLE2T(localStr.m_str) );

    // Now to return the string, we need to use SysAllocString to allocate
    // system memory and pass that back to the com container
    *stringOut = ::SysAllocString(localStr.m_str);

    // You must still return with a S_OK from the actual function as this is
    // a COM call. The client will get stringOut as a return value as specified
    // in the IDL
    return S_OK;
}

查找我们的隐藏窗口

接下来要谈的是 SendData 中调用的 EnumerateWindows() 函数。在 C++ 中,这会带来一个问题。您知道,Windows API 调用 EnumWindows 需要传递一个指向函数的指针,该函数会在找到每个窗口时被调用。但是,C++ 类的成员不能用作回调函数,因为它们不是按声明的方式调用的,而是将 *this 指针预先压入堆栈。所以

MemberFunction(int x)
变成
MemberFunction(this, x)
另一方面,静态成员函数是按声明的方式调用的,但它们没有 *this 指针的访问权限,因此无法访问任何成员函数或变量。这两个文章很好地解决了这个问题,第二篇文章提供了一个很好的头文件,它封装了需要回调的 Windows API 调用。我已经为 EnumWindows 函数使用了这个头文件。
  1. Windows 过程作为类成员函数
  2. 将 C 风格的回调和线程用作成员函数 - 一个通用解决方案

WndProc 函数

实现子类化所需的 WndProc 函数与上面描述的遇到了相同的问题。我们仍然想知道 *this 是什么,以便我们可以访问成员变量和方法。上面使用包装器的解决方案既复杂又冗长,对我来说,可以使用一个更简单的方法来调用 WndProc 方法。

您可能已经看到,可以使用静态成员函数作为回调。但是它没有 *this 指针,因此无法访问成员变量/函数。在此应用程序中,每个类实例都有一个窗口。窗口有一个非常有用的属性叫做 USERDATA。它是一种可以存储任意数据的长整型数据类型,并且始终初始化为零。通过将其设置为 *this 的值,当一个消息触发并调用静态成员函数时,我们可以让静态函数提取此信息,并将一个新的指针强制转换为与该窗口关联的对象。我认为最好用图来解释这一点。
WndProc Flow Diagram

请记住,这只有在我们每个类实例有 **一个** 窗口时才有效。此外,对于此应用程序,我们只关心接收 WM_COPYDATA 消息的窗口。这意味着我们不介意在等待 CreateWindowEx 返回时丢失初始创建消息,例如 WM_CREATE。Class Member Function 文章涵盖了这种情况,如果您需要实现它。

事件触发函数

COM 中的事件通过称为连接点(connection points)的东西来实现。如果您按照文章顶部的项目启动说明进行操作,您的类视图中应该有一个 CIPC 和 IIPC 的根。此外,还有一个 _IIPCEvents。在 VC6 中,它出现在类视图的根目录;在 VC7 中,它位于 InterProcessCommsLib 下。通过右键单击 _IIPCEvents,您可以添加将成为最终组件中事件的方法。添加完所有方法后,**必须编译 IDL 文件**。这是许多挫败感的根源。如果不编译 IDL,就没有类型库,连接点向导也会失败。

IDL 编译后,右键单击类(在本例中为 CIPC),然后单击“实现连接点”。选择 _IIPCEvents 并完成。这会创建一些新文件和一个名为 Fire_[eventname] 的函数,可以调用该函数来触发客户端应用程序中的事件。很简单,不是吗!

测试应用程序

已包含一个测试应用程序。要使用该 exe,您必须安装 VB6 运行时,并且还需要使用 regsvr32.exe InterProcessComms.dll 在解压测试应用程序的目录中注册 InterProcessComms.dll 文件。另外请记住,要打开应用程序的多个实例,因为它默认情况下不向自身发送消息。

进一步工作

代码可以工作,但读者还有一些可以进一步扩展该项目的方法。目前,消息仅限于 BSTRs 和 long。该控件可以扩展到支持其他数据类型。有一篇很棒的文章描述了 跨 DCOM 传输 C++ 对象,这可能会有所帮助。此外,缓存隐藏窗口句柄而不是每次发送消息时都查找它们可能是一个好主意。可能还可以引入一些组件到组件的 IPC 来处理新实例并缓存隐藏窗口。

最后评论

这些信息很多都可以从文档和网络上找到,但似乎非常零散,而且实际应用案例不多。我希望将其中一些信息整合到一个有用的、实际的应用中。我使用的大部分有用参考都包含在文本中,如果我遗漏了任何内容,我深表歉意,如果您给我发送电子邮件,我会添加。这是我在这里的第一篇文章,所以无论是好是坏,都欢迎您的反馈。

© . All rights reserved.