42、.NET与COM编程指南

.NET与COM编程指南

1. 引言

在编程领域,开发者都希望有一个全新的开始,但对于大多数公司而言,扔掉过去编写的代码重新开始并不现实。过去十年里,许多开发组织在开发和购买COM组件及ActiveX控件上投入了大量资源。微软承诺确保这些遗留组件能在.NET应用程序中使用,同时也让.NET组件能被COM轻松调用。本文将介绍.NET在导入ActiveX控件和COM组件、将.NET类暴露给基于COM的应用程序以及直接调用Win32 API等方面的支持,还会涉及C#指针和直接访问内存的关键字。

2. 导入ActiveX控件

ActiveX控件是COM组件,通常会被放置在窗体中,可能有也可能没有用户界面。自微软开发OCX标准以来,ActiveX控件革命兴起,成千上万的此类控件被开发、销售和使用。它们体积小、易于使用,是二进制重用的有效范例。尽管COM对象与.NET对象差异较大,但将ActiveX控件导入.NET却出人意料地简单。Visual Studio 2008能自动导入ActiveX控件,此外,微软还开发了命令行工具Aximp,可创建在.NET应用程序中使用控件所需的程序集。

2.1 创建ActiveX控件

为了演示在.NET应用程序中使用经典ActiveX控件的能力,我们将先开发一个简单的四则运算计算器作为ActiveX控件,然后在C#应用程序中调用该控件。具体步骤如下:
1. 打开VB 6 :选择ActiveX控件作为新项目类型。由于该控件没有用户界面,将项目窗体设置得尽可能小。
2. 重命名控件和项目 :右键单击UserControl1,选择“属性”,在属性窗口中将其重命名为Calculator。点击项目资源管理器中的“项目”,在属性窗口中将项目重命名为CalcControl,并立即保存项目,将文件和项目都命名为CalcControl。
3. 添加计算器功能 :右键单击CalcControl窗体,从弹出菜单中选择“查看代码”,输入以下VB代码:

Public Function _
Add(left As Double, right As Double) _
    As Double
    Add = left + right
End Function

Public Function _
Subtract(left As Double, right As Double) _
    As Double
    Subtract = left - right
End Function

Public Function _
Multiply(left As Double, right As Double) _
    As Double
    Multiply = left * right
End Function

Public Function _
Divide(left As Double, right As Double) _
    As Double
    Divide = left / right
End Function
  1. 编译控件 :在VB 6菜单栏中选择“文件” -> “生成CalcControl.ocx”,将代码编译为CalcControl.ocx文件。
  2. 测试控件 :打开一个新的VB标准可执行项目(EXE),将窗体命名为TestForm,项目命名为CalcTest并保存。按下Ctrl - T,从“控件”选项卡中选择CalcControl,将其添加为组件。这会在工具箱中添加一个新控件,将其拖到TestForm窗体上并命名为CalcControl(该控件无用户界面,不可见)。添加两个文本框、四个按钮和一个标签,将按钮分别命名为btnAdd、btnSubtract、btnMultiply和btnDivide。为计算器按钮的点击事件实现处理方法,示例代码如下:
Private Sub btnAdd_Click( )
    Label1.Caption = _
        calcControl.Add(CDbl(Text1.Text), _
        CDbl(Text2.Text))
End Sub

Private Sub btnDivide_Click( )
    Label1.Caption = _
        calcControl.Divide(CDbl(Text1.Text), _
        CDbl(Text2.Text))
End Sub

Private Sub btnMultiply_Click( )
    Label1.Caption = _
        calcControl.Multiply(CDbl(Text1.Text), _
        CDbl(Text2.Text))
End Sub

Private Sub btnSubtract_Click( )
    Label1.Caption = _
        calcControl.Subtract(CDbl(Text1.Text), _
        CDbl(Text2.Text))
End Sub
2.2 在.NET中导入控件

当确认CalcControl ActiveX控件能正常工作后,将CalcControl.ocx文件复制到.NET开发环境中。复制后,需要使用Regsvr32注册该文件(如果运行的是Vista系统,需要以管理员身份执行):

Regsvr32 CalcControl.ocx

接下来,在.NET中构建一个测试程序来使用该计算器,步骤如下:
1. 创建项目 :在Visual Studio 2008中创建一个Visual C# Windows Forms应用程序,命名为InteropTest,并设计一个窗体(如前面在VB中创建的TestForm窗体),将窗体命名为TestForm。
2. 导入控件 :有两种导入方式:
- 使用Visual Studio 2008工具 :右键单击工具箱,添加一个名为COM的选项卡,再次右键单击并选择“选择项”,在弹出的“选择工具箱项”对话框中选择“COM组件”选项卡,找到并添加CalcControl。
- 手动导入 :打开命令框,使用Aximp.exe工具手动导入控件:

Aximp.exe CalcControl.ocx

该命令会生成三个文件:AxCalcControl.dll(.NET Windows控件)、CalcControl.dll(代理.NET类库)和AxCalcControl.pdb(调试文件)。完成后,返回“选择工具箱项”窗口,这次选择“.NET Framework组件”,浏览到生成AxCalcControl.dll的位置并将其导入到工具箱。
3. 添加控件到窗体 :导入后,控件会出现在工具箱菜单中,将其拖到Windows窗体上并使用其功能。为四个按钮添加事件处理程序,事件处理程序将工作委托给在VB 6中编写并导入到.NET的ActiveX控件,示例代码如下:

using System;
using System.Windows.Forms;
namespace InteropTest
{
    public partial class Form1 : Form
    {
        public Form1( )
        {
            InitializeComponent( );
        }
        private void btnAdd_Click(object sender, EventArgs e)
        {
            double left = double.Parse(textBox1.Text);
            double right = double.Parse(textBox2.Text);
            label1.Text = axCalculator1.Add(ref left, ref right).ToString( );
        }
        private void btnSubtract_Click(object sender, EventArgs e)
        {
            double left = double.Parse(textBox1.Text);
            double right = double.Parse(textBox2.Text);
            label1.Text = axCalculator1.Subtract(ref left, ref right).ToString( );
        }
        private void btnMultiply_Click(object sender, EventArgs e)
        {
            double left = double.Parse(textBox1.Text);
            double right = double.Parse(textBox2.Text);
            label1.Text = axCalculator1.Multiply(ref left, ref right).ToString( );
        }
        private void btnDivide_Click(object sender, EventArgs e)
        {
            double left = double.Parse(textBox1.Text);
            double right = double.Parse(textBox2.Text);
            label1.Text = axCalculator1.Divide(ref left, ref right).ToString( );
        }
    }
}
3. P/Invoke

在C#中可以调用非托管代码,通常在需要完成FCL无法实现的任务时会这样做。随着.NET 2.0版本的推出,P/Invoke的使用将相对减少。.NET平台调用功能(P/Invoke)最初旨在提供对Windows API的访问,但也可用于调用任何DLL中的函数。

以重命名文件为例,我们可以使用Windows kernel32.dll中的MoveFile方法来实现与FileInfo类的MoveTo方法相同的功能。具体步骤如下:
1. 声明方法 :使用DllImport属性声明方法为静态外部方法:

[DllImport("kernel32.dll", EntryPoint="MoveFile",
 ExactSpelling=false, CharSet=CharSet.Unicode,
 SetLastError=true)]
static extern bool MoveFile(
 string sourceFile, string destinationFile);

DllImport属性的参数说明如下:
| 参数 | 说明 |
| ---- | ---- |
| DLL name | 要调用的DLL名称 |
| EntryPoint | 要调用的DLL入口点(方法)的名称 |
| ExactSpelling | 允许CLR根据命名约定匹配名称略有不同的方法 |
| CharSet | 指示方法的字符串参数应如何封送 |
| SetLastError | 设置为true时,允许调用Marshal.GetLastWin32Error检查调用方法时是否发生错误 |
2. 调用方法 :使用静态方法语义调用MoveFile方法:

Tester.MoveFile(file.FullName,file.FullName + ".bak");

完整示例代码如下:

using System;
using System.IO;
using System.Runtime.InteropServices;
namespace UsingPInvoke
{
    class Tester
    {
        // declare the WinAPI method you wish to P/Invoke
        [DllImport("kernel32.dll", EntryPoint = "MoveFile",
        ExactSpelling = false, CharSet = CharSet.Unicode,
        SetLastError = true)]
        static extern bool MoveFile(
        string sourceFile, string destinationFile);
        public static void Main( )
        {
            // make an instance and run it
            Tester t = new Tester( );
            string theDirectory = @"c:\test\media";
            DirectoryInfo dir =
            new DirectoryInfo(theDirectory);
            t.ExploreDirectory(dir);
        }
        // Set it running with a directory name
        private void ExploreDirectory(DirectoryInfo dir)
        {
            // make a new subdirectory
            string newDirectory = "newTest";
            DirectoryInfo newSubDir =
            dir.CreateSubdirectory(newDirectory);
            // get all the files in the directory and
            // copy them to the new directory
            FileInfo[] filesInDir = dir.GetFiles( );
            foreach (FileInfo file in filesInDir)
            {
                string fullName = newSubDir.FullName +
                "\\" + file.Name;
                file.CopyTo(fullName);
                Console.WriteLine("{0} copied to newTest",
                file.FullName);
            }
            // get a collection of the files copied in
            filesInDir = newSubDir.GetFiles( );
            // delete some and rename others
            int counter = 0;
            foreach (FileInfo file in filesInDir)
            {
                string fullName = file.FullName;
                if (counter++ % 2 == 0)
                {
                    // P/Invoke the Win API
                    Tester.MoveFile(fullName, fullName + ".bak");
                    Console.WriteLine("{0} renamed to {1}",
                    fullName, file.FullName);
                }
                else
                {
                    file.Delete( );
                    Console.WriteLine("{0} deleted.",
                    fullName);
                }
            }
            // delete the subdirectory
            newSubDir.Delete(true);
        }
    }
}
4. 指针

在C#中,指针主要用于不常见的高级编程,通常仅在使用P/Invoke时才会用到。C#支持常见的C指针运算符,如下表所示:
| 运算符 | 含义 |
| ---- | ---- |
| & | 取地址运算符,返回值的地址指针 |
| * | 解引用运算符,返回指针地址处的值 |
| -> | 成员访问运算符,用于访问类型的成员 |

使用指针时,必须使用C#的unsafe修饰符标记代码,因为指针可以直接操作内存位置,这在普通C#程序中是无法实现的。在不安全代码中,可以直接访问内存、进行指针和整数类型之间的转换、获取变量的地址等,但会失去垃圾回收、对未初始化变量的保护、防止悬空指针以及防止访问数组越界等功能。

以下是一个使用指针的示例,通过调用两个Win32 API函数CreateFile和ReadFile将文件内容读取到控制台:

using System;
using System.Runtime.InteropServices;
using System.Text;
namespace UsingPointers
{
    class APIFileReader
    {
        const uint GenericRead = 0x80000000;
        const uint OpenExisting = 3;
        const uint UseDefault = 0;
        int fileHandle;
        [DllImport("kernel32", SetLastError = true)]
        static extern unsafe int CreateFile(
        string filename,
        uint desiredAccess,
        uint shareMode,
        uint attributes,
        uint creationDisposition,
        uint flagsAndAttributes,
        uint templateFile);
        [DllImport("kernel32", SetLastError = true)]
        static extern unsafe bool ReadFile(
        int hFile,
        void* lpBuffer,
        int nBytesToRead,
        int* nBytesRead,
        int overlapped);
        // constructor opens an existing file
        // and sets the file handle member
        public APIFileReader(string filename)
        {
            fileHandle = CreateFile(
            filename, // filename
            GenericRead, // desiredAccess
            UseDefault, // shareMode
            UseDefault, // attributes
            OpenExisting, // creationDisposition
            UseDefault, // flagsAndAttributes
            UseDefault); // templateFile
        }
        public unsafe int Read(byte[] buffer, int index, int count)
        {
            int bytesRead = 0;
            fixed (byte* bytePointer = buffer)
            {
                ReadFile(
                fileHandle, // hfile
                bytePointer + index, // lpBuffer
                count, // nBytesToRead
                &bytesRead, // nBytesRead
                0); // overlapped
            }
            return bytesRead;
        }
    }
    class Test
    {
        public static void Main( )
        {
            // create an instance of the APIFileReader,
            // pass in the name of an existing file
            APIFileReader fileReader =
            new APIFileReader("8Swnn10.txt");
            // create a buffer and an ASCII coder
            const int BuffSize = 128;
            byte[] buffer = new byte[BuffSize];
            ASCIIEncoding asciiEncoder = new ASCIIEncoding( );
            // read the file into the buffer and display to console
            while (fileReader.Read(buffer, 0, BuffSize) != 0)
            {
                Console.Write("{0}", asciiEncoder.GetString(buffer));
            }
        }
    }
}

在这个示例中,使用了fixed关键字将缓冲区固定在内存中,以确保在调用ReadFile函数时指针的有效性。编译包含不安全代码的程序时,需要使用/unsafe编译器选项,可在项目属性的“生成”选项卡中勾选“允许不安全代码”。

综上所述,通过本文的介绍,我们了解了在.NET应用程序中导入ActiveX控件、使用P/Invoke调用非托管代码以及使用指针进行高级编程的方法和技巧。这些技术在处理遗留组件和实现特定功能时非常有用,但在使用时需要注意安全性和性能问题。

.NET与COM编程指南

5. 指针使用的深入分析

在前面的示例中,我们看到了指针在调用Win32 API时的应用。下面我们进一步深入分析指针在C#中的使用场景和潜在风险。

5.1 指针使用的场景

指针在C#中虽然不常用,但在某些特定场景下却非常有用。除了前面提到的文件读取,还有一些其他场景也会用到指针:
- 高性能计算 :在需要处理大量数据且对性能要求极高的场景下,使用指针可以避免托管代码的开销,直接操作内存,提高计算效率。
- 与非托管代码交互 :当需要调用一些底层的非托管库时,指针是必不可少的工具,因为非托管库通常使用指针来传递数据和执行操作。

5.2 指针使用的潜在风险

使用指针虽然可以带来一些性能上的优势,但也伴随着很多潜在风险:
- 内存管理问题 :在使用指针时,需要手动管理内存,这意味着需要自己负责内存的分配和释放。如果没有正确处理,可能会导致内存泄漏或悬空指针等问题。
- 类型安全问题 :指针操作绕过了C#的类型系统,这可能会导致类型不匹配的错误,从而引发程序崩溃或产生不可预期的结果。
- 安全性问题 :指针可以直接访问内存,这使得程序更容易受到恶意攻击,例如缓冲区溢出攻击。

为了降低这些风险,在使用指针时需要格外小心,遵循一些最佳实践:
- 尽量减少指针的使用 :只有在确实需要提高性能或与非托管代码交互时才使用指针。
- 仔细管理内存 :确保在使用完指针后及时释放内存,避免内存泄漏。
- 进行严格的类型检查 :在进行指针操作时,要确保指针指向的内存区域类型正确,避免类型不匹配的问题。

6. 综合应用示例

为了更好地理解前面介绍的技术,我们来看一个综合应用示例,将导入ActiveX控件、P/Invoke和指针的使用结合起来。

假设我们要开发一个Windows Forms应用程序,该程序需要使用一个ActiveX控件来进行简单的计算,同时还需要调用Win32 API来进行文件操作,并且在某些情况下使用指针来提高性能。

6.1 项目搭建

首先,按照前面介绍的方法,创建一个Visual C# Windows Forms应用程序,并导入之前创建的CalcControl ActiveX控件。

6.2 实现计算功能

在窗体上添加几个文本框和按钮,用于输入计算数据和触发计算操作。为按钮添加事件处理程序,调用ActiveX控件的计算方法,示例代码如下:

using System;
using System.Windows.Forms;

namespace ComprehensiveApp
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void btnAdd_Click(object sender, EventArgs e)
        {
            double left = double.Parse(textBox1.Text);
            double right = double.Parse(textBox2.Text);
            label1.Text = axCalculator1.Add(ref left, ref right).ToString();
        }

        private void btnSubtract_Click(object sender, EventArgs e)
        {
            double left = double.Parse(textBox1.Text);
            double right = double.Parse(textBox2.Text);
            label1.Text = axCalculator1.Subtract(ref left, ref right).ToString();
        }

        private void btnMultiply_Click(object sender, EventArgs e)
        {
            double left = double.Parse(textBox1.Text);
            double right = double.Parse(textBox2.Text);
            label1.Text = axCalculator1.Multiply(ref left, ref right).ToString();
        }

        private void btnDivide_Click(object sender, EventArgs e)
        {
            double left = double.Parse(textBox1.Text);
            double right = double.Parse(textBox2.Text);
            label1.Text = axCalculator1.Divide(ref left, ref right).ToString();
        }
    }
}
6.3 实现文件操作功能

使用P/Invoke调用Win32 API来进行文件操作,例如重命名文件。在窗体上添加一个按钮,为其添加事件处理程序,示例代码如下:

using System;
using System.IO;
using System.Runtime.InteropServices;

namespace ComprehensiveApp
{
    class FileOperator
    {
        [DllImport("kernel32.dll", EntryPoint = "MoveFile",
        ExactSpelling = false, CharSet = CharSet.Unicode,
        SetLastError = true)]
        static extern bool MoveFile(
        string sourceFile, string destinationFile);

        public static void RenameFile(string source, string destination)
        {
            MoveFile(source, destination);
        }
    }

    public partial class Form1 : Form
    {
        // 前面的代码...

        private void btnRenameFile_Click(object sender, EventArgs e)
        {
            string source = textBox3.Text;
            string destination = textBox4.Text;
            FileOperator.RenameFile(source, destination);
        }
    }
}
6.4 实现高性能数据处理功能

在某些情况下,需要对大量数据进行处理,这时可以使用指针来提高性能。例如,我们可以编写一个方法来使用指针遍历数组并进行求和操作,示例代码如下:

using System;

namespace ComprehensiveApp
{
    class HighPerformanceProcessor
    {
        public static unsafe int SumArray(int[] array)
        {
            fixed (int* ptr = array)
            {
                int sum = 0;
                int length = array.Length;
                for (int i = 0; i < length; i++)
                {
                    sum += *(ptr + i);
                }
                return sum;
            }
        }
    }

    public partial class Form1 : Form
    {
        // 前面的代码...

        private void btnSumArray_Click(object sender, EventArgs e)
        {
            int[] array = { 1, 2, 3, 4, 5 };
            int sum = HighPerformanceProcessor.SumArray(array);
            label2.Text = sum.ToString();
        }
    }
}
7. 总结与展望

通过本文的介绍,我们详细了解了在.NET应用程序中导入ActiveX控件、使用P/Invoke调用非托管代码以及使用指针进行高级编程的方法和技巧。这些技术在处理遗留组件和实现特定功能时非常有用,但在使用时需要注意安全性和性能问题。

在未来的开发中,随着技术的不断发展,可能会有更多的替代方案出现,减少对这些底层技术的依赖。例如,.NET平台可能会提供更多的高性能API,使得在不使用指针的情况下也能实现高效的数据处理。同时,对于ActiveX控件和COM组件,也可能会有更好的迁移方案,让开发者能够更轻松地将遗留代码迁移到现代的开发环境中。

然而,在当前阶段,这些技术仍然是非常重要的,特别是在处理一些旧系统和特定需求时。作为开发者,我们需要掌握这些技术,以便在实际项目中能够灵活运用,解决各种复杂的问题。

总之,.NET与COM编程是一个复杂而又有趣的领域,需要我们不断学习和探索,才能更好地应对各种挑战。希望本文能够为你在.NET开发中提供一些帮助和启发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值