65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (46投票s)

2002 年 7 月 3 日

10分钟阅读

viewsIcon

410187

downloadIcon

18044

本文介绍了如何从 C# 读取文件和文件夹图标,然后构建一个管理类,以便在多达两个 ImageList 对象中维护文件图标。

Sample Image - Cap1.gif

引言

本文基于 MSDN Cold Rooster Consulting 案例研究中的代码。CRC 富客户端的一部分包含对文件图标的支持,这是我自己想要做的事情。本文和类是我尝试在自己的应用程序中使用 MSDN 代码的结果。

MSDN 文章详细解释了 Shell32 和 User32 中的函数是如何封装的,但这里是文章中的一小段摘录:

"与 COM 对象和 .NET 框架公开的接口的互操作性通过一个名为运行时可调用包装器 (RCW) 的代理进行处理。.NET 框架自动处理大部分封送工作。

从非托管库导出的 C 风格函数则不同。它们不会自动封装,因为函数所需的参数信息不像 COM 类型库提供的信息那样丰富。要调用从非托管库(例如 Microsoft Windows® Shell32 API)导出的 C 风格函数,您需要使用平台调用服务 (PInvoke)..."

代码大部分保留了原始文章中的内容,尽管只保留了 SHGetFileInfoDestroyIcon

我个人发现将 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 中,然后将该位置返回给调用者。然后,在指定图标索引时,可以将此位置用于将图标添加到 ListViewTreeView 类型中。

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 调用以获取它们。但是,如果这是人们希望添加的功能,那么它非常容易实现。

演示应用程序展示了如何使用这些类,并包含一个 ListViewButton。当您单击 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 新闻组上注意到被提及几次的事情,因此附带的类应该能帮助您。

如果您对此文章有任何疑问(特别是如果我做得不好),请随时给我发送电子邮件

© . All rights reserved.