Create a Thumbnail Provider with C# using IExtractImage

Posted in Uncategorized by walkingTarget on February 21, 2011 1 Comment

As far back as Windows 2000, Microsoft has offered interfaces for developers to extend the behavior of the Windows shell. Implementing these interfaces delivers a variety of functions for new and old file types within Windows Explorer – context menus, icons, custom tooltips, etc. In this article, we’ll cover a bit of the topic and use C# to implement a Thumbnail Handler for a custom file type in Windows XP.

Maybe you’re wondering, “Why Windows XP in 2011?” First of all, Windows XP is still operated by the majority of Windows users. Also, Windows Vista and Windows 7 introduce a new thumbnail handler interface that is not compatible with Windows XP. Fortunately, the Thumbnail Handler interface available for Windows XP is supported on newer versions of Windows.

The interface I’m talking about is IExtractImage. As you can see from the MSDN article, there isn’t a whole of information on this. The Internet ether is also lacking on examples of how to implement this interface in C#. However, I had a project at the office that didn’t lend itself to an unmanaged language. So despite the shortcomings and with the help of the PInvoke.net and a number of articles, I cobbled together a working thumbnail handler from IExtractImage in C# (we’ll discuss the limitations at the end of this article).

Lets start by creating a new project in whatever flavor of Visual Studio you prefer. Create a new Class Library project, give it a name and click OK. Open the Project Properties and click the “Assembly Information” button. Check the “Make assembly COM-Visible” checkbox (I noticed later that I forgot to do this the first time, so it may not matter). I also changed the target framework of the project to .NET 2.0.

Now that our project is set up, add a new code file named IExtractImage.cs. This will contain all the definitions of the COM interfaces and objects we’ll be using in our implementation. The code listed here should be reusable enough to work as the base data for any IExtractImage project, so just copy and paste the following into your new file:

using System;
using System.Text;
using System.Runtime.InteropServices;

namespace IExtractImage_Example
{
    public enum IEIFLAG
    {
        ASYNC = 0x0001, // ask the extractor if it supports ASYNC extract (free threaded)
        CACHE = 0x0002, // returned from the extractor if it does NOT cache the thumbnail
        ASPECT = 0x0004, // passed to the extractor to beg it to render to the aspect ratio of the supplied rect
        OFFLINE = 0x0008, // if the extractor shouldn't hit the net to get any content neede for the rendering
        GLEAM = 0x0010, // does the image have a gleam ? this will be returned if it does
        SCREEN = 0x0020, // render as if for the screen (this is exlusive with IEIFLAG_ASPECT )
        ORIGSIZE = 0x0040, // render to the approx size passed, but crop if neccessary
        NOSTAMP = 0x0080, // returned from the extractor if it does NOT want an icon stamp on the thumbnail
        NOBORDER = 0x0100, // returned from the extractor if it does NOT want an a border around the thumbnail
        QUALITY = 0x0200 // passed to the Extract method to indicate that a slower, higher quality image is desired, re-compute the thumbnail
    }

    /// <summary>
    /// The SIZE structure specifies the width and height of a rectangle.
    /// </summary>
    [StructLayout(LayoutKind.Sequential)]
    public struct SIZE
    {
        /// <summary>
        /// Specifies the rectangle's width. The units depend on which function uses this.
        /// </summary>
        public int cx;

        /// <summary>
        /// Specifies the rectangle's height. The units depend on which function uses this.
        /// </summary>
        public int cy;

        /// <summary>
        /// Simple constructor for SIZE structs.
        /// </summary>
        /// <param name="cx">The initial width of the SIZE structure.</param>
        /// <param name="cy">The initial height of the SIZE structure.</param>
        public SIZE(int cx, int cy)
        {
            this.cx = cx;
            this.cy = cy;
        }
    }

    /// <summary>
    /// Exposes methods that request a thumbnail image from a Shell folder.
    /// </summary>
    [ComImportAttribute()]
    [GuidAttribute("BB2E617C-0920-11d1-9A0B-00C04FC2D6C1")]
    [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
    interface IExtractImage
    {
        /// <summary>
        /// Gets a path to the image that is to be extracted.
        /// </summary>
        /// <param name="pszPathBuffer">The buffer used to return the path description. This value identifies the image so you can avoid loading the same one more than once.</param>
        /// <param name="cch">The size of pszPathBuffer in characters.</param>
        /// <param name="pdwPriority">Not used.</param>
        /// <param name="prgSize">A pointer to a SIZE structure with the desired width and height of the image. Must not be NULL.</param>
        /// <param name="dwRecClrDepth">The recommended color depth in units of bits per pixel. Must not be NULL.</param>
        /// <param name="pdwFlags">Flags that specify how the image is to be handled.</param>
        [PreserveSig]
        long GetLocation(out StringBuilder pszPathBuffer, int cch, ref int pdwPriority, ref SIZE prgSize, int dwRecClrDepth, ref int pdwFlags);

        /// <summary>
        /// Requests an image from an object, such as an item in a Shell folder.
        /// </summary>
        /// <param name="phBmpThumbnail">The buffer to hold the bitmapped image.</param>
        [PreserveSig]
        long Extract(out IntPtr phBmpThumbnail);
    }
}

Next, we’re going to create the class that will implement our interfaces. If you took a look at the MSDN article for IExtractImage, you’ll remember we also need to implement the IPersistFile interface. We also need to generate a unique GUID to identify our class in COM. If you’re using Visual Studio, a GUID generator should be in the Tools menu. If not, www.guidgenerator.com does a perfectly fine job. Here is the sample code I cooked up as an example of what your implementation might look like:

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Drawing;

namespace IExtractImage_Example
{
    [ComVisible(true), ClassInterface(ClassInterfaceType.None)]
    [ProgId("IExtractImage_Example.ExtractImage"), Guid("7CA3151C-2F5C-11E0-B2B8-B039E0D72085")]
    public class ExtractImage : IExtractImage, IPersistFile
    {
        #region ExtractImage Private Fields

        private Size m_size = Size.Empty;
        private string m_filename = String.Empty;

        #endregion

        private const long S_OK = 0x00000000L;
        private const long E_PENDING = 0x8000000AL;

        #region IExtractImage Members
        public long GetLocation(out StringBuilder pszPathBuffer, int cch, ref int pdwPriority, ref SIZE prgSize, int dwRecClrDepth, ref int pdwFlags)
        {
            pszPathBuffer = new StringBuilder();
            pszPathBuffer.Append(m_filename);
            m_size = new Size(prgSize.cx, prgSize.cy);

            if (((IEIFLAG)pdwFlags & IEIFLAG.ASYNC) != 0)
                return E_PENDING;

            return S_OK;
        }

        public long Extract(out IntPtr phBmpThumbnail)
        {
            Bitmap bmp = new Bitmap(m_size.Width, m_size.Height);

            using (Graphics g = Graphics.FromImage(bmp))
            {
                using (Pen p = new Pen(Color.Black))
                {
                    g.Clear(Color.White);
                    g.DrawLine(p, 0, 0, m_size.Width, m_size.Height);
                }
            }
            phBmpThumbnail = bmp.GetHbitmap();

            return S_OK;
        }
        #endregion

        #region IPersistFile Members
        public void GetClassID(out Guid pClassID)
        {
            throw new NotImplementedException();
        }

        public void GetCurFile(out string ppszFileName)
        {
            throw new NotImplementedException();
        }

        public int IsDirty()
        {
            throw new NotImplementedException();
        }

        public void Load(string pszFileName, int dwMode)
        {
            m_filename = pszFileName;
        }

        public void Save(string pszFileName, bool fRemember)
        {
            throw new NotImplementedException();
        }

        public void SaveCompleted(string pszFileName)
        {
            throw new NotImplementedException();
        }
        #endregion
    }
}

Once you’ve got your class written, build your project and it should be ready to be registered. Since we’re using a .NET, we’re going to have to use Regasm to register our .NET assembly for COM Interop. You can do this using the regasm.exe tool that comes with Visual Studio, or you can create an installer that does the work for you. I opened a Visual Studio Command Prompt and ran regasm /codebase /regfile to generate the information I needed to create an NSIS installer that does all the work.

Now that COM is ready to accept access our assembly there’s just a little more left to do. Even though COM can get at our implementation, we still haven’t told Windows that there’s a new Thumbnail Handler in town. To do this, we are going to edit registry again. Under HKEY_CLASSES_ROOT, we’re going to add or change the key for a file extension and add the interface’s GUID underneath the shellex its key. You saw the GUID earlier, actually, in the interface definition in IExtractImage.cs. To say it graphically, we need to create a key like this:

HKEY_CLASSES_ROOT
|----<File Extension>
    |-----shellex
         |-----{BB2E617C-0920-11D1-9A0B-00C04FC2D6C1}
              |-----(Default) = {<Our Class Guid>}

As soon as you make this change, thumbnails should start showing up once you open a folder containing your custom file type. If not, then the the source of the problem may be difficult to find. While debugging my work, I used the Task Manager to kill explorer.exe when things went wrong. Just launch a new instance of explorer.exe right after that. This will also ensure that the new extension is loaded by the shell, just to be safe. One problem that you might run into if you’re using a 64-bit version of Windows, is that your bitmap handle is 32-bit. Cast this to a 64-bit pointer before you assign it to phBmpImage. I found some fellow agonizing developers at this site.

Finally, lets talk about the problems with writing this extension in managed code. First and foremost, you’ll be annoyed when your extensions DLL file gets locked because it’s in use by Windows Explorer. This is because the project is written using managed code. Unlike an unmanaged project, managed code uses managed memory which is handled by the non-deterministic Garbage Collector. Its way of marshaling memory means that you don’t know when your class will be unloaded. An unmanaged Thumbnail Provider may be more difficult to write, but it will be unloaded as soon as its reference count reaches zero.

Another problem I ran into was caused by the extension using one of our custom libraries to generate the thumbnail. I included a seperate copy of this second library with the installer of the shell extension so that we could update the application’s copy without worrying about the file being locked. However, this seems to cause problems when we use common File Dialogs in the application because the extension’s version of the library would override the application’s version when it got loaded. Unfortunately, the only solution I’ve come up with so far is to rename the extension’s version of the library, and this isn’t a very maintainable design decision.

Well, that conclude this tutorial. I hope I helped you a little with this rather confusing topic. I also hope I didn’t hurt too much with my ignorance of the topic. I’ll be the first to admit I’m not an expert in this field yet. If you discover any problems with my implementation, please let me know in the comments. Likewise, I’ll be sure to let you know of any developments in this project that I make in the future.

Happy coding!

Comments
  • Thanos:

    Hello and thank you for sharing your knowledge!
    I would like a bit of assistance.
    First I need you to take a look at this image and tell me, if I have made the structure of my solution as your instructions suggest:

    http://s9.postimg.org/w571eraxb/Untitled.jpg

    Second, I need tour advice.
    I am trying to make my own custom filetype, with thumbnail preview in windows explorer (obviously) and my plan so far is to create an xml, change its filetype to my custom one, and embed inside it the thumb image of my choice, converted to base64 and convert it back to an image to be used by the IExtractImage.

    I have already the necessary code in order to convert a base64 series of digits to an actual image.

    Here is a small app that jumps from base64 to Image and reverse, which I found online and adapted to c# and wpf, if you would like to check my code and see how it works:

    https://mega.co.nz/#!gZBBVCxY!4kLEEAqX9BSgd4xiXhJXjxJ_9Z3BpLXSilaYoVEKrxs

    Do you think this is possible, if I use my base64 to image code inside the Extact method of your code?

Leave a Comment
*