點擊下載本文配套的演示程序代碼http://files.cnblogs.com/xdesigner/VB-CS-WinformControl.zip。
一.前言:
雖然IT開發技術日新月異,不過業界仍然運行着大量的VB系統,這些系統凝聚了不少客戶的投資,應當要一定程度的保護和利用。因此也就產生了一種需求,也就是使用舊的開發技術仍然可以使用新技術的產出。本文就討論如何在VB6.0開發中使用上WinForm.NET控件。[袁永福版權所有]
二.軟件原理:
運行VB IDE,打開或創建一個EXE工程,打開窗體設計器,如下圖所示:
為了能在窗體上添加控件,需要往窗體左邊的工具箱上添加項目,需要點擊菜單項目“Project-Components”,此時會彈出如下圖所示的對話框:
點擊“Browse”按鈕,彈出文件選擇對話框,這個對話框中優先選擇OCX文件,而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函數,將WinForm.NET控件硬塞入VB窗體中。這樣在用戶界面上,用戶能看到和使用WinForm.NET控件;在后台,VB代碼能訪問.NET組件提供的公開的屬性、方法和事件,實現了VB全方位的調用WinForm.NET控件。
三.C#開發
C#控件開發
根據上述的軟件原理,筆者開發一個WinForm.NET控件並成功的應用於VB6.0的開發中,現對軟件進行說明。
這個WinForm.NET控件名為MyWinFormControl,派生自System.Windows.Forms.UserControl類型,它包含在一個名為DCWinFormControlLib的C#項目中,項目輸出類型為類庫,目標框架為.NET2.0,添加了對System.Windows.Forms.dll的引用。
界面設計:
MyWinFormControl控件的用戶界面設計如下:
在界面上放置一個名為“btnAction”的按鈕,一個名為“myTextBox”的文本框。
定義公開屬性和方法:
打開該控件的C#代碼文件,可以看到聲明該類型的C#代碼如下:
[System.Runtime.InteropServices.ComVisible(true)] [System.Runtime.InteropServices.Guid("60550064-C97F-4306-A8B2-6908F50780E3")] [System.Runtime.InteropServices.ComSourceInterfaces(typeof(IComMyEvent))] public partial class MyWinFormControl : UserControl { }
這段代碼中,第一行代碼的ComVisible標記類型為COM公開的;第二行代碼Guid標記了類型在COM中的唯一編號;第三行代碼的ComSourceInterfaces指明該類型實現了名為IComMyEvent的事件接口。[袁永福版權所有]
VB中無法直接綁定編譯階段未知的控件事件,同時也無法直接感應C#中的事件,為此需要編寫一個接口通知VB存在若干事件,使得VB能綁定事件。因此在此定義了IComMyEvent接口,聲明了C#控件中的事件,IComMyEvent接口定義如下
using System.Runtime.InteropServices; [Guid("096EF9A6-24CB-4091-A18F-34DA38C9A6F1")] [ComVisible( true )] [InterfaceType( ComInterfaceType.InterfaceIsIDispatch )] public interface IComMyEvent { /// <summary> /// 按鈕按下事件 /// </summary> [DispId(12340)] void ComButtonClick(); /// <summary> /// 文本內容修改事件t /// </summary> [DispId(12350)] void ComTextChanged(); } /// <summary> /// 無參數無返回值委托類型 /// </summary> public delegate void VoidEventHandler();
而后在控件的C#代碼中添加以下代碼:
#region 實現 IComMyEvent 中的成員 /// <summary> /// 按鈕按下事件 /// </summary> public event VoidEventHandler ComButtonClick = null; /// <summary> /// 文本內容修改事件 /// </summary> public event VoidEventHandler ComTextChanged = null; #endregion
這樣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公開的事件后,就可以編寫COM公開的屬性和方法,其代碼如下:
/// <summary> /// 公開的屬性 /// </summary> public string UserText { get { return myTextBox.Text; } set { myTextBox.Text = value; } } /// <summary> /// 公開的方法 /// </summary> public double Calcute(double p) { return Math.Sin(p); }
這個用戶控件雖然能在VB代碼中創建和訪問,但還不能直接拖放到VB窗體上,此時還需要使用代碼將C#控件添加到VB窗體上:
/// <summary> /// 將控件添加到指定句柄的窗體中 /// </summary> /// <param name="containerHandle">指定的窗體句柄對象</param> /// <returns>操作是否成功</returns> public bool AppendToContainerControl(int containerHandle) { CrossPlatformControlHostManager man = new CrossPlatformControlHostManager(); man.ContainerHandle = new IntPtr(containerHandle); man.ControlHandle = this.Handle; man.Dock = this.Dock; return man.UpdateLayout(); }
在這個函數中,參數為VB窗體中某個控件的句柄,該控件用於承載C#控件。這段代碼使用了筆者編寫的一個CrossPlatformControlHostManager類型,該類型專業用於執行跨應用程序的控件承載,實現“乾坤大挪移”,該類型首先定義了幾個屬性:[袁永福版權所有]
private IntPtr _ControlHandle = IntPtr.Zero; /// <summary> /// 操作的控件句柄對象 /// </summary> public IntPtr ControlHandle { get{ return _ControlHandle; } set{ _ControlHandle = value; } } private IntPtr _ContainerHandle = IntPtr.Zero; /// <summary> /// 容器元素對象 /// </summary> public IntPtr ContainerHandle { get{ return _ContainerHandle; } set{ _ContainerHandle = value; } } private DockStyle _Dock = DockStyle.Fill; /// <summary> /// 停靠樣式 /// </summary> public DockStyle Dock { get{ return _Dock; } set{ _Dock = value; } }
此外還定義了一個方法,其代碼如下:
/// <summary> /// 更新排版 /// </summary> /// <returns>操作是否成功</returns> public bool UpdateLayout() { WindowInformation info = new WindowInformation(this.ControlHandle); 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.ClientBounds; 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 Rectangle( 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 = descBounds; } return true; }
這段代碼中,用到了一個WindowInformation的類型,這個筆者編寫的另外一個類型,實現了一些窗體相關的Win32API的封裝。
在UpdateLayout中,首先判斷雙方的窗體句柄是否有效,然后判斷兩個窗體控件的父子關系,若不是則設置控件的父子關系,底層調用的是Win32API函數SetParent。[袁永福版權所有]
然后根據對象的Dock屬性值和父控件的客戶區大小來計算出子控件應有的位置和大小,然后設置子控件的位置和大小。
這樣這個控件本身開發完畢了。
設置程序集
另外還需要對C#項目進行一些設置來完成公開COM開發接口的工作。
打開工程文件中的AssemblyInfo.cs文件,該文件默認在Properties目錄下,設置其代碼內容為:
using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // 有關程序集的常規信息通過以下 // 特性集控制。更改這些特性值可修改 // 與程序集關聯的信息。 [assembly: AssemblyTitle("袁永福的COM測試")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("DCWinFormControlLib")] [assembly: AssemblyCopyright("Copyright © 2012")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // 將 ComVisible 設置為 false 使此程序集中的類型 // 對 COM 組件不可見。如果需要從 COM 訪問此程序集中的類型, // 則將該類型上的 ComVisible 特性設置為 true。 [assembly: ComVisible(true)] // 如果此項目向 COM 公開,則下列 GUID 用於類型庫的 ID [assembly: Guid("c241537f-dfe8-4502-b43a-967f9437ee7c")] [assembly: AssemblyVersion("1.0.0.*")]
這段代碼中,重點之一是“ComVisible”,這是程序集允許向COM公開;還有一個“Guid”,這是為類型庫提供一個編號;還有“AssemblyVersion”設置程序集的版本號,這里有一個*表示這個版本號是累計的,[袁永福版權所有]每次編譯生成的程序集的版本號都會增加的。
強名稱
要提供COM開發接口,.NET程序集必須是強名稱的。
要對.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中打開項目的屬性頁面,切換到“簽名”頁面,如下圖所示:
勾選“為程序集簽名”,然后在強名稱秘鑰文件下列列表中點擊“瀏覽”項目,選擇SNK文件。然后重新編譯,即可生成強名稱的.NET程序集DLL文件了。
注冊和生成類型庫文件
當成功的編譯生成.NET程序集DLL文件后,需要對該程序集進行注冊和生成COM開發使用的類型庫文件。[袁永福版權所有]
在VS.NET命令行環境,執行命令“regasm 編譯輸出目錄\DCWinFormControlLib.dll /tlb /codebase”,則該命令行輸出的內容如下:
Microsoft(R) .NET Framework 程序集注冊實用工具 4.0.30319.1
版權所有(C) Microsoft Corporation 1998-2004。保留所有權利。
成功注冊了類型
成功注冊了導出到“編譯輸出目錄\dcwinformcontrollib.tlb”的程序集和類型庫
特別要注意,對於Win7或更高系統中,必須以管理員的模式啟動VS.NET命令行環境,否則程序會爆無權限的錯誤。
四.VB程序開發
當成功的執行完上述操作后,我們就已經生成了可用於VB開發的.NET程序組件了。
筆者使用VB6.0開發了一個程序能應用MyWinFormControl控件,程序已經寫好,現對其進行說明。[袁永福版權所有]
首先該VB工程名為DCVBExe,它是 一個普通的EXE的VB工程。在VB6.0 IDE中點擊菜單“工程-引用”,顯示如下圖所示的添加引用對話框:
在該對話框中就可以看到電腦系統中已經有了DCWinFormControlLib的引用了,VB工程就包含了該引用。
本VB工程的主窗體設計如下:
這個VB窗體中放置了一個名為“cmdSetText”的按鈕控件,還有一個名為“picContainer”的PictureBox控件。
該窗體的VB代碼如下:
Option Explicit ' 定義控件變量 Private WithEvents myWinFormControl As DCWinFormControlLib.myWinFormControl ' VB按鈕點擊事件處理 Private Sub cmdSetText_Click() myWinFormControl.UserText = "袁永福到此一游" End Sub ' 窗體加載事件 Private Sub Form_Load() Set myWinFormControl = New DCWinFormControlLib.myWinFormControl myWinFormControl.Dock = 4 myWinFormControl.AppendToContainerControl Me.picContainer.hWnd End Sub ' 窗體大小改變事件 Private Sub Form_Resize() If Me.ScaleWidth > 3000 And Me.ScaleHeight > 3000 Then Me.picContainer.Width = Me.ScaleWidth - Me.picContainer.Left - 50 Me.picContainer.Height = Me.ScaleHeight - Me.picContainer.Top - 50 If Not myWinFormControl Is Nothing Then myWinFormControl.AppendToContainerControl Me.picContainer.hWnd End If End If End Sub ' 窗體卸載事件 Private Sub Form_Unload(Cancel As Integer) Set myWinFormControl = Nothing End Sub ' 響應控件ComButtonClick事件 Private Sub myWinFormControl_ComButtonClick() MsgBox "用戶點擊了控件中的按鈕" End Sub ' 響應控件ComTextChanged事件 Private Sub myWinFormControl_ComTextChanged() Me.Caption = "用戶修改了控件中的文字" End Sub
在這段代碼中“Private WithEvents myWinFormControl As DCWinFormControlLib.myWinFormControl”定義了C#控件的變量,此處使用了WithEvents關鍵字聲明變量可以綁定事件,此時就可以添加myWinFormControl_ComButtonClick方法和myWinFormControl_ComTextChanged方法,這兩個方法就是控件的事件處理。[袁永福版權所有]
在窗體加載代碼中調用了“myWinFormControl.Dock = 4”,實際上就是設置了C#中的System.Windows.Forms.Control類型的Dock屬性,由於VB中還不能識別System.Windows.Forms.DockStyle枚舉類型,因此只能設置整數,這里的整數4等價於DockStyle.Right值。
由於MyWinFormControl類型只聲明了IComMyEvnet接口,沒實現其他接口,因此在開發階段,VB無法識別控件的所有的屬性和方法,因此在編寫代碼的時候無法彈出類型成員列表,因此只能利用VB的動態語言特性先硬編碼,在運行時VB會后期的識別和調用組件的公開的類型成員。
在窗體加載時還運行了“myWinFormControl.AppendToContainerControl Me.picContainer.hWnd”,用於將控件作為圖片框的子控件硬塞入到VB窗體中。
在窗體大小改變事件中,這行代碼還重新執行了一遍,用於根據作為容器的圖片框控件的大小來更新WinForm.NET控件的大小。
這樣VB程序開發完畢,編譯執行EXE文件,該VB程序的運行界面如下圖所示:
本程序有一個已知的BUG,那就是輸入焦點分配有點混亂。由於很多代碼繞過了VB運行庫,而是在.NET運行庫中運行,這是一種不多見的軟件運行架構。因此用戶界面上盡量少放置或不放置可以獲得焦點的VB控件。
五.常見問題和調試
VB調用.NET控件是一種異構軟件架構,很容易出現各種問題,最常見的是在VB中試圖創建控件對象實例時爆出“ActiveX component can't create object”的錯誤。
解決方法如下
1. 首先確定系統安裝了.NET框架。
2. 執行命令行“regasm 程序集目錄\DCWinFormControlLib.dll /tlb /codebase”。
3. 若還不行需要清理下系統注冊表,對於本控件,設置的GUID值為“60550064-C97F-4306-A8B2-6908F50780E3”,因此使用注冊表編輯器打開注冊項目路徑“HKEY_CLASSES_ROOT\CLSID\{60550064-C97F-4306-A8B2-6908F50780E3}\InprocServer32”,可以看到該組件歷次版本的注冊信息,刪掉這些注冊表項目,然后重新運行一遍regasm命令行。
4. 可以使用gacutil工具將DCWinFormControlLib.dll放置在全局程序集緩存中。
5. 還可以將VB編譯生成的EXE文件放在DCWinFormControlLib.dll和DCWinFormControl.tlb的文件目錄中運行。
另外一個有可能遇到的情況就是VB程序不能運行,運行VB程序需要系統中有VB虛擬機,要檢查Windows系統目錄下的System32子目錄中是否存在MSVBVM60.DLL,若沒有則需要下載安裝VB6運行時,推薦下載地址為“http://download.microsoft.com/download/5/a/d/5ad868a0-8ecd-4bb0-a882-fe53eb7ef348/VB6.0-KB290887-X86.exe”。[袁永福版權所有]
六。小結
本文雖然討論的僅僅是VB中調用C#寫的WinForm控件,實際上可以擴展開來使用,比如可以以此為基礎,實現在VB\PB\DELPHI\VC窗體中嵌入WinForm.NET控件,由於WinForm.NET控件能承載WPF元素,因此也就能在VB中嵌入WPF程序了。