现在的位置: 首页 > 综合 > 正文

Obtaining (and managing) file and folder icons using SHGetFileInfo in C#

2018年04月09日 ⁄ 综合 ⁄ 共 14753字 ⁄ 字号 评论关闭

Sample Image - Cap1.gif

Introduction

This article is based upon code from the MSDN Cold Rooster Consulting case study. Included in part of the CRC Rich Client is support for file icons, something I wanted to do myself. This article and classes are the result of my attempts to use the MSDN code in my own application.

The MSDN article explains how functions from Shell32 and User32 were wrapped in detail, but here's a short clip from the article:

"Interoperability with interfaces exposed by COM objects and the .NET Framework is handled via a proxy called the Runtime Callable Wrapper (RCW). The majority of the marshalling work is handled automatically by the .NET Framework.

C-style functions exported from an unmanaged library are a different matter. They are not wrapped automatically because information about the parameters required by the function is not as rich as the information provided by a COM type library. To call C-style functions exported from an unmanaged library, such as the Microsoft Windows® Shell32 API, you use the Platform Invocation Services (PInvoke)..."

The code was left largely unchanged from the original article, although only SHGetFileInfo and DestroyIcon were retained.

I personally found it quite hard to incorporate the MSDN code in my own application and after a few hours of wrestling with the masses of code and still getting errors when trying to build my own project I decided I would try and build up some classes around the Shell32 and User32 wrapped functions that I could use myself.

After looking back at the MSDN article the architecture of my solution and theirs is pretty similar, however I found it easier to develop my own classes and incorporate them into my own project.

This article explains how I modified the MSDN article's code so that it can be used to retrieve icons as a stand-alone class in the form of the IconReader type, and then the IconListManager type which can be used to maintain ImageLists of file icons. It shields you from having to call the IconReader type's members directly, instead adding file icons to specified image lists. To prevent icons for the same file type being added more than once, a HashTable is used to store the file's extension at the time of adding the icon to the ImageList.

Top Level View

The end result is two classes which make use of .NET's Interoperability to call the Win32 API to obtain icons for specified files and or folders. The IconReader class enables the caller to obtain icons directly (which may be all you need). However, an IconListManager class is then created which maintains icons within two ImageList types and shields you from retrieving icons directly.

A couple of additional enumerations were also included to make the library a little more .NET-esque.

IconReader - GetFileIcon Explanation

GetFileIcon is used to obtain icons for files, and uses three parameters:

  • name - Complete file and path names to read.
  • size - Whether to obtain 16x16 or 32x32 pixels, uses the IconSize enumeration.
  • linkOverlay - Specify whether the returned icon should include the small link overlay.

It is a static member function since it doesn't need to store any state, and is intended primarily as an added layer of abstraction. If I needed to obtain a file's icon in the future (and not store it in an ImageList etc.) then I could do so using this class. Once I had a type that wrapped up the necessary API functions to obtain file icons I would then build another type to manage large and small ImageLists that would enable me to make a single call to add an icon, and if it was already added, return the index that the icon was in the ImageList.

<font style="BACKGROUND-COLOR: #c0c0c0"><font style="BACKGROUND-COLOR: #ffffff"><font color="#0000ff"><span class="cs-keyword">public</span> <span class="cs-keyword">static</span> System.Drawing.Icon GetFileIcon(<span class="cs-keyword">string</span> name, IconSize size, 
                                              <span class="cs-keyword">bool</span> linkOverlay)
{
    Shell32.SHFILEINFO shfi = <span class="cs-keyword">new</span> Shell32.SHFILEINFO();
    <span class="cs-keyword">uint</span> flags = Shell32.SHGFI_ICON | Shell32.SHGFI_USEFILEATTRIBUTES;
 
    <span class="cs-keyword">if</span> (<span class="cs-keyword">true</span> == linkOverlay) flags += Shell32.SHGFI_LINKOVERLAY;
 
 
    <span class="cs-comment">/* Check the size specified for return. */</span>
    <span class="cs-keyword">if</span> (IconSize.Small == size)
    {
        flags += Shell32.SHGFI_SMALLICON ; <span class="cs-comment">// include the small icon flag</span>
    } 
    <span class="cs-keyword">else</span> 
    {
        flags += Shell32.SHGFI_LARGEICON ;  <span class="cs-comment">// include the large icon flag</span>
    }
 
    Shell32.SHGetFileInfo( name, 
        Shell32.FILE_ATTRIBUTE_NORMAL, 
        <span class="cs-keyword">ref</span> shfi, 
        (<span class="cs-keyword">uint</span>) System.Runtime.InteropServices.Marshal.SizeOf(shfi), 
        flags );
 
 
    <span class="cs-comment">// Copy (clone) the returned icon to a new object, thus allowing us </span>
    <span class="cs-comment">// to call DestroyIcon immediately</span>
    System.Drawing.Icon icon = (System.Drawing.Icon)
                         System.Drawing.Icon.FromHandle(shfi.hIcon).Clone();
    User32.DestroyIcon( shfi.hIcon ); <span class="cs-comment">// Cleanup</span>
    <span class="cs-keyword">return</span> icon;</font></font>
</font><font style="BACKGROUND-COLOR: #ffffff" color="#0000ff">}</font>

Firstly, a SHFILEINFO structure is created from the following definition:

<font color="#0000ff">[StructLayout(LayoutKind.Sequential)]
<span class="cs-keyword">public</span> <span class="cs-keyword">struct</span> SHFILEINFO
{ 
    <span class="cs-keyword">public</span> <span class="cs-keyword">const</span> <span class="cs-keyword">int</span> NAMESIZE = <span class="cs-literal">80</span>;
    <span class="cs-keyword">public</span> IntPtr hIcon; 
    <span class="cs-keyword">public</span> <span class="cs-keyword">int</span> iIcon; 
    <span class="cs-keyword">public</span> <span class="cs-keyword">uint</span> dwAttributes; 
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=MAX_PATH)]
    <span class="cs-keyword">public</span> <span class="cs-keyword">string</span> szDisplayName; 
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=NAMESIZE)]
    <span class="cs-keyword">public</span> <span class="cs-keyword">string</span> szTypeName; 
};</font>

The SHFILEINFO struct includes an attribute to define a formatted type, which is "a structure or class member annotated with the StructLayoutAttribute to ensure predictable layout information to its members." This ensures that the unmanaged code we call receives the struct as it is intended - i.e. in the order that the members are declared. More details on passing structures are on MSDN

Once the SHFILEINFO struct is created, flags are then set specifying how SHGetFileInfo should behave and what type of icon to retrieve. The code for this part is pretty self explanatory.

Once the various parameters have been finalised, its time to call Shell32.SHGetFileInfo. The code for the Shell32 class was written entirely as part of the MSDN article, and so I cannot take credit for it (and so if you would like more info on how this was done I recommend you take a look at the original CRC article). However as a quick example of how simple it is the unmanaged function is declared as:

<font color="#0000ff">DWORD_PTR SHGetFileInfo( LPCTSTR pszPath,
   DWORD dwFileAttributes,
   SHFILEINFO* psfi,
   UINT cbFileInfo,
   UINT uFlags
);</font>

Which translated to managed code is:

<font color="#0000ff">[DllImport(<span class="cpp-string">"Shell32.dll"</span>)]
<span class="cs-keyword">public</span> <span class="cs-keyword">static</span> <span class="cs-keyword">extern</span> IntPtr SHGetFileInfo(
    <span class="cs-keyword">string</span> pszPath,
    <span class="cs-keyword">uint</span> dwFileAttributes,
    <span class="cs-keyword">ref</span> SHFILEINFO psfi,
    <span class="cs-keyword">uint</span> cbFileInfo,
    <span class="cs-keyword">uint</span> uFlags
);</font>

Once the SHFILEINFO struct had been populated, its then time to get the hIcon that points to the file's icon. This hIcon can then be passed as a parameter of System.Drawing.Icon.FromHandle() which returns the file's icon. After looking through the original code I noticed that a DestroyIcon function was also included, so I looked it up on MSDN and found it was used to (funnily enough) "destroys an icon and frees any memory the icon occupied". I decided it would be a good idea to do this immediately after the icon had been retrieved (since this class was intended to be used in any number of ways). The icon could then be cleaned up as soon as necessary by the GC, or stored in an ImageList. If this isn't necessary then please let me know.

Originally, I didn't use the Clone member function to obtain a copy of the icon, and just left it at FromHandle. However, the call to DestroyIcon immediately after then meant that the returned Icon was now useless and generated an exception. Since I thought this class could be used in any number of ways, I decided to stick with a static call which would obtain a copy of the icon, and then call DestroyIcon immediately after. It suited what I needed to do, and this was something different to the original MSDN code.

The function then returns with the specified icon.

IconReader - GetFolderIcon

The code for GetFolderIcon is very similar to GetFileIcon, except that the dwFileAttributes parameter is passed Shell32.FILE_ATTRIBUTE_DIRECTORY as opposed to Shell32.FILE_ATTRIBUTE_NORMAL for files.

It also requires fewer parameters, specifying whether a large or small icon is desired, and whether to retrieve the open or closed version.

IconListManager - Overview

IconListManager was created after I had produced IconReader, and was designed to manage up to two ImageList types with file icons. The type requires itself to be instantiated, and can be passed up to two parameters when constructed - specifying ImageList objects.

Firstly, there are some member fields which are declared as:

    <font color="#0000ff"><span class="cs-keyword">private</span> Hashtable _extensionList = <span class="cs-keyword">new</span> Hashtable();
 
    <span class="cs-comment">//will hold ImageList objects</span>
    <span class="cs-keyword">private</span> System.Collections.ArrayList _imageLists = <span class="cs-keyword">new</span> ArrayList(); 
    <span class="cs-keyword">private</span> IconHelper.IconReader.IconSize _iconSize;
 
    <span class="cs-comment">//flag, used to determine whether to create two ImageLists.</span>
    <span class="cs-keyword">bool</span> ManageBothSizes = <span class="cs-keyword">false</span>;</font>

The HashTable is used to contain a list of extensions that have been added to the ImageList. We only need to store each icon once, so a HashTable can be used to look up whether an extension exists, if so, whereabouts the icon is in the ImageList.

The ArrayList is used to contain references to ImageList objects, this is so that two constructors can be provided. The first allows the caller to manage a single ImageList with a specified size. The second constructor uses two ImageList parameters, allowing the type to manage both large and small icons.

The first constructor looks like:

<font color="#0000ff"><span class="cs-keyword">public</span> IconListManager(System.Windows.Forms.ImageList imageList, 
                       IconReader.IconSize iconSize )
{
    <span class="cs-comment">// Initialise the members of the class that will hold the image list we're</span>
    <span class="cs-comment">// targeting, as well as the icon size (32 or 16)</span>
    _imageLists.Add( imageList );   <span class="cs-comment">// add ImageList reference to the array list</span>
    _iconSize = iconSize;           <span class="cs-comment">// specify what size to retrieve</span>
}</font>

This stores icons only for a single size in a single ImageList.

The second constructor (which fill allow the type to be used for both large and small icons) looks like:

<font color="#0000ff"><span class="cs-keyword">public</span> IconListManager(System.Windows.Forms.ImageList smallImageList, 
                       System.Windows.Forms.ImageList largeImageList )
{
    <span class="cs-comment">//add both our image lists</span>
    _imageLists.Add( smallImageList );
    _imageLists.Add( largeImageList );
 
    <span class="cs-comment">//set flag</span>
    ManageBothSizes = <span class="cs-keyword">true</span>;
}</font>

This adds both ImageList types to the ArrayList, and then sets a flag specifying that calls to IconReader class's member functions should retrieve both sizes. Its not the neatest way to do it, but it worked, and if I have enough time I'll go through and tidy a few things up.

The class has a few internal functions which are used to make the code a little cleaner, the first of which is AddExtension. This adds a file extension to the HashTable, along with a number which is used to hold the icon's position in the ImageList.

AddFileIcon adds a file's icon to the ImageList, and forms the majority of the code for IconListManager:

<font color="#0000ff"><span class="cs-keyword">public</span> <span class="cs-keyword">int</span> AddFileIcon( <span class="cs-keyword">string</span> filePath )
{
    <span class="cs-comment">// Check if the file exists, otherwise, throw exception.</span>
    <span class="cs-keyword">if</span> (!System.IO.File.Exists( filePath )) 
         <span class="cs-keyword">throw</span> <span class="cs-keyword">new</span> System.IO.FileNotFoundException(<span class="cpp-string">"File    does not exist"</span>);
 
    <span class="cs-comment">// Split it down so we can get the extension</span>
    <span class="cs-keyword">string</span>[] splitPath = filePath.Split(<span class="cs-keyword">new</span> Char[] {'.'});
    <span class="cs-keyword">string</span> extension = (<span class="cs-keyword">string</span>)splitPath.GetValue( splitPath.GetUpperBound(<span class="cs-literal">0</span>) );
 
    <span class="cs-comment">//Check that we haven't already got the extension, if we have, then</span>
    <span class="cs-comment">//return back its index</span>
    <span class="cs-keyword">if</span> (_extensionList.ContainsKey( extension.ToUpper() ))
    {
        <span class="cs-comment">// it already exists</span>
        <span class="cs-keyword">return</span> (<span class="cs-keyword">int</span>)_extensionList[extension.ToUpper()]; <span class="cs-comment">//return existing index</span>
    } 
    <span class="cs-keyword">else</span> 
    {
        <span class="cs-comment">// It's not already been added, so add it and record its position.</span>
        <span class="cs-comment">//store current count -- new item's index</span>
        <span class="cs-keyword">int</span> pos = ((ImageList)_imageLists[<span class="cs-literal">0</span>]).Images.Count;
 
        <span class="cs-keyword">if</span> (ManageBothSizes == <span class="cs-keyword">true</span>)
        {
            <span class="cs-comment">//managing two lists, so add it to small first, then large</span>
            ((ImageList)_imageLists[<span class="cs-literal">0</span>]).Images.Add( 
                           IconReader.GetFileIcon( filePath, 
                                                   IconReader.IconSize.Small, 
                                                   <span class="cs-keyword">false</span> ) );
            ((ImageList)_imageLists[<span class="cs-literal">1</span>]).Images.Add(
                           IconReader.GetFileIcon( filePath, 
                                                   IconReader.IconSize.Large, 
                                                   <span class="cs-keyword">false</span> ) );
        } 
        <span class="cs-keyword">else</span>
        {
            <span class="cs-comment">//only doing one size, so use IconSize as specified in _iconSize.</span>
            <span class="cs-comment">//add to image list</span>
            ((ImageList)_imageLists[<span class="cs-literal">0</span>]).Images.Add( 
                           IconReader.GetFileIcon( filePath, 
                                                   _iconSize, <span class="cs-keyword">false</span> ) );
        }
 
        AddExtension( extension.ToUpper(), pos ); <span class="cs-comment">// add to hash table</span>
        <span class="cs-keyword">return</span> pos;                                  <span class="cs-comment">// return its position</span>
    }
}</font>

The code is pretty well covered through comments but works as follows. Firstly, it splits the filePath so that the extension can be obtained (string after the final period - ".", i.e. the string at the highest position in the array). Once this has been done, a check is done on the HashTable to determine whether that extension has already been added. If it has, then return the contents of the HashTable for the given key (the file extension). So, if "TXT" exists, the "TXT" key is looked up and the contents returned, which is the position of the icon in the ImageList.

If it doesn't exist in the HashTable it hasn't been added, so obtain the current count of items (and thus determine the index the new icon will be inserted at). Then, if it's managing both large and small ImageList objects, then call GetFileIcon twice. If it isn't for both sizes, then just retrieve the specified size icon.

Once this has been done, the extension can then be added to the ImageList with its position, and the position then returned to the caller. This position can then be used when adding icons to ListView or TreeView types when specifying the icon index.

ClearList is included in case its necessary to start over,

<font color="#0000ff"><span class="cs-keyword">public</span> <span class="cs-keyword">void</span> ClearLists()
{
    <span class="cs-keyword">foreach</span>( ImageList imageList <span class="cs-keyword">in</span> _imageLists )
    {
        imageList.Images.Clear(); <span class="cs-comment">//clear current imagelist.</span>
    }
 
    _extensionList.Clear(); <span class="cs-comment">//empty hashtable of entries too.</span>
}</font>

Firstly it iterates through the ArrayList and clears the respective ImageList, and then clears the HashTable that contained the file extensions.

That covers the classes. I had originally wanted to produce a FileIconImageList control that derived from ImageList. This would have incorporated the functionality that IconListManager did, but would have been a slightly neater way of doing it (i.e. instantiate an ImageList, and then call AddFileIcon to add the icon like with IconListManager). However, when I tried this I found I couldn't derive from ImageList and so this wasn't possible. Producing IconListManager was the next best thing I could do.

In the end, a calling application only needs to create an object of type IconListManager, pass it the ImageList references you are using, and then use the AddFileIcon method. I haven't yet added an AddFolderIcon member, since there are only a couple of folder icons (and they would probably go in a separate ImageList to file icons) the calls to obtain them could be made directly from IconReader. However, if this is something people would like added its very easy to do.

The demo application shows how to use the classes, and includes a ListView and Button. When you click the Button anOpenFileDialog is displayed. The filename is then retrieved, and the icon added to the ListView. The snippet below gives you the basic code. Note that I set color depth to 32-bit to ensure support for alpha channel smoothing.

<font color="#0000ff"><span class="cs-keyword">public</span> <span class="cs-keyword">class</span> Form1 : System.Windows.Forms.Form
{
    <span class="cs-keyword">private</span> ImageList _smallImageList = <span class="cs-keyword">new</span> ImageList();
    <span class="cs-keyword">private</span> ImageList _largeImageList = <span class="cs-keyword">new</span> ImageList();
    <span class="cs-keyword">private</span> IconListManager _iconListManager;
 
    .
    .
    .
 
    <span class="cs-keyword">public</span> Form1()
    {
        <span class="cs-comment">//</span>
        <span class="cs-comment">// Required for Windows Form Designer support</span>
        <span class="cs-comment">//</span>
        InitializeComponent();
 
        _smallImageList.ColorDepth = ColorDepth.Depth32Bit;
        _largeImageList.ColorDepth = ColorDepth.Depth32Bit;
 
        _smallImageList.ImageSize = <span class="cs-keyword">new</span> System.Drawing.Size( <span class="cs-literal">16</span>, <span class="cs-literal">16</span> );
        _largeImageList.ImageSize = <span class="cs-keyword">new</span> System.Drawing.Size( <span class="cs-literal">32</span>, <span class="cs-literal">32</span> );
        _iconListManager = <span class="cs-keyword">new</span> IconListManager( _smallImageList, _largeImageList );
        listView1.SmallImageList = _smallImageList;
        listView1.LargeImageList = _largeImageList;
    }
    .
    .
    .
    <span class="cs-keyword">private</span> <span class="cs-keyword">void</span> addButton_Click(<span class="cs-keyword">object</span> sender, System.EventArgs e)
    {
        OpenFileDialog dlgOpenFile = <span class="cs-keyword">new</span> OpenFileDialog();
        <span class="cs-keyword">if</span>(dlgOpenFile.ShowDialog() == DialogResult.OK)
        {
            listView1.Items.Add( dlgOpenFile.FileName, 
                             _iconListManager.AddFileIcon( dlgOpenFile.FileName ) );
        }
    }</font>

Important Notes

It's taken me a long time to figure this out, but gave me real grief at one point. Windows XP introduced Visual Styles, enabling you to use icons with alpha channel blending to produce nice smooth icons. However, to include support for this you must include a manifest file. Without one, you get a really ugly black border. For more information on including visual styles support, you ought to read the MSDN Article "Using Windows XP Visual Styles With Controls on Windows Forms". As I said, I forgot to include a manifest and it drove me crazy for weeks.

Thanks

Well this is my first article to CodeProject (finally), although I've not been a registered member here long I've been a quiet lurking one, and even used CodeGuru in the good old days for my MFC learning. I'm not a massively accomplished programmer, but I hope this has been of help to you. Reading file icons is something I've noticed being mentioned a few times on the MS Newsgroups, and so the included classes should help you on your way.

If you have any questions about this article (particularly if I've done something in a bad way), please feel free to email me.



About Paul Ingles

I graduated with a first class BSc honours degree in a Computer Science/Management Science hybrid from Loughborough University. I live in London and currently work as a .NET Developer in central London.

I'm also currently blogging my progress at further developing my articles into commercial components. Read my blog here.

I've also recently started dabbling at digital photography, and digital re-touching, and developing small simple multiplayer games in Flash.

Click here to view Paul Ingles's online profile.

 

【上篇】
【下篇】

抱歉!评论已关闭.