1、 虽然 IT 开发技术日新月异,不过业界仍然运行着大量的 VB 系统,这些系统凝聚了不少客户的投资,应当要一定程度的保护和利用。因此也就产生了一种 需求,也就是使用旧的开发技术仍然可以使用新技术的产出。本文就讨论如何在 VB6.0 开发中使用上 WinForm.NET 控件。袁永福版权所有二软件原理:运行 VB IDE,打开或创建一个 EXE 工程,打开窗体设计器,如下图所示:为了能在窗体上添加控件,需要往窗体左边的工具箱上添加项目,需要点击菜单项目“Project-Components”,此时会弹出如下图所示的对话框: 点击“Browse”按钮,弹出文件选择对话框,这个对话框中优先选择 OC
2、X文件,而 C#编译结果绝不可能是 OCX 文件的,此时即使选择一 个.NET 程序集DLL 文件,无论如何必然会报错“This file not registerable as an ActiveX Component”。袁永福版权所有因此也就是说,使用 C#开发的 WinForm.NET 控件是不可能直接通过传统的模式放置在 VB 窗体上。不过 VB 仍然可以通过 COM 方式调用.NET 程序集中的对 COM 公开的类型。此时就可以想出一种曲线实现方式,那就是 VB 创建 C#组件,该组件 是一个WinForm.NET 控件,然后调用 Win32API SetParent 函数,将 Wi
3、nForm.NET 控件硬塞入 VB 窗体中。这样在用户界面上,用户能看到和使用 WinForm.NET 控件;在后台,VB 代码能访问.NET 组件提供的公开的属性、方法和事件,实现了 VB全方位的调用 WinForm.NET 控件。 三C#开发C#控件开发根据上述的软件原理,笔者开发一个 WinForm.NET 控件并成功的应用于VB6.0 的开发中,现对软件进行说明。这个 WinForm.NET 控件名为 MyWinFormControl,派生自 System.Windows.Forms.UserControl 类型,它包含在一个名为DCWinFormControlLib 的 C#项目中
4、,项目输 出类型为类库,目标框架为.NET2.0,添加了对 System.Windows.Forms.dll 的引用。界面设计:MyWinFormControl 控件的用户界面设计如下: 在界面上放置一个名为“btnAction”的按钮,一个名为“myTextBox”的文本框。定义公开属性和方法:打开该控件的 C#代码文件,可以看到声明该类型的 C#代码如下:System.Runtime.InteropServices.ComVisible(true)System.Runtime.InteropServices.Guid(“60550064-C97F-4306-A8B2-6908F50780E
5、3“)System.Runtime.InteropServices.ComSourceInterfaces(typeof(IComMyEvent)public partial class MyWinFormControl : UserControl这段代码中,第一行代码的 ComVisible 标记类型为 COM 公开的;第二行代码 Guid 标记了类型在 COM 中的唯一编号;第三行代码的 ComSourceInterfaces指明该类型实现了名为 IComMyEvent 的事件接口。袁永福版权所有VB 中无法直接绑定编译阶段未知的控件事件,同时也无法直接感应 C#中的事件,为此需要编写一个
6、接口通知 VB 存在若干事件,使得 VB 能绑定事件。因此在此定义了 IComMyEvent 接口,声明了 C#控件中的事件,IComMyEvent 接口定义如下using System.Runtime.InteropServices;Guid(“096EF9A6-24CB-4091-A18F-34DA38C9A6F1“)ComVisible( true )InterfaceType( ComInterfaceType.InterfaceIsIDispatch )public interface IComMyEvent/ / 按钮按下事件/ DispId(12340)void ComButto
7、nClick();/ / 文本内容修改事件 t/ DispId(12350)void ComTextChanged();/ / 无参数无返回值委托类型/ public delegate void VoidEventHandler();而后在控件的 C#代码中添加以下代码:#region 实现 IComMyEvent 中的成员/ / 按钮按下事件/ public event VoidEventHandler ComButtonClick = null;/ / 文本内容修改事件/ public event VoidEventHandler ComTextChanged = null;#endreg
8、ion这样 C#中定义的事件在 VB 中就能绑定了,以下代码就是触发这些事件的:private void btnAction_Click(object sender, EventArgs e)if (ComButtonClick != null)/ 触发 ComButtonClick 事件ComButtonClick();private void myTextBox_TextChanged(object sender, EventArgs e)if (ComTextChanged != null)/ 触发 ComTextChanged 事件ComTextChanged();对控件实现了 COM
9、 公开的事件后,就可以编写 COM 公开的属性和方法,其代码如下:/ / 公开的属性/ public string UserTextgetreturn myTextBox.Text;setmyTextBox.Text = value;/ / 公开的方法/ public double Calcute(double p)return Math.Sin(p);这个用户控件虽然能在 VB 代码中创建和访问,但还不能直接拖放到 VB 窗体上,此时还需要使用代码将 C#控件添加到 VB 窗体上:/ / 将控件添加到指定句柄的窗体中/ / 指定的窗体句柄对象/ 操作是否成功public bool Appen
10、dToContainerControl(int containerHandle)CrossPlatformControlHostManager man = new CrossPlatformControlHostManager();man.ContainerHandle = new IntPtr(containerHandle);man.ControlHandle = this.Handle;man.Dock = this.Dock;return man.UpdateLayout();在这个函数中,参数为 VB 窗体中某个控件的句柄,该控件用于承载 C#控件。这段代码使用了笔者编写的一个 Cr
11、ossPlatformControlHostManager 类型,该类型专业用于执行跨应用程序的控件承载,实现“乾坤大挪移”,该类型首先定义了 几个属性:袁永福版权所有private IntPtr _ControlHandle = IntPtr.Zero;/ / 操作的控件句柄对象/ public IntPtr ControlHandleget return _ControlHandle; set _ControlHandle = value; private IntPtr _ContainerHandle = IntPtr.Zero;/ / 容器元素对象/ public IntPtr Con
12、tainerHandleget return _ContainerHandle; set _ContainerHandle = value; private DockStyle _Dock = DockStyle.Fill;/ / 停靠样式/ public DockStyle Dockget return _Dock; set _Dock = value; 此外还定义了一个方法,其代码如下:/ / 更新排版/ / 操作是否成功public bool UpdateLayout()WindowInformation info = new WindowInformation(this.Control
13、Handle);WindowInformation container = new WindowInformation(this.ContainerHandle);if (info.CheckHandle() = false| container.CheckHandle() = false)return false;if (info.ParentHandle != container.Handle)if (info.SetParent(container.Handle) = false)return false;Rectangle clientRect = container.ClientBo
14、unds;Rectangle bounds = info.Bounds;Rectangle descBounds = bounds;switch (this.Dock)case DockStyle.Fill:descBounds = clientRect;break;case DockStyle.Bottom:descBounds = new Rectangle(0,clientRect.Height - bounds.Height, clientRect.Width,bounds.Height);break;case DockStyle.Left:descBounds = new Recta
15、ngle(0,0,bounds.Width, clientRect.Height);break;case DockStyle.Right:descBounds = new Rectangle(clientRect.Width - bounds.Width,0,bounds.Width,clientRect.Height);break;case DockStyle.Top:descBounds = new Rectangle(0,0,clientRect.Width,bounds.Height);break;if (descBounds != bounds)info.Bounds = descB
16、ounds;return true;这段代码中,用到了一个 WindowInformation 的类型,这个笔者编写的另外一个类型,实现了一些窗体相关的 Win32API 的封装。在 UpdateLayout 中,首先判断双方的窗体句柄是否有效,然后判断两个窗体控件的父子关系,若不是则设置控件的父子关系,底层调用的是 Win32API 函数 SetParent。袁永福版权所有然后根据对象的 Dock 属性值和父控件的客户区大小来计算出子控件应有的位置和大小,然后设置子控件的位置和大小。这样这个控件本身开发完毕了。设置程序集另外还需要对 C#项目进行一些设置来完成公开 COM 开发接口的工作。打
17、开工程文件中的 AssemblyInfo.cs 文件,该文件默认在 Properties 目录下,设置其代码内容为:using System.Reflection;using System.Runtime.CompilerServices;using System.Runtime.InteropServices;/ 有关程序集的常规信息通过以下/ 特性集控制。更改这些特性值可修改/ 与程序集关联的信息。assembly: AssemblyTitle(“袁永福的 COM 测试“)assembly: AssemblyDescription(“)assembly: AssemblyConfigura
18、tion(“)assembly: AssemblyCompany(“)assembly: AssemblyProduct(“DCWinFormControlLib“)assembly: AssemblyCopyright(“Copyright 2012“)assembly: AssemblyTrademark(“)assembly: AssemblyCulture(“)/ 将 ComVisible 设置为 false 使此程序集中的类型/ 对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型,/ 则将该类型上的 ComVisible 特性设置为 true。assembly: Com
19、Visible(true)/ 如果此项目向 COM 公开,则下列 GUID 用于类型库的 IDassembly: Guid(“c241537f-dfe8-4502-b43a-967f9437ee7c“)assembly: AssemblyVersion(“1.0.0.*“)这段代码中,重点之一是“ComVisible”,这是程序集允许向 COM 公开;还有一个“Guid”,这是为类型库提供一个编号;还有 “AssemblyVersion”设置程序集的版本号,这里有一个*表示这个版本号是累计的,袁永福版权所有每次编译生成的程序集的版本号都会增加 的。强名称要提供 COM 开发接口,.NET 程序
20、集必须是强名称的。要对.NET 程序集强名称,首先是要有一个 SNK 签名文件,若没有,则可以使用.NET SDK 中的 SN.EXE 工具生成一个 SNK 文件。打开 VS.NET 自带的.NET SDK 命令行界面,执行命令行“SN.EXE k d:dctest.snk”,即可生成一个强名称使用的 SNK 文件。则命令行输出文本为:Microsoft(R) .NET Framework 强名称实用工具 版本 4.0.30319.1版权所有(C) Microsoft Corporation。保留所有权利。密钥对写入到 d:dctest.snk在 VS.NET 中打开项目的属性页面,切换到“签
21、名”页面,如下图所示:勾选“为程序集签名”,然后在强名称秘钥文件下列列表中点击“浏览”项目,选择 SNK 文件。然后重新编译,即可生成强名称的.NET 程序集 DLL 文件了。注册和生成类型库文件当成功的编译生成.NET 程序集 DLL 文件后,需要对该程序集进行注册和生成COM 开发使用的类型库文件。袁永福版权所有在 VS.NET 命令行环境,执行命令“regasm 编译输出目录DCWinFormControlLib.dll /tlb /codebase”,则该命令行输出的内容如下:Microsoft(R) .NET Framework 程序集注册实用工具 4.0.30319.1版权所有(C
22、) Microsoft Corporation 1998-2004。保留所有权利。成功注册了类型成功注册了导出到“编译输出目录dcwinformcontrollib.tlb”的程序集和类型库特别要注意,对于 Win7 或更高系统中,必须以管理员的模式启动 VS.NET 命令行环境,否则程序会爆无权限的错误。四VB 程序开发当成功的执行完上述操作后,我们就已经生成了可用于 VB 开发的.NET 程序组件了。笔者使用 VB6.0 开发了一个程序能应用 MyWinFormControl 控件,程序已经写好,现对其进行说明。袁永福版权所有首先该 VB 工程名为 DCVBExe,它是 一个普通的 EXE
23、 的 VB 工程。在 VB6.0 IDE 中点击菜单“工程-引用”,显示如下图所示的添加引用对话框:在该对话框中就可以看到电脑系统中已经有了 DCWinFormControlLib 的引用了,VB 工程就包含了该引用。本 VB 工程的主窗体设计如下:这个 VB 窗体中放置了一个名为“cmdSetText”的按钮控件,还有一个名为“picContainer”的 PictureBox 控件。该窗体的 VB 代码如下:Option Explicit 定义控件变量Private WithEvents myWinFormControl As DCWinFormControlLib.myWinFormCo
24、ntrol VB 按钮点击事件处理Private Sub cmdSetText_Click()myWinFormControl.UserText = “袁永福到此一游“End Sub 窗体加载事件Private Sub Form_Load()Set myWinFormControl = New DCWinFormControlLib.myWinFormControlmyWinFormControl.Dock = 4myWinFormControl.AppendToContainerControl Me.picContainer.hWndEnd Sub 窗体大小改变事件Private Sub F
25、orm_Resize()If Me.ScaleWidth 3000 And Me.ScaleHeight 3000 ThenMe.picContainer.Width = Me.ScaleWidth - Me.picContainer.Left - 50Me.picContainer.Height = Me.ScaleHeight - Me.picContainer.Top - 50If Not myWinFormControl Is Nothing ThenmyWinFormControl.AppendToContainerControl Me.picContainer.hWndEnd If
26、End IfEnd Sub 窗体卸载事件Private Sub Form_Unload(Cancel As Integer)Set myWinFormControl = NothingEnd Sub 响应控件 ComButtonClick 事件Private Sub myWinFormControl_ComButtonClick()MsgBox “用户点击了控件中的按钮“End Sub 响应控件 ComTextChanged 事件Private Sub myWinFormControl_ComTextChanged()Me.Caption = “用户修改了控件中的文字“End Sub在这段代码
27、中“Private WithEvents myWinFormControl As DCWinFormControlLib.myWinFormControl”定义了 C#控件的变量,此处使用了WithEvents 关键字声明变量可以 绑定事件,此时就可以添加myWinFormControl_ComButtonClick 方法和 myWinFormControl_ComTextChanged 方法,这两个方法就是控件的事件处理。袁永福版权所有在窗体加载代码中调用了“myWinFormControl.Dock = 4”,实际上就是设置了 C#中的 System.Windows.Forms.Contr
28、ol 类型的 Dock 属性,由于 VB 中还不能识别 System.Windows.Forms.DockStyle 枚举类型,因此只能设置整数,这里的整数 4 等价于 DockStyle.Right 值。由于 MyWinFormControl 类型只声明了 IComMyEvnet 接口,没实现其他接口,因此在开发阶段,VB 无法识别控件的所有的属 性和方法,因此在编写代码的时候无法弹出类型成员列表,因此只能利用 VB 的动态语言特性先硬编码,在运行时 VB 会后期的识别和调用组件的公开的类型成员。在窗体加载时还运行了“myWinFormControl.AppendToContainerCon
29、trol Me.picContainer.hWnd”,用于将控件作为图片框的子控件硬塞入到 VB 窗体中。在窗体大小改变事件中,这行代码还重新执行了一遍,用于根据作为容器的图片框控件的大小来更新 WinForm.NET 控件的大小。这样 VB 程序开发完毕,编译执行 EXE 文件,该 VB 程序的运行界面如下图所示:本程序有一个已知的 BUG,那就是输入焦点分配有点混乱。由于很多代码绕过了 VB 运行库,而是在.NET 运行库中运行,这是一种不多见的软件运行架构。因此用户界面上尽量少放置或不放置可以获得焦点的 VB 控件。五常见问题和调试VB 调用.NET 控件是一种异构软件架构,很容易出现各
30、种问题,最常见的是在 VB 中试图创建控件对象实例时爆出“ActiveX component cant create object”的错误。解决方法如下1. 首先确定系统安装了.NET 框架。2. 执行命令行“regasm 程序集目录DCWinFormControlLib.dll /tlb /codebase”。3. 若还不行需要清理下系统注册表,对于本控件,设置的 GUID 值为“60550064-C97F-4306-A8B2-6908F50780E3”,因此使 用注册表编辑器打开注册项目路径“HKEY_CLASSES_ROOTCLSID60550064-C97F-4306- A8B2-69
31、08F50780E3InprocServer32”,可以看到该组件历次版本的注册信息,删掉这些注册表项目,然后重新运行一遍 regasm 命令行。4. 可以使用 gacutil 工具将 DCWinFormControlLib.dll 放置在全局程序集缓存中。5. 还可以将 VB 编译生成的 EXE 文件放在 DCWinFormControlLib.dll 和DCWinFormControl.tlb 的文件目录中运行。另外一个有可能遇到的情况就是 VB 程序不能运行,运行 VB 程序需要系统中有 VB 虚拟机,要检查 Windows 系统目录下的 System32 子目 录中是否存在MSVBVM60.DLL,若没有则需要下载安装 VB6 运行时,推荐下载地址为 “http:/ fe53eb7ef348/VB6.0-KB290887-X86.exe”。袁永福版权所有六。小结本文虽然讨论的仅仅是 VB 中调用 C#写的 WinForm 控件,实际上可以扩展开来使用,比如可以以此为基础,实现在 VBPBDELPHI VC 窗体中嵌入WinForm.NET 控件,由于 WinForm.NET 控件能承载 WPF 元素,因此也就能在 VB中嵌入 WPF 程序了。案列: