浅谈在C#中调用COM组件——以文件夹选择器为例
【文 / 张赐荣】
在现如今的这个时代,提到跨语言调用或者系统级操作,许多开发者第一时间会想到.NET、Web API等现代技术。然而,不得不说,COM组件这门技术可能在许多年轻开发者的学习清单中早已被“扫进角落”了。毕竟现如今.NET、Web API、云技术满天飞,谁还会关心什么COM组件呢?可偏偏这玩意儿不但还活着,而且在Windows系统的某些角落里依然坚挺存在。尤其是,当你需要和Windows底层API打交道时,可能绕不开它。
今天这篇文章,就让我这热爱上古技术的“老开发者”,跟大家聊聊这个略显古早的——COM组件,并通过一个简单的文件夹选择器例子,详细讲讲C#是怎么调用COM的。
COM组件,听起来就很古老?
没错,COM(Component Object Model,组件对象模型)这个东西最早出现在90年代。那时候,.NET还没影儿呢,微软推出它是为了解决跨语言调用和软件组件复用的问题。说白了,COM就是为了解决“不同语言的代码怎么互相调用”这个难题。
举个例子吧,你用C++写了一个图像处理模块,想在用VB写的程序中调用它?没问题,COM来帮你搞定跨语言调用。不仅如此,COM的伟大之处在于它还能提供跨进程调用,这在当年简直就是黑科技。
然而,时过境迁,.NET的横空出世让开发者有了托管代码的选择,COM渐渐“退居二线”。但即便如此,Windows底层依然有大量API依赖它——比如今天我们要用到的文件夹选择器就是其中之一。只要你是Windows开发者,哪怕是个年轻人,也迟早得跟它打个照面。
如今为什么还要学COM组件?
我知道你们可能会问:“现在.NET都能搞定很多事了,为什么还要学COM?” 这个问题问得好。实际上,许多Windows底层功能,尤其是那些老牌的系统级API,依然离不开COM。再比如,微软的Office套件,很多功能还是通过COM接口实现的。如果你想在Windows底层API和老系统打交道,COM就是那把钥匙。
学COM,说难不难,说简单也不简单。接下来,我用一个常见的文件夹选择器来带你了解C#如何调用COM组件。
C#调用COM组件的基本步骤
在C#中调用COM组件并不复杂,但由于COM是非托管代码,C#需要通过互操作(Interop)机制来与其交互。互操作机制主要是让托管代码(C#)能够调用非托管代码(COM)。
调用COM组件的基本流程可以总结为以下几步:
- 导入COM接口:使用
[ComImport]
、[Guid]
等特性引入COM接口。 - 创建COM对象:通过接口的实现类实例化COM对象。
- 调用方法:通过接口调用COM的功能。
- 释放资源:由于COM组件是非托管代码,使用完要记得手动释放资源。
别急,我会一步步拆解这些过程,接下来我们通过一个简单的文件夹选择器示例来具体演示如何在C#中调用COM组件。
示例代码:实现文件夹选择器
这个示例的主要功能是让用户通过Windows对话框选择一个文件夹,选中的文件夹路径会返回给程序。我们使用了COM组件 IFileOpenDialog
,它是Windows提供的文件对话框接口之一。
接下来,我会详细剖析这段代码。
1. 命名空间导入和声明类
using System;
using System.Runtime.InteropServices;
namespace WinApi
{
static class FolderPicker
{
public static string ChooseDirectory()
{
上面我们导入了System.Runtime.InteropServices
,这是C#与非托管代码(比如COM组件)打交道的关键命名空间。FolderPicker
是一个静态类,里面的ChooseDirectory
方法就是用于弹出文件夹选择对话框的关键部分。
2. 创建 IFileOpenDialog
对象
IFileOpenDialog dialog = null;
try
{
dialog = (IFileOpenDialog)new FileOpenFileDialog();
这里的IFileOpenDialog
是Windows API提供的接口,用来显示文件(或文件夹)选择对话框。我们通过FileOpenFileDialog
实例化IFileOpenDialog
。这个FileOpenFileDialog
是什么?稍后我们会看到它的定义。需要注意的是,创建COM对象的方式和我们通常用new
实例化类的方式不同,COM对象需要通过接口来操作。
3. 设置对话框选项
uint options;
dialog.GetOptions(out options);
options |= (uint)FOS.FOS_PICKFOLDERS;
dialog.SetOptions(options);
这一段的意思是,首先通过GetOptions
获取当前对话框的选项,然后使用FOS.FOS_PICKFOLDERS
标志位将对话框设置为“文件夹选择”模式(而不是默认的选择文件)。最后通过SetOptions
重新应用这些设置。
FOS_PICKFOLDERS
是一个常量,它表示这次我们只关心文件夹,不需要文件。
4. 显示对话框并处理返回值
int hr = dialog.Show(IntPtr.Zero);
if (hr == (int)HRESULT.ERROR_CANCELLED)
{
return null;
}
else if (hr != 0)
{
Marshal.ThrowExceptionForHR(hr);
}
接下来,通过Show
方法显示对话框。这里传入的IntPtr.Zero
表示没有父窗口。如果用户取消选择,Show
方法会返回一个HRESULT.ERROR_CANCELLED
,这种情况下我们返回null
。否则,如果hr
返回的值不为0,我们通过Marshal.ThrowExceptionForHR
将错误码转换为C#的异常,这样便于后续处理。
5. 获取用户选择的文件夹路径
IShellItem item;
dialog.GetResult(out item);
string path;
item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out path);
return path;
用户选定文件夹后,我们通过GetResult
获取用户选择的文件夹(即IShellItem
对象),然后通过GetDisplayName
获取文件夹的路径。这里的SIGDN.SIGDN_FILESYSPATH
是说我们要获取文件夹的完整文件路径。
6. 释放COM对象
finally
{
if (dialog != null)
{
Marshal.ReleaseComObject(dialog);
}
}
COM对象与托管代码不同,C#的垃圾回收器并不能自动回收COM对象,因此我们需要手动释放它们。在finally
块中,我们通过Marshal.ReleaseComObject
确保即便出错也能正确释放IFileOpenDialog
,防止内存泄漏。
7. 相关的COM接口定义
接下来我们看看与IFileOpenDialog
和IShellItem
相关的COM接口定义。为了方便调用,C#通过[ComImport]
和[Guid]
特性来导入COM接口。
FileOpenFileDialog
类
[ComImport]
[Guid("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7")]
[ClassInterface(ClassInterfaceType.None)]
class FileOpenFileDialog
{
}
这是FileOpenFileDialog
的定义,它是一个COM类。我们用[ComImport]
告诉C#编译器这个类是从外部导入的COM对象,并通过[Guid]
提供这个COM类的唯一标识符。
IFileOpenDialog
接口
[ComImport]
[Guid("D57C7288-D4AD-4768-BE02-9D969532D960")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IFileOpenDialog
{
[PreserveSig]
int Show([In] IntPtr parent);
void SetOptions(uint fos);
void GetOptions(out uint fos);
void GetResult(out IShellItem ppsi);
}
IFileOpenDialog
接口定义了文件对话框的核心操作方法。我们主要用到Show
方法来显示对话框,SetOptions
和GetOptions
来设置和获取对话框选项,GetResult
则用于获取用户选择的文件夹或文件。
IShellItem
接口
[ComImport]
[Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IShellItem
{
void GetDisplayName(SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
}
IShellItem
是文件系统对象(如文件/文件夹)的COM接口。我们用它的GetDisplayName
方法来获取文件夹的路径。
总结
最后附上完整的代码吧:
using System;
using System.Runtime.InteropServices;
namespace WinApi
{
static class FolderPicker
{
public static string ChooseDirectory()
{
IFileOpenDialog dialog = null;
try
{
dialog = (IFileOpenDialog)new FileOpenFileDialog();
uint options;
dialog.GetOptions(out options);
options |= (uint)FOS.FOS_PICKFOLDERS;
dialog.SetOptions(options);
int hr = dialog.Show(IntPtr.Zero);
if (hr == (int)HRESULT.ERROR_CANCELLED)
{
return null;
}
else if (hr != 0)
{
Marshal.ThrowExceptionForHR(hr);
}
IShellItem item;
dialog.GetResult(out item);
string path;
item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out path);
return path;
}
finally
{
if (dialog != null)
{
Marshal.ReleaseComObject(dialog);
}
}
}
}
[ComImport]
[Guid("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7")]
[ClassInterface(ClassInterfaceType.None)]
class FileOpenFileDialog
{
}
[ComImport]
[Guid("D57C7288-D4AD-4768-BE02-9D969532D960")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IFileOpenDialog
{
[PreserveSig]
int Show([In] IntPtr parent);
void SetFileTypes(uint cFileTypes, [In] IntPtr rgFilterSpec);
void SetFileTypeIndex(uint iFileType);
void GetFileTypeIndex(out uint piFileType);
void Advise(IntPtr pfde, out uint pdwCookie);
void Unadvise(uint dwCookie);
void SetOptions(uint fos);
void GetOptions(out uint fos);
void SetDefaultFolder(IShellItem psi);
void SetFolder(IShellItem psi);
void GetFolder(out IShellItem ppsi);
void GetCurrentSelection(out IShellItem ppsi);
void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName);
void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText);
void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
void GetResult(out IShellItem ppsi);
void AddPlace(IShellItem psi, uint fdap);
void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
void Close(int hr);
void SetClientGuid(ref Guid guid);
void ClearClientData();
void SetFilter(IntPtr pFilter);
void GetResults(out IntPtr ppenum);
void GetSelectedItems(out IntPtr ppsai);
}
[ComImport]
[Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IShellItem
{
void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv);
void GetParent(out IShellItem ppsi);
void GetDisplayName(SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
void Compare(IShellItem psi, uint hint, out int piOrder);
}
enum SIGDN : uint
{
SIGDN_FILESYSPATH = 0x80058000,
}
enum FOS : uint
{
FOS_PICKFOLDERS = 0x00000020,
}
enum HRESULT : int
{
ERROR_CANCELLED = unchecked((int)0x800704C7),
}
}
通过这个例子,我们可以看到C#调用COM组件的基本流程:导入COM接口、创建COM对象、调用接口方法、释放资源。每一步都依赖于C#和Windows系统之间的互操作机制,尤其是对COM对象的正确释放,至关重要。
实际上对于年轻开发者来说,不必对COM退避三舍,它依然是打开Windows底层世界的钥匙。掌握这门技术,你会发现,在处理一些系统级的操作或与遗留代码打交道时,COM可以让你“所向披靡”。
至于像我这样的老开发者,虽然我们热衷于这些“上古技术”,但无论是怀旧还是实用,技术本身的价值总是无可替代的。希望通过这篇文章,你能对COM有一个清晰的认识,也许在未来的某个项目中,你也会用上这门“古早”技术。
有什么问题或者想法,欢迎在评论区讨论!
标签:浅谈,为例,void,IShellItem,文件夹,uint,COM,选择器,out From: https://www.cnblogs.com/netlog/p/18423143