VC++ 和 VB 进程间通信以及 VB 子类化介绍






4.57/5 (16投票s)
2005 年 11 月 9 日
14分钟阅读

96813

1726
VC++ DLL 与 VB 6 应用程序之间(但不仅限于此)的通信方法。
引言
本教程将引导您了解非 DDE 进程间通信的机制。进程间通信可能对您应用程序的未来至关重要。我选择本文来实现在两种截然不同(且流行)的编程语言之间进行通信:Visual C++(具体来说是 6.0 版本)和 Visual Basic(同样是 6.0 版本)。然而,本文的内容在 Visual C++ .NET 和 Visual Basic .NET 中也可能无需修改或只需稍作修改即可工作。
我们不仅可以将进程间通信用于两个应用程序之间,还可以用于应用程序和 DLL 之间,而这正是本文的主题。更重要的是,当您需要链接两个应用程序时,本文也很有用。由于我们将使用 API 和回调函数,读者将不会花费太多精力来理解如何在两个应用程序、两个 DLL 或更复杂的组合之间实现通信。
请注意,即使您已经知道如何在一个或多个进程之间进行通信,本文仍然可以帮助您更好地理解这个概念,并可能为您带来新的想法和技巧。初学者或对该主题不太熟悉的程序员肯定会在这里找到很多有趣的内容。
通用目的
我个人在进程间通信方面的困扰源于需要结合 Visual Basic 的 RAD(快速应用程序开发)能力和 Visual C++ 的效率。众所周知,Visual Basic 6 无法构建真正的 DLL(只能构建 COM)。不幸的是,当我开始一个大型项目时,对此并不知情,直到为时已晚,无法将其移植到 Visual C++(我也会失去 VB 处理 ActiveX 的 RAD 和便捷性)。进程间通信将我从一个危险的境地中解救出来,迫使我花费数月时间移植到 Visual C++,或者放弃我项目中最重要的一项功能!
这种通信方式非常适合:通过使用 Windows API 和 DLL 导出的函数,结合两种甚至多种编程语言的优势。就我而言,例如,利用 Visual Basic 的 RAD 和 Visual C++ 的强大功能,以达到最佳效果。
文章代码及使用方法
代码包括一个 Visual Basic 6 测试应用程序和一个 Visual C++ 6 测试 DLL。只需将 Zip 存档下载并解压到方便的位置,然后运行提供的 Visual Basic 可执行文件即可。测试应用程序和 DLL 的代码都经过了非常详尽的注释!我这样写是为了让任何人都能理解。
Zip 存档内容
- Visual Basic 6 测试应用程序和 Visual C++ 6 测试 DLL 的完整源代码和演示项目。
- 图表(可通过 *TestVBApp.exe* 直接显示),以帮助读者更快更好地理解代码和涉及的进程,此外,还有一篇格式精美的 Word 文档。
覆盖率
本文仅专注于 Visual Basic 应用程序和 Visual C++ DLL,因为这种情况涵盖了进程间通信的所有必需领域,而又不会过于复杂。如果您的个人项目结构不同,此处详细介绍的 API 和技术仍然会很有帮助。如果您只使用 Visual Basic,事情会更简单,因为 Visual Basic 允许轻松连接组件。本文将只处理更棘手的情况,您可能认为“困难”的事情。
对于像控件注入这样的编程技术新手来说,本文也极具教育意义,因为我将在 Visual Basic 中使用控件注入来实现一些我试图将其与 Visual C++ 工作联系起来的深夜苦思冥想时发现的小技巧。
建议先决条件
- 在您的计算机上安装了 Microsoft Visual C++ 6 或更高版本以及 Microsoft Visual Basic 6 或更高版本。
- 中级 Visual C++ 水平。
- 中级 Visual Basic 6 水平。
- Windows 操作系统(应可在任何版本上运行)。
警告
尽管我多次检查了我的代码、注释和文章,但我不是计算机编程的专家。不要指望我的代码是完美的。注释中可能也存在一些错误。但是,请相信我,我已经尽了我最大的努力,为您提供一篇编写良好、代码优良且错误尽量少的文章,尽可能地利用我当时的能力。请原谅我可能犯下的任何愚蠢错误,如果您有时间,请给我发一封电子邮件(axonnus at yahoo dot com),提供您可能有的任何意见或建议。我们都在一起互相学习,这正是我所尝试做的。我喜欢建设性的批评!
感谢...
在正式开始工作之前,我想感谢整个互联网社区在我作为一名计算机程序员的成长过程中给予我的巨大帮助。还要感谢 Nicholas Skapura 的一篇文章,它为我提供了相当多的关于 VB 和 VC++ 之间通信的技朧见解,以及 Randor 修复了 C++-VB 回调数组问题。当然,我最深切的“感谢”信息献给 Code Project 论坛上提供帮助的人们,特别是那些回答我问题的人们:)。他们无数次地支持我,几乎总是给我提供好主意并帮助我解决问题。同样的信息也适用于互联网上的其他论坛和那些牺牲自己业余时间、敲打键盘为他人贡献的人们。你们都赢得了我的尊敬,我希望我能通过我的文章和软件,贡献一点知识给每个人。感谢大家。
为什么选择 VB?!
好问题。尽管 Pascal 是我的第一门编程语言,但我花费在 VB 上的时间远比其他语言多(尽管 C++ 也在快速赶上)。很多人低估了 VB。这可能是因为他们认为它“不够精英”或者根本不知道如何使用它。事实上,VB 确实无法做 **一些** 事情,但它有其他优势。巨大的优势。例如,处理 ActiveX 或开发用户界面。VB 意味着快速应用程序开发,它可以让您的生活更轻松。
示例架构
示例的布局相当简单。使用 Visual Basic 应用程序来处理 VC++ DLL。VB 应用程序调用 VC++ DLL 中的各种方法,DLL 则做出相应的响应。
诸如 `Activate` 和 `Demonstrate` 之类的函数将不期望从 DLL 获得任何结果。`Demonstrate` 将实际触发 DLL 执行对 VB 函数的特定回调或其他类型的演示。
Private Sub cmdStringDemo_Click()
Demonstrate 1
'Calling the Demonstrate function from
'the VC++ DLL with 2 as parameter. 2 indicates that
'the string demo has to be started.
End Sub
VC++ DLL 中的 `Demonstrate` 函数将根据传递给它的整数参数启动特定的演示。
//The VB Application will call this function //to trigger the DLL to begin a particular demonstration. void WINAPI Demonstrate (int WhatToDemonstrate) { switch (WhatToDemonstrate) //Depending on what demonstration has been commanded. { case 1: //Demo 1 = Demonstrate calling a string function from VB. { //Calling a VB function that accepts a string as parameter. CallVBFunctionForString("Greetings from VC++!"); break; } ... } }
诸如 `GetDLLData`、`VCNumericArray` 或 `VCStringArray` 之类的函数将使用简单变量、数组或结构与 DLL 进行交互。
子类化
示例的重要部分依赖于控件注入。控件注入用于完全控制 VB 应用程序从操作系统接收的消息。VC++ DLL 将使用 `WM_SETTEXT` 和 `WM_USER` + 241(241 是我选择的一个任意值)来向 VB 发送字符串数据。或者使用 `WM_COPYDATA` 发送数字、字符串或字符串数组到 VB。
在 Visual Basic 中,只要您声明了正确的 API 函数和常量,控件注入就很容易实现。控件注入是一个非常强大的工具,它允许程序员对 Visual Basic 应用程序进行广泛的控制。没有这样的工具,就无法捕获和响应我刚刚提到的那些消息。
示例的这一特定区域通过代码注释得到了很好的解释,读者应该可以轻松理解。
嘿,C++,你在哪儿?
几乎所有 VB 程序员都应该知道如何声明 API 函数。如果您不知道,没关系,现在您就知道了。那么,C++ DLL 中的函数呢?首先,DLL 必须导出该函数。我使用了一个 *.DEF* 文件,其中包含了要导出的函数,如下所示
LIBRARY TestVCDLL DESCRIPTION "Test DLL used for demonstration " "of linking Visual C++ with Visual Basic." EXPORTS Activate @1 Demonstrate VCNumber @2 VCStructure @3 GetDLLData VCNumericArray @4 VCStringArray @5 SECTIONS DLLShare READ WRITE SHARED
这几乎是所有导出函数的列表。某些函数名后的 `@X` 表示它们需要参数。当然,您也必须在 CPP 中正确声明函数。我使用了 `WINAPI` 标签,如下所示
void WINAPI VCNumber (long ValueFromVB) { ... }
至于 VB 端,这里是如何声明我们 DLL 中的一个函数(幸运的是,这与 API 函数相同)
'A VC++ DLL function that accepts a long parameter
'as value and will also call a function from this VB Application.
Public Declare Sub VCNumber Lib ".\Debug\TestVCDLL.dll" (ByVal Value As Long)
很简单吧?当然,`TestVCDLL` 必须位于您运行 Visual Basic 测试应用程序的文件夹的 *Debug* 子文件夹中。
触及 VB
从 VB 到 VC++ 的通信并非难事。然而,反之则稍微棘手一些。首先,我们的 VB 不是 DLL,而是应用程序。但它是一个正在运行的应用程序。因此,它的任何函数在计算机内存中都有一个地址。为了让 DLL 调用 VB 中的一个函数,它必须首先知道该函数的地址。VB 将通过 `Activate` 过程将一些函数地址发送给 DLL。
Activate AddressOf NumberDemoFunction, _
AddressOf StringDemoFunction, _
AddressOf StructureDemoFunction, _
AddressOf ArrayDemoFunction
回到 DLL 中,`Activate` 过程将保存这些地址,DLL 将在需要时使用它们来调用 VB 中的函数。
现在我们可以随时调用 DLL 中的函数,也可以随时从 DLL 中的函数调用 EXE 中的函数。接下来,这些函数必须执行一些操作。由于我不需要关心您的应用程序或 DLL 将做什么,所以我不需要关心例程中的复杂函数或任何此类东西。我只关心如何尽快可靠地将数据从一个地方传输到另一个地方。有很多方法可以做到这一点,我在示例中探索了几种。
按引用传递
通过这种方式,VB 可以调用 DLL 中的一个函数,并按引用传递一些参数。DLL 可以修改参数,然后 VB 可以访问新值。这是一种标准的做法,不需要其他额外的注释。这是 VB 调用某些 DLL 函数的代码
Private Sub cmdGetDataDemo_Click()
Dim cmStatus As DLLStatusStruct
'A variable of our own custom type, DLLStatusStruct.
...
GetDLLData cmStatus
'Passing cmStatus by reference
'(it was declared using ByRef in the module).
...
End Sub
以及 DLL 函数本身
//Puts some data in a custom structure. void WINAPI GetDLLData (DLLStatusStruct *Response) { Response->lFirst = extNumberProc; ... }
VC++ DLL 修改了结构体的某些成员,当执行返回到 VB 时,测试应用程序将可以访问新值。
我现在将专注于我在示例中涵盖的更棘手的方法。
VC++ 到 VB 的调用
这是我如何从 DLL 向 VB 发送数字(此调用独立于 VB 应用程序的内部条件执行。它可以是 DLL 内部计时器的结果,或其他任何 DLL 内部条件。在示例中,是 `Demonstrate` 过程在发号施令,而它又被 VB 调用,但这并不意味着 DLL 无法在任何其他给定时刻调用 VB 函数)。
//Using the extNumberProc variable, //which is a pointer to the VB function for Numbers, calling //that function and passing to it the value received as parameter. void CallVBFunctionForNumber (long lSomeValueToSend) { //Defining the prototype of the function. typedef void (__stdcall *OutsideFunction)(long AValue); //Creating an instance that will be used to call the function. OutsideFunction FunctionCall; //Assigning the address to be used for the call. FunctionCall = (OutsideFunction)extNumberProc; //Calling the function with the parameter. FunctionCall(lSomeValueToSend); }
注释基本解释了这一点。我们定义了一个 `__stdcall` 调用约定函数原型,然后用它来声明函数实例,然后将函数实例与其地址(通过 `Activate` 函数从 VB 接收)关联起来。之后,就可以调用 VB 中的函数了。
在这种情况下,在 VB 中,它实际上不是一个函数,而是一个过程,一个 `Sub`。它不返回任何值;因此,我们函数的返回类型是 `void`。但是,如果您想返回另一个数字,只需将其更改为 `long`,并将 VB 中的 `Sub` 更改为一个返回 `Long` 的函数。
如果您想返回一个字符串,请注意,VB 使用 `BSTR` 作为返回类型。代码对此进行了仔细解释,并提供了处理此问题的注释和技巧。
VC++ DLL 提供了许多此类调用的示例。甚至数组也可以发送。查看 `CallVBFunctionForArray`,它可以向 VB 发送数字数组或字符串数组。由于它使用了 `LPSAFEARRAY` 数据类型,因此有点棘手,但它确实有效,一旦理解了,就很容易利用。
利用控件注入的力量
现在这里有一个您不常遇到的技巧(或者您会遇到吗……)。
假设您不想费力地从 VB 向 VC++ 发送函数地址。假设您不想经历所有 `__stdcall` 转换等内容。或者,您可能希望进行异步调用。以下是一些可以做到这一切,甚至更多,可以简化您的生活的方法。看看这段小代码
SendMessage(hwndExternalApplication, WM_USER + 241, NULL, (LPARAM) "Addresses of VB functions saved." " Ready for demonstration. (Sent via WM_USER + 241)");
这就是将该字符串(转换为 `LPARAM` 的那个)发送到 VB 所需的全部内容。当然,也有一个陷阱。大部分代码在 VB 中。看看示例中 VB 应用程序的模块,在 `MsgProc` 函数(负责控件注入的函数)中。然而,那里的注释证明这实际上并不困难。此外,这样发送数据可以异步进行。虽然回调是同步的,但您可以使用 `PostMessage` 来异步完成任务,而无需担心多线程或其他类似问题。
还有更多。这并不是您可以使用控件注入和消息发送做的唯一事情。这里还有一个不错的小技巧。
WM_COPYDATA 的世界
这个精巧的消息允许您从 VC++ 发送几乎任何东西到 VB。我用它来发送数字、字符串、数字数组甚至字符串数组。在 C++ 中,事情很简单;同样,就像使用 `WM_USER` + 241 一样,您不需要了解您正在发送数据的应用程序的任何信息,除了它的窗口句柄。您只关心发送消息;之后,外部应用程序(在本例中是 VB 测试应用程序)将处理其余的事情。
`WM_COPYDATA` 通过使用 `COPYDATASTRUCT` 结构来实现。在发送消息本身之前,您需要适当地设置这些成员。这是我发送字符串到 VB 的方法
COPYDATASTRUCT cdsData; //The structure used to send data. //SENDING A STRING VIA WM_COPYDATA. char cTemp[50]; //A temporary character. BSTR sString; //String to send to VB. //The string to send. strcpy(cTemp, "This is the string passed via WM_COPYDATA.\0"); //Creating VB compatible string. sString = SysAllocString(ConvertCharToBSTR(cTemp)); //Setting an arbitrary value which //will tell VB we're sending a string. cdsData.dwData = 1000; //Wide characters = lenght times two. cdsData.cbData = strlen(cTemp)*2; //Setting the string to be sent. cdsData.lpData = sString; //Sending the data. SendMessage(hwndExternalApplication, WM_COPYDATA, (WPARAM)hwndExternalApplication, (LPARAM)&cdsData); SysFreeString(sString); //Freeing memory used by SysAlloc.
再次,我强烈建议您查看我示例的 VB 代码以获取如何处理 VB 端信息的完整详细信息。它解释得很清楚,我不想在文章中充斥无用的信息,因为您可以很容易地从那里阅读到。
由于 `WM_COPYDATA` 直接使用内存结构,在 VB 中,您需要知道要解释哪种结构。我使用 `dwData` 成员来告诉 VB 它应该在 `lpData` 成员中查找哪种结构。我认为这相当不错。如前所述,通过这种方式通信消除了向 VC++ 发送内部函数地址并通过 `__stdcall` 转换或类似方式调用它们的需要。
使用相同的 `WM_COPYDATA`,我也成功地发送了数字或字符串数组,没有任何问题。我发现的一个很酷的事情是,VB 中的数组可以随时由 VC++ 应用程序通过发送 `WM_COPYDATA` 消息来修改。由于每当 VB 应用程序接收到消息时,`MsgProc` 控件注入过程就会执行,因此如果您发送 `WM_COPYDATA` 消息来更新数组,这将在 VB 无需任何其他干预的情况下完成。因此,您可以实际上从 VC++ DLL 同步或异步更新 VB 端变量。如果您有一个 DLL 作为 VB 应用程序的引擎,并且引擎不断更新 VB 应用程序中的内容,那么这可能会非常有用。引擎将更新所有需要更新的数据结构,一旦控件注入到位,您就不需要在 VB 中移动一根手指。
结论
进程间通信还有其他方法。您可以考虑 DDE、套接字、文件或其他任何您认为更适合您的方法。我在这里涵盖了几种更容易也可能更快被利用的方法。此外,这些也是我个人在我迄今为止最大的项目中使用过的方法,这是一个免费应用程序,在我发布本文第一个版本时尚未发布。
使用 `WM_USER` 或 `WM_COPYDATA` 等消息发送数据提供了一种快速简单的目标方法。它还提供了异步数据传输的可能性,这对于许多应用程序可能很有用。
我希望您享受这篇文章的程度至少和我为您撰写它一样。这是我的第一篇文章,也是我第一次公开为互联网程序员社区做出贡献。它花了我很长时间才完成,因为我工作和其他项目非常忙碌,但最终,它现在在这里了……
历史
- 2005-11-10 (星期四, 11月 10日) – 版本 1.1。文章已更新。示例已更新。修复了 VC++ 到 VB 的数组问题(感谢 Randor!)。
- 2005-11-06 (星期日, 11月 6日) – 版本 1.0。文章和示例。