首页 > 编程语言 >浅谈在C#中调用COM组件——以文件夹选择器为例

浅谈在C#中调用COM组件——以文件夹选择器为例

时间:2024-09-20 19:26:08浏览次数:1  
标签:浅谈 为例 void IShellItem 文件夹 uint COM 选择器 out

浅谈在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组件的基本流程可以总结为以下几步:

  1. 导入COM接口:使用[ComImport][Guid]等特性引入COM接口。
  2. 创建COM对象:通过接口的实现类实例化COM对象。
  3. 调用方法:通过接口调用COM的功能。
  4. 释放资源:由于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接口定义

接下来我们看看与IFileOpenDialogIShellItem相关的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方法来显示对话框,SetOptionsGetOptions来设置和获取对话框选项,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

相关文章

  • 快速上手高德JS API——以可视化公交站点线路为例
    前言在利用高德地图进行开发时,我们经常需要使用不同的API来实现特定的功能。为了帮助开发者快速定位所需API并掌握正确的使用方法,本文将以可视化任意公交站点路线为例,分享相关经验。根据需求粗略匹配参考示例在开始写代码我都会思考一下该功能的实现逻辑是什么:1、通过什么方式......
  • STM32寄存器,标准库和HAL库编程(浅谈)
    寄存器编程、标准库编程和HAL库编程在嵌入式系统开发中是三种常见的编程方式,它们主要区别在于抽象层次、灵活性和开发效率。以下是对这三种编程方式的详细比较:1.寄存器编程(Register-LevelProgramming)寄存器编程是指直接操作微控制器或处理器的硬件寄存器,以控制外设和执行......
  • el-cascader 级联选择器 选中范围扩大方式
    遇到的问题:el-cascader打开的选择框只能点击圆点才能被选中,点击文字并不能选中通过组件库配置项popper-class来实现class上面写样式:注意不能是scoped的样式隔离,<stylerel="stylesheet/scss"lang="scss">.cascaderCla{color:red;&.el-radio{......
  • Ant select 选择器 地址联动 多层包裹
    //省<a-selectv-model:value="formState.address"style="width:180px;margin-right:20px":options="province.map((prov)=>({value:prov.name}))"......
  • 浅谈解释型语言 PHP 和编译型语言 Go 特性
    浅谈解释型语言PHP和编译型语言Go特性分享人:zxy_coding时长:40min写在前面本次分享的目的旨在互相交流,欢迎会后大家多多讨论交流。不会花过多的时间在细节上,同时请各位大佬轻喷。在分享之前,请允许我简单的带大家温习下一些会提到的点:高级语言vs低级语言:这两者是一......
  • 基于QGIS 3.16.0 的OSM路网矢量范围裁剪实战-以湖南省为例
    目录前言一、相关数据介绍1、OMS路网数据2、路网数据3、路网图层属性 二、按省域范围进行路网裁剪1、裁剪范围制定2、空间裁剪  3、裁剪结果 三、总结前言        改革开放特别是党的十八大以来,我国公路发展取得了举世瞩目的成就。国家高速公路网由“7射、11纵、18横......
  • 浅谈红外测温技术在变电站运维中的应用
    0引言随着市场经济的繁荣发展,社会对电力的需求持续增长。城市供电网络的规模和用电设备的总量也在不断扩大,这导致城市电力系统中潜在的网络安全隐患日益增多。作为电力系统核心组成部分的变压器,其安全、稳定的工作直接关系到电能的质量和供应的稳定性。红外检测温度技术能够在......
  • 浅谈pSLC ,鱼和熊掌如何兼得
    浅谈pSLC,鱼和熊掌如何兼得一、什么是pSLCpSLC(Pseudo-SingleLevelCell)即伪SLC,是一种将MLC/TLC改为SLC的一种技术,现NandFlash基本支持此功能,可以通过指令控制MLC进入pSCL模式,存储时在MLC的每个单元中仅存储1bit数据,使MLC拥有SLC的性能,同时具有MLC的性价......
  • 【浅谈pSLC ,鱼和熊掌如何兼得】
    ......
  • 浅谈OpenAI GPT4o 的使用
      OpenAI-o1的首次总结在阅读了OpenAI的出版物后,我对其本质特点进行了总结,并得出了以下结论:1.复杂问题的推理能力显著提升:OpenAI-o1在处理复杂问题时表现出色,尤其在逻辑任务方面。2.定期更新和改进:通过不断的训练,模型学会完善自己的思维过程,尝试不同的策略,并识别和......