VB調用C#寫的WinForm.NET控件


點擊下載本文配套的演示程序代碼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程序了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM