Extracting shell icons and thumbnails (WPF)

** Requires Windows Vista / Server 2008 or above.

Introduction of IShellItemImageFactory interface in Windows Vista greatly simplified icon and thumbnail extractions. This article will explain the process of extraction using C# and WPF.

Windows API Declarations

We first need to declare the IShellItemImageFactory interface and its associated flags & struct:

[Flags]
public enum SIIGBF
{
SIIGBF_RESIZETOFIT = 00,
SIIGBF_BIGGERSIZEOK = 01,
SIIGBF_MEMORYONLY = 02,
SIIGBF_ICONONLY = 04,
SIIGBF_THUMBNAILONLY = 08,
SIIGBF_INCACHEONLY = 10
}

[StructLayout(LayoutKind.Sequential)]
public struct SIZE
{
public int cx;
public int cy;
public SIZE(int x, int y)
{
cx = x;
cy = y;
}
}

[ComImportAttribute()]
[GuidAttribute("bcc18b79-ba16-442f-80c4-8a59c30c463b")]
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
public interface IShellItemImageFactory
{
void GetImage(
[In, MarshalAs(UnmanagedType.Struct)] SIZE size,
[In] SIIGBF flags,
[Out] out IntPtr phbm);
}

Copied to clipboard!

We will make an API call to shell using SHCreateItemFromParsingName function to obtain the IShellItemImageFactory interface for each shell item:

[DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
[return: MarshalAs(UnmanagedType.Interface)]
public static extern IShellItemImageFactory SHCreateItemFromParsingName(
[In][MarshalAs(UnmanagedType.LPWStr)] string pszPath,
[In] IntPtr pbc,
[In][MarshalAs(UnmanagedType.LPStruct)] Guid riid);

The GetImage method of IShellItemImageFactory returns a bitmap pointer. We will use DeleteObject method to free this resource once we are done with it.

[DllImport("gdi32.dll", EntryPoint = "DeleteObject")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DeleteObject([In] IntPtr hObject);

Extracting the image

Before we can extract the image, we first need to obtain the IShellItemImageFactory interface of the shell item. We will do this by calling the SHCreateItemFromParsingName function. This method takes three arguments:

  • PCWSTR: This is the parsing name for the shell item. It generally corresponds with file and folder paths (e.g. ‘c:\NewFolder\readme.txt’), but you can also pass CSIDLs (e.g. ‘::{5399E694-6CE5-4D6C-8FCE-1D8870FDCBA0}’ for Control Panel folder)
  • IBindCtx This argument is used for passing binding context information. It is not relevant to us, so we will always use an IntPtr.Zero for this argument.
  • REFIID: We will pass the GUID for the interface we are trying to obtain, in this case the GUID for IShellItemImageFactory.

In my XAML code behind file I am going to create a GetIcon method that takes a file path as its only argument (we will expand this function later) and returns a ImageSource object:

private ImageSource GetIcon(string parsingName)
{
IShellItemImageFactory isiif = null;
Guid iid = typeof(IShellItemImageFactory).GUID;
try
{
isiif = NativeMethods.SHCreateItemFromParsingName(parsingName, IntPtr.Zero, iid);
if (isiif == null) return;
}
catch (Exception e)
{
Console.WriteLine(
"Failed to obtain the IShellItemImageFactory interface using for "
+ parsingName, e);
return ;
}

If our call is successful we should have a pointer to the IShellItemImageFactory interface which we can use to extract the item icon or thumbnail. IShellItemImageFactory’s GetImage method takes three arguments:

  • SIZE: Specifies the size of the image to be extracted. In our samples we will use 256×256 pixels
  • SIIGBF: We will initially use only the SIIGBF_BIGGERSIZEOK flag which ensures that the request image is not resized by the shell.
  • HBITMAP: Handle to the returned bitmap

Here is the continuation of our GetIcon method. We’ll declare our variables and make the GetImage call using the IShellItemImageFactory interface. And let’s not forget to free the IShellItemImageFactory interface once we are done:

SIIGBF flags = SIIGBF.SIIGBF_BIGGERSIZEOK;
IntPtr hBitmap=IntPtr.Zero;
try
{
isiif.GetImage(new SIZE(256, 256), flags, out hBitmap);
}
catch (Exception e)
{
Console.WriteLine(
"GetImage call failed for "
+ parsingName, e);
}

Marshal.FinalReleaseComObject(isiif);

if (hBitmap == IntPtr.Zero) return;

At this point we should have a pointer to a memory bitmap which we can use it to create a WPF ImageSource (and delete the memory bitmap):

try
{
imgSource = Imaging.CreateBitmapSourceFromHBitmap(
hBitmap,
IntPtr.Zero,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
imgSource.Freeze();
}
catch (Exception ex)
{
Console.WriteLine("Get Icon Failed", ex);
}

NativeMethods.DeleteObject(hBitmap);

That is it. We can then assign the ImageSource to an XAML Image control.

Extending our GetIcon method

You will find that if you try to extract an icon for a shell object (which presumably has an associated IThumbnailProvider handler) such as a JPG image, the IShellItemImageFactory will return a thumbnail at the specified size instead of its associated icon. To obtain the associated icon you need to add the SIIGBF_ICONONLY to your flags.

Here is the complete modified method:

private ImageSource GetIcon(string parsingName, bool iconOnly)
{
// obtain the IShellItemImageFactory interface
IShellItemImageFactory isiif = null;
Guid iid = typeof(IShellItemImageFactory).GUID;
try
{
isiif = NativeMethods.SHCreateItemFromParsingName(parsingName, IntPtr.Zero, iid);
if (isiif == null) return null;
}
catch (Exception e)
{
Console.WriteLine(
"Failed to obtain the IShellItemImageFactory interface using for "
+ parsingName, e);
return null;
}

// extract memory bitmap
SIIGBF flags = SIIGBF.SIIGBF_BIGGERSIZEOK;
if (iconOnly) flags |= SIIGBF.SIIGBF_ICONONLY;
IntPtr hBitmap = IntPtr.Zero;
ImageSource imgSource = null;
try
{
isiif.GetImage(new SIZE(256, 256), flags, out hBitmap);
}
catch (Exception e)
{
Console.WriteLine(
"GetImage call failed for "
+ parsingName, e);
}
Marshal.FinalReleaseComObject(isiif);
if (hBitmap == IntPtr.Zero) return null;

// create an ImageSource object from HBITMAP
try
{
imgSource = Imaging.CreateBitmapSourceFromHBitmap(
hBitmap,
IntPtr.Zero,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
imgSource.Freeze();
}
catch (Exception ex)
{
Console.WriteLine("Get Icon Failed", ex);
}
NativeMethods.DeleteObject(hBitmap);

return imgSource;
}

Simple icon/thumbnail browser in WPF

Place the following XAML code in your window. This will create a button for browsing for files, a checkbox for requesting icons only and an image control for displaying the extracted image:

<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel VerticalAlignment="Top" Grid.Column="0" >
<Button Width="100" Height="50" Name="btnBrowse" Click="BtnBrowse_Click">Browse</Button>
<CheckBox Name="chkIconOnly">Icon Only</CheckBox>
</StackPanel>
<Image Name="imgIcon" Grid.Column="1" Width="256" Height="256"/>
</Grid>

The following methods extract and display the icon/thumbnail on a background thread. Paste these three methods into your code behind file:

private ImageSource GetIcon(string parsingName, bool iconOnly)
{
// obtain the IShellItemImageFactory interface
IShellItemImageFactory isiif = null;
Guid iid = typeof(IShellItemImageFactory).GUID;
try
{
isiif = NativeMethods.SHCreateItemFromParsingName(parsingName, IntPtr.Zero, iid);
if (isiif == null) return null;
}
catch (Exception e)
{
Console.WriteLine(
"Failed to obtain the IShellItemImageFactory interface using for "
+ parsingName, e);
return null;
}

// extract memory bitmap
SIIGBF flags = SIIGBF.SIIGBF_BIGGERSIZEOK;
if (iconOnly) flags |= SIIGBF.SIIGBF_ICONONLY;
IntPtr hBitmap = IntPtr.Zero;
ImageSource imgSource = null;
try
{
isiif.GetImage(new SIZE(256, 256), flags, out hBitmap);
}
catch (Exception e)
{
Console.WriteLine(
"GetImage call failed for "
+ parsingName, e);
}
Marshal.FinalReleaseComObject(isiif);
if (hBitmap == IntPtr.Zero) return null;

// create an ImageSource object from HBITMAP
try
{
imgSource = Imaging.CreateBitmapSourceFromHBitmap(
hBitmap,
IntPtr.Zero,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
imgSource.Freeze();
}
catch (Exception ex)
{
Console.WriteLine("Get Icon Failed", ex);
}
NativeMethods.DeleteObject(hBitmap);

return imgSource;
}

private void GetIconStart(Dispatcher dispatcher,string path,bool iconOnly)
{
Action addFolder = () => imgIcon.Source = GetIcon(path,iconOnly);
dispatcher.Invoke(addFolder);
}

private void BtnBrowse_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog f = new OpenFileDialog();
if (f.ShowDialog()==true)
{
var dispatcher = Dispatcher.CurrentDispatcher;
bool iconOnly = chkIconOnly.IsChecked.HasValue && chkIconOnly.IsChecked.Value;
ThreadStart start = () =>
{
GetIconStart(dispatcher, f.FileName, iconOnly);
};

var t = new Thread(start);

t.Start();
}
}

Add a class named NativeMethods to your project and paste the following code:

static class NativeMethods
{
[DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
[return: MarshalAs(UnmanagedType.Interface, IidParameterIndex = 2)]
public static extern IShellItemImageFactory SHCreateItemFromParsingName(
[In][MarshalAs(UnmanagedType.LPWStr)] string pszPath,
[In] IntPtr pbc,
[In][MarshalAs(UnmanagedType.LPStruct)] Guid riid);

[DllImport("gdi32.dll", EntryPoint = "DeleteObject")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DeleteObject([In] IntPtr hObject);
}

Finally, add another class to your project and name it IShellItemImageFactory and paste the following code:

[Flags]
public enum SIIGBF
{
SIIGBF_RESIZETOFIT = 00,
SIIGBF_BIGGERSIZEOK = 01,
SIIGBF_MEMORYONLY = 02,
SIIGBF_ICONONLY = 04,
SIIGBF_THUMBNAILONLY = 08,
SIIGBF_INCACHEONLY = 10
}

[StructLayout(LayoutKind.Sequential)]
public struct SIZE
{
public int cx;
public int cy;
public SIZE(int x, int y)
{
cx = x;
cy = y;
}
}

[ComImportAttribute()]
[GuidAttribute("bcc18b79-ba16-442f-80c4-8a59c30c463b")]
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
public interface IShellItemImageFactory
{
void GetImage(
[In, MarshalAs(UnmanagedType.Struct)] SIZE size,
[In] SIIGBF flags,
[Out] out IntPtr phbm);
}

That is it! Once you hit F5 you should have a simple icon/thumbnail extractor.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: