在 .NET 应用程序中嵌入 VB6 窗体






4.78/5 (15投票s)
2007年6月27日
7分钟阅读

179637

5736
本文介绍如何在 .NET MDI 窗体中打开 VB6 MDI 子窗体。
引言
通常,提供 .NET 代码和 VB6 代码之间互操作性的可用工具基于代理、包装器或其他中间件,允许 VB6 应用程序调用 .NET Winforms。到目前为止,我们还没有看到允许我们反向操作的解决方案,即在 .NET 应用程序中嵌入 VB6 窗体。因此,我们决定自己创建解决方案。在 MasterSoft,我们有许多用 Visual Basic 6 开发的应用程序,我们投入了数千小时的开发时间。现在,我们面临着以渐进方式并以最少精力将它们迁移到 .NET 的必要性。
为了实现这一目标,我们设想了一种 VB6 和 .NET 之间的互操作性,它允许我们在 .NET 应用程序中打开 VB6 窗体。这将使我们有机会逐步迁移应用程序,用“原生” .NET 代码添加新功能,并保持 VB6 中编写的所有内容不变(或尽可能不变)。这种策略的主要优点在于利用我们自己开发的 .NET 框架,该框架为菜单设计和安全性管理提供了强大的工具,以及许多其他功能。如果我们继续将应用程序的核心保留在 VB6 中,所有这些好处都将丢失。
我们采用的解决方案基于 .NET MDI 环境,从中我们可以打开包含在 DLL 中的 VB6 窗体,就像它们是 .NET 窗体的 MDI 子窗体一样。我们没有使用 .NET-VB6 互操作的工具包或 Power Packs,而是使用了 .NET 容器窗体的实例,并在其中嵌入了 VB6 窗体。通过使用 API 函数,模拟了嵌入式窗体的正常行为。
实现细节
为了从 .NET 应用程序中使用 VB6 窗体,我们选择在 VB.NET 中开发一个具有 MDI 子功能窗体的“外壳”(容器)。这通过 DLL ActiveX 中的特定类 `ClsForms` 来创建 VB6 窗体的实例。使用 API 函数并与 DLL 通信,VB6 窗体被实例化并嵌入到容器中。
在 VB6 DLL 中,有一个公共类负责实例化窗体并与 .NET 应用程序通信。这个类充当 DLL 中已实例化窗体的控制器。它通过几个方法提供对它们的访问,这些方法帮助容器的行为就像 VB6 窗体在 VB6 MDI 容器中的行为一样。由于 VB6 窗体位于 DLL ActiveX 中,我们需要确保它们的 `MDIChild` 属性设置为 False,因为我们无法弄清楚如何在运行时更改该属性。为了管理 VB6 窗体的事件,.NET 容器使用了一个 Timer 控件,该控件不断查询 VB6 类以获取要处理的消息。
我们需要解决的一个大问题是 VB6 窗体之间的通信。在许多情况下,我们的应用程序窗体会相互调用,通过属性传递参数和结果(例如,搜索结果)。为了在 .NET 应用程序中保持 VB6 窗体的这种行为,有必要让 VB6 窗体有机会创建容器的新实例并在其中嵌入其他 VB6 窗体。如下文所述,已实现了这一点。
显示序列
为了使事物更具封装性,我们在 .NET 项目中开发了一个 `FormController` 类,该类维护 DLL 的实例并创建 .NET 容器的多个实例。因此,我们可以使用 `ClsForms` 的同一实例打开多个窗体。
'We instantiate FormController by passing
'the MDI form and the Dll ActiveX name
Dim objFormController As New FormController(objMDIForm, "FormsVB6")
'then we just call the OpenForm function passing the VB6 form name
objFormController.OpenForm("frmChildForm")
展示对象之间交互的最佳方式是使用此 UML 序列图。
`FormController` 中的 `OpenForm` 函数创建 ActiveX DLL 中 `ClsForms` 类的实例(该类包含所有 VB6 窗体),然后实例化 `frmContainer` 窗体。最后,它调用其 `OpenForm` 方法。容器中的 `OpenForm` 方法依次调用 `ClsForms` 中的 `OpenForm` 方法。它这样做是为了创建窗体名称的新实例,该名称通过隐藏的 VB6 `Forms.Add("formname")` 指令作为参数传递。
当窗体实例被创建后,`OpenForm` 方法将实例的句柄返回给容器。通过该句柄,以及通过 API 函数和其他 `ClsForms` 类的方法,VB6 窗体被“绘制”在 .NET 容器内部。`OpenForm` 容器获取 VB6 窗体的属性值,如 `Title`、`Width`、`Height` 等,以便在容器中复制它们,即上面图中的 `GetWindowTitle`。容器窗体还负责检查其嵌入式窗体的状态。这样,它就知道 VB6 窗体是否已被 `Unload Me` 指令关闭,从而可以关闭容器。
窗体行为
为了模仿 VB6 窗体事件的触发,容器使用 `Timer` 控件不断检查 VB6 窗体中是否有需要处理的事件。我们可以选择直接将事件从 VB6 窗体传递给 .NET 应用程序,但这将意味着对这些窗体代码进行更深入的修改,而这正是我们想要避免的。
为了使 VB6 窗体能够打开“子”非模态窗体并保持其 `MDIChild` 状态,有必要在 VB6 中使用 `ShowForm` 函数而不是通常的 `.Show` 方法。`ShowForm` 函数执行了所有必要的过程,以将调用的子窗体保留在 .NET 环境的容器内。这基本上是嵌入 VB6 窗体到 .NET 容器中所需的所有工作。
'Opens a new child form
ShowForm(frmChildForm, True)
'Opens a new modal form
ShowForm(frmModalForm, False)
因此,我们只需要将代码中的 `Show` 调用替换为 `ShowForm()`,传递要显示的窗体和一个第二个布尔参数,指示它是要成为 MDI 子窗体还是非 MDI 窗体。如果是 MDI 子窗体,它会调用容器中的 `OpenForm`,并向其传递“OpenForm”消息。如果不是,它将窗体作为常规的模态窗体打开。
为了保持 VB6 窗体及其各自容器作为 MDI 子窗体的正常行为(例如,Ctrl-Tab 的正常使用、窗体之间的焦点切换,或者 .NET MDI 窗体保持激活状态以打开其他 .NET 或 VB6 窗体),我们需要使用各种 API 函数,如 `SendMessage`、`SetForegroundWindow` 和 `LockWindow`。
为了增强此技术,我们计划向位于 .NET MDI 窗体和容器之间的中间类添加代码。这将允许从 VB6 触发影响 .NET 环境的事件。例如,修改 MDI 窗体的 `Caption` 属性,打开 .NET“原生”窗体等。
参考文献
我们使用了 Stephen Kent 的一个函数来在运行时更改窗体边框样式。
使用代码
代码非常简单,并且在我认为必要的地方进行了注释。我们没有使用高级编程技术,只用了几个 API 调用来完成“魔术”。
结论
这种 .NET 和 VB6 之间的互操作方式使我们能够为应用程序启动一个渐进式的 .NET 迁移路径,而无需立即修改它们。它还立即为应用程序添加了我们开发的 .NET 框架的许多好处。作为一项附加功能,这种渐进式迁移路径允许最终用户逐步学习如何使用应用程序的 .NET 版本提供的所有新功能,而无需一次性学习所有更改。
特别感谢 MasterSoft 的 Gustavo Du Mortier 和 Mariano Aranda;他们提供了极大的帮助和协作。
历史
- 2007 年 6 月 27 日 -- 发布原文
- 2007 年 7 月 12 日 -- 文章已编辑并移至 CodeProject.com 主文章库