在 C# 中使用 SHGetFileInfo 获取(和管理)文件和文件夹图标






4.90/5 (46投票s)
2002 年 7 月 3 日
10分钟阅读

410187

18044
本文介绍了如何从 C# 读取文件和文件夹图标,然后构建一个管理类,以便在多达两个 ImageList 对象中维护文件图标。
引言
本文基于 MSDN Cold Rooster Consulting 案例研究中的代码。CRC 富客户端的一部分包含对文件图标的支持,这是我自己想要做的事情。本文和类是我尝试在自己的应用程序中使用 MSDN 代码的结果。
MSDN 文章详细解释了 Shell32 和 User32 中的函数是如何封装的,但这里是文章中的一小段摘录:
"与 COM 对象和 .NET 框架公开的接口的互操作性通过一个名为运行时可调用包装器 (RCW) 的代理进行处理。.NET 框架自动处理大部分封送工作。
从非托管库导出的 C 风格函数则不同。它们不会自动封装,因为函数所需的参数信息不像 COM 类型库提供的信息那样丰富。要调用从非托管库(例如 Microsoft Windows® Shell32 API)导出的 C 风格函数,您需要使用平台调用服务 (PInvoke)..."
代码大部分保留了原始文章中的内容,尽管只保留了 SHGetFileInfo
和 DestroyIcon
。
我个人发现将 MSDN 代码整合到我自己的应用程序中相当困难,在与大量代码搏斗了几个小时,并且在尝试构建自己的项目时仍然出现错误之后,我决定尝试围绕 Shell32 和 User32 封装的函数构建一些类,以便我自己可以使用。
回顾 MSDN 文章后,我发现我的解决方案和他们的架构非常相似,但是我觉得开发自己的类并将其整合到我自己的项目中更容易。
本文解释了我是如何修改 MSDN 文章中的代码,使其可以用作独立类来检索图标,以 IconReader
类型形式,然后是 IconListManager
类型,该类型可用于维护文件图标的 ImageList。它使您无需直接调用 IconReader
类型的成员,而是将文件图标添加到指定的图像列表中。为了防止同一文件类型的图标被多次添加,使用 HashTable
来存储文件在将图标添加到 ImageList
时的扩展名。
顶层视图
最终结果是两个类,它们利用 .NET 的互操作性来调用 Win32 API,以获取指定文件和/或文件夹的图标。IconReader
类允许调用者直接获取图标(这可能就是您所需要的一切)。但是,随后创建了一个 IconListManager
类,它在两个 ImageList
类型中维护图标,并使您免于直接检索图标。
还包括了几个额外的枚举,以使库更具 .NET 风格。
IconReader - GetFileIcon 解释
GetFileIcon 用于获取文件的图标,它使用三个参数
name
- 要读取的完整文件和路径名。size
- 是否获取 16x16 或 32x32 像素,使用 IconSize 枚举。linkOverlay
- 指定返回的图标是否应包含小的链接覆盖。
它是一个静态成员函数,因为它不需要存储任何状态,并且主要用作额外的抽象层。如果我将来需要获取文件的图标(并且不将其存储在 ImageList 等中),那么我可以使用这个类。一旦我有一个封装了获取文件图标所需 API 函数的类型,我将构建另一个类型来管理大图标和小图标的 ImageList,这将使我能够一次调用即可添加图标,如果它已经添加,则返回图标在 ImageList
中的索引。
public static System.Drawing.Icon GetFileIcon(string name, IconSize size,
bool linkOverlay)
{
Shell32.SHFILEINFO shfi = new Shell32.SHFILEINFO();
uint flags = Shell32.SHGFI_ICON | Shell32.SHGFI_USEFILEATTRIBUTES;
if (true == linkOverlay) flags += Shell32.SHGFI_LINKOVERLAY;
/* Check the size specified for return. */
if (IconSize.Small == size)
{
flags += Shell32.SHGFI_SMALLICON ; // include the small icon flag
}
else
{
flags += Shell32.SHGFI_LARGEICON ; // include the large icon flag
}
Shell32.SHGetFileInfo( name,
Shell32.FILE_ATTRIBUTE_NORMAL,
ref shfi,
(uint) System.Runtime.InteropServices.Marshal.SizeOf(shfi),
flags );
// Copy (clone) the returned icon to a new object, thus allowing us
// to call DestroyIcon immediately
System.Drawing.Icon icon = (System.Drawing.Icon)
System.Drawing.Icon.FromHandle(shfi.hIcon).Clone();
User32.DestroyIcon( shfi.hIcon ); // Cleanup
return icon;
}
首先,从以下定义创建了一个 SHFILEINFO
结构
[StructLayout(LayoutKind.Sequential)]
public struct SHFILEINFO
{
public const int NAMESIZE = 80;
public IntPtr hIcon;
public int iIcon;
public uint dwAttributes;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=MAX_PATH)]
public string szDisplayName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=NAMESIZE)]
public string szTypeName;
};
SHFILEINFO
结构包含一个属性来定义格式化类型,即“使用 StructLayoutAttribute
注释的结构或类成员,以确保其成员具有可预测的布局信息。”这确保了我们调用的非托管代码按预期接收结构——即,按成员声明的顺序。有关传递结构的更多详细信息可在 MSDN 上找到。
创建 SHFILEINFO
结构后,将设置标志,指定 SHGetFileInfo
的行为方式以及要检索的图标类型。这部分代码非常不言自明。
一旦各种参数都已确定,就该调用 Shell32.SHGetFileInfo
了。Shell32
类的代码完全是作为 MSDN 文章的一部分编写的,所以我不能为此居功(因此,如果您想了解更多关于如何完成此操作的信息,我建议您查看原始的 CRC 文章)。但是,作为一个快速示例,说明它是多么简单,非托管函数被声明为
DWORD_PTR SHGetFileInfo( LPCTSTR pszPath,
DWORD dwFileAttributes,
SHFILEINFO* psfi,
UINT cbFileInfo,
UINT uFlags
);
转换为托管代码就是
[DllImport("Shell32.dll")]
public static extern IntPtr SHGetFileInfo(
string pszPath,
uint dwFileAttributes,
ref SHFILEINFO psfi,
uint cbFileInfo,
uint uFlags
);
SHFILEINFO
结构填充完毕后,便可获取指向文件图标的 hIcon
。此 hIcon
可作为 System.Drawing.Icon.FromHandle()
的参数传递,该函数返回文件图标。查阅原始代码后,我注意到还包含了一个 DestroyIcon
函数,于是我在 MSDN 上查找,发现它用于(有趣的是)“销毁图标并释放图标占用的任何内存”。我决定在图标检索后立即执行此操作是个好主意(因为这个类旨在以多种方式使用)。然后,图标可以在必要时立即由 GC 清理,或存储在 ImageList
中。如果不需要,请告诉我。
最初,我没有使用 Clone
成员函数来获取图标的副本,而只是停留在 FromHandle
。然而,紧随其后的 DestroyIcon
调用意味着返回的 Icon
现在无用并产生了异常。由于我认为这个类可以以多种方式使用,所以我决定坚持使用静态调用,它将获取图标的副本,然后立即调用 DestroyIcon
。它适合我需要做的事情,这与原始的 MSDN 代码有所不同。
函数随后返回指定的图标。
IconReader - GetFolderIcon
GetFolderIcon
的代码与 GetFileIcon
非常相似,只是 dwFileAttributes
参数传递的是 Shell32.FILE_ATTRIBUTE_DIRECTORY
,而不是文件的 Shell32.FILE_ATTRIBUTE_NORMAL
。
它也需要更少的参数,指定是需要大图标还是小图标,以及是检索打开版本还是关闭版本。
IconListManager - 概述
IconListManager
是在我编写 IconReader
之后创建的,旨在管理多达两个带有文件图标的 ImageList
类型。该类型需要自身被实例化,并且在构造时可以传入多达两个参数 - 指定 ImageList
对象。
首先,有一些成员字段声明如下:
private Hashtable _extensionList = new Hashtable();
//will hold ImageList objects
private System.Collections.ArrayList _imageLists = new ArrayList();
private IconHelper.IconReader.IconSize _iconSize;
//flag, used to determine whether to create two ImageLists.
bool ManageBothSizes = false;
HashTable
用于包含已添加到 ImageList
的扩展名列表。我们只需要存储每个图标一次,因此可以使用 HashTable
查找扩展名是否存在,如果存在,则查找图标在 ImageList
中的位置。
ArrayList
用于包含对 ImageList
对象的引用,这是为了提供两个构造函数。第一个允许调用者管理具有指定大小的单个 ImageList
。第二个构造函数使用两个 ImageList
参数,允许该类型管理大图标和小图标。
第一个构造函数如下所示:
public IconListManager(System.Windows.Forms.ImageList imageList,
IconReader.IconSize iconSize )
{
// Initialise the members of the class that will hold the image list we're
// targeting, as well as the icon size (32 or 16)
_imageLists.Add( imageList ); // add ImageList reference to the array list
_iconSize = iconSize; // specify what size to retrieve
}
这仅将单一尺寸的图标存储在一个 ImageList
中。
第二个构造函数(它将允许该类型同时用于大图标和小图标)如下所示:
public IconListManager(System.Windows.Forms.ImageList smallImageList,
System.Windows.Forms.ImageList largeImageList )
{
//add both our image lists
_imageLists.Add( smallImageList );
_imageLists.Add( largeImageList );
//set flag
ManageBothSizes = true;
}
这将两个 ImageList
类型都添加到 ArrayList
中,然后设置一个标志,指定对 IconReader
类成员函数的调用应检索两种尺寸。这不是最简洁的方法,但它奏效了,如果我有足够的时间,我会整理一些东西。
该类有一些内部函数用于使代码更清晰,其中第一个是 AddExtension
。它将文件扩展名添加到 HashTable
中,以及一个数字,用于保存图标在 ImageList
中的位置。
AddFileIcon
将文件图标添加到 ImageList
,并构成 IconListManager
的大部分代码。
public int AddFileIcon( string filePath )
{
// Check if the file exists, otherwise, throw exception.
if (!System.IO.File.Exists( filePath ))
throw new System.IO.FileNotFoundException("File does not exist");
// Split it down so we can get the extension
string[] splitPath = filePath.Split(new Char[] {'.'});
string extension = (string)splitPath.GetValue( splitPath.GetUpperBound(0) );
//Check that we haven't already got the extension, if we have, then
//return back its index
if (_extensionList.ContainsKey( extension.ToUpper() ))
{
// it already exists
return (int)_extensionList[extension.ToUpper()]; //return existing index
}
else
{
// It's not already been added, so add it and record its position.
//store current count -- new item's index
int pos = ((ImageList)_imageLists[0]).Images.Count;
if (ManageBothSizes == true)
{
//managing two lists, so add it to small first, then large
((ImageList)_imageLists[0]).Images.Add(
IconReader.GetFileIcon( filePath,
IconReader.IconSize.Small,
false ) );
((ImageList)_imageLists[1]).Images.Add(
IconReader.GetFileIcon( filePath,
IconReader.IconSize.Large,
false ) );
}
else
{
//only doing one size, so use IconSize as specified in _iconSize.
//add to image list
((ImageList)_imageLists[0]).Images.Add(
IconReader.GetFileIcon( filePath,
_iconSize, false ) );
}
AddExtension( extension.ToUpper(), pos ); // add to hash table
return pos; // return its position
}
}
代码通过注释已经很好地涵盖了,但其工作原理如下。首先,它会分割 filePath
以获取扩展名(最后一个句点 "." 之后的字符串,即数组中位置最高的字符串)。完成此操作后,会对 HashTable
进行检查,以确定该扩展名是否已添加。如果已添加,则返回给定键(文件扩展名)的 HashTable
内容。因此,如果“TXT”存在,则查找“TXT”键并返回其内容,即图标在 ImageList
中的位置。
如果它在 HashTable
中不存在,则表示它尚未添加,因此获取当前项目计数(并由此确定新图标将插入的索引)。然后,如果它同时管理大图标和小图标的 ImageList
对象,则调用 GetFileIcon
两次。如果不是两种大小,则只检索指定大小的图标。
完成此操作后,可以将扩展名及其位置添加到 ImageList
中,然后将该位置返回给调用者。然后,在指定图标索引时,可以将此位置用于将图标添加到 ListView
或 TreeView
类型中。
ClearList
包含在内,以防需要重新开始,
public void ClearLists()
{
foreach( ImageList imageList in _imageLists )
{
imageList.Images.Clear(); //clear current imagelist.
}
_extensionList.Clear(); //empty hashtable of entries too.
}
首先,它遍历 ArrayList
并清除相应的 ImageList
,然后清除包含文件扩展名的 HashTable
。
这涵盖了这些类。我最初想创建一个派生自 ImageList
的 FileIconImageList 控件。这将包含 IconListManager
所做的功能,但会是一种稍微更简洁的方法(即实例化一个 ImageList,然后像使用 IconListManager
一样调用 AddFileIcon 来添加图标)。但是,当我尝试这样做时,我发现我无法从 ImageList
派生,因此这不可行。创建 IconListManager
是我能做的次优选择。
最后,调用应用程序只需创建一个 IconListManager
类型的对象,将您正在使用的 ImageList
引用传递给它,然后使用 AddFileIcon
方法。我还没有添加 AddFolderIcon
成员,因为只有几个文件夹图标(而且它们可能会放在与文件图标不同的 ImageList 中),所以可以直接从 IconReader
调用以获取它们。但是,如果这是人们希望添加的功能,那么它非常容易实现。
演示应用程序展示了如何使用这些类,并包含一个 ListView
和 Button
。当您单击 Button
时,将显示一个 OpenFileDialog
。然后检索文件名,并将图标添加到 ListView
。下面的代码片段给出了基本代码。请注意,我将颜色深度设置为 32 位,以确保支持 alpha 通道平滑。
public class Form1 : System.Windows.Forms.Form
{
private ImageList _smallImageList = new ImageList();
private ImageList _largeImageList = new ImageList();
private IconListManager _iconListManager;
.
.
.
public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
_smallImageList.ColorDepth = ColorDepth.Depth32Bit;
_largeImageList.ColorDepth = ColorDepth.Depth32Bit;
_smallImageList.ImageSize = new System.Drawing.Size( 16, 16 );
_largeImageList.ImageSize = new System.Drawing.Size( 32, 32 );
_iconListManager = new IconListManager( _smallImageList, _largeImageList );
listView1.SmallImageList = _smallImageList;
listView1.LargeImageList = _largeImageList;
}
.
.
.
private void addButton_Click(object sender, System.EventArgs e)
{
OpenFileDialog dlgOpenFile = new OpenFileDialog();
if(dlgOpenFile.ShowDialog() == DialogResult.OK)
{
listView1.Items.Add( dlgOpenFile.FileName,
_iconListManager.AddFileIcon( dlgOpenFile.FileName ) );
}
}
重要说明
我花了很多时间才弄明白这一点,一度让我非常苦恼。Windows XP 引入了视觉样式,使您可以使用带有 alpha 通道混合的图标来生成漂亮的平滑图标。但是,要包含对此的支持,您必须包含一个清单文件。如果没有,您会得到一个非常丑陋的黑色边框。有关包含视觉样式支持的更多信息,您应该阅读 MSDN 文章“在 Windows 窗体上将 Windows XP 视觉样式与控件结合使用”。正如我所说,我忘记包含清单,这让我发疯了好几周。
谢谢
嗯,这是我第一篇 CodeProject 文章(终于),虽然我在这里注册会员时间不长,但我一直是一个潜水者,甚至在以前学习 MFC 的美好时光里使用过 CodeGuru。我不是一个非常优秀的程序员,但我希望这能对您有所帮助。读取文件图标是我在 MS 新闻组上注意到被提及几次的事情,因此附带的类应该能帮助您。
如果您对此文章有任何疑问(特别是如果我做得不好),请随时给我发送电子邮件。