.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
- 编译控件 :在VB 6菜单栏中选择“文件” -> “生成CalcControl.ocx”,将代码编译为CalcControl.ocx文件。
- 测试控件 :打开一个新的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开发中提供一些帮助和启发。
超级会员免费看
727

被折叠的 条评论
为什么被折叠?



