In an earlier post, I explained how to host Preview Handlers in Windows Forms. You may recall that Preview Handlers are implementations of the COM interface IPreviewHandler and are initialised either by a file or a stream (depending on whether they implement IInitializeWithStream or IInitializeWithFile).

I observed recently that the MAPI Mail Preview Handler (the handler registered for Outlook .MSG files) did not implement either of the aforementioned interfaces. After some further research, I have discovered that there is a third initialisation technique – which some preview handler implementations support to the exclusion of all others.

The COM interface IInitializeWithItem is used to initialise a preview handler (or thumbnail provider, etc) with an implementation of IShellItem. The latter interface is used by the Windows Shell API and represents a file, folder or virtual item in Windows Explorer. So why insist on a shell item instead of initialising the preview handler with a file or stream? Well, you have to remember that the primary host for preview handlers is, of course, Windows Explorer – which works primarily with shell items, rather than raw files or streams. They also remove the need for preview handlers to know the location of the item on disk (if indeed it is even on disk), allowing a wider variety of content to be loaded into them.

Declaration

The declaration for the interface is as follows:

internal const string GUID_ISHELLITEM = "43826d1e-e718-42ee-bc55-a1e261c37bfe";

(Don’t worry, i’ll explain that later…)

[
    ComImport,
    InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
    Guid("7F73BE3F-FB79-493C-A6C7-7EE14E245841")
]
interface IInitializeWithItem {
    void Initialize(IShellItem psi, uint grfMode);
}

[
    ComImport,
    InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
    Guid(PreviewHandlerHost.GUID_ISHELLITEM)
]
interface IShellItem {
    void BindToHandler(
        IntPtr pbc,
        [MarshalAs(UnmanagedType.LPStruct)]Guid bhid,
        [MarshalAs(UnmanagedType.LPStruct)]Guid riid,
        out IntPtr ppv
    );
    void GetParent(out IShellItem ppsi);
    void GetDisplayName(uint sigdnName, out IntPtr ppszName);
    void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
    void Compare(IShellItem psi, uint hint, out int piOrder);
};

Rather unsurprisingly, there is no managed API for obtaining instances of IShellItem, and they can’t be instantiated via Activator.CreateInstance(). Instead, the Shell API function SHCreateItemFromParsingName() is needed:

[DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
    static extern void SHCreateItemFromParsingName(
    [In][MarshalAs(UnmanagedType.LPWStr)] string pszPath,
    [In] IntPtr pbc, [In][MarshalAs(UnmanagedType.LPStruct)] Guid riid,
    [Out][MarshalAs(UnmanagedType.Interface, IidParameterIndex = 2)] out IShellItem ppv
);

(There are other functions that return IShellItem instances, but this one is the most appropriate for obtaining them for file system objects.)

Usage

Recalling the implementation of PreviewHandlerHost (my Windows Forms control), we can add a third condition to the section which initialises the preview handler:

if (mCurrentPreviewHandler is IInitializeWithFile) {
    ((IInitializeWithFile)mCurrentPreviewHandler).Initialize(filename, 0);
}
else if (mCurrentPreviewHandler is IInitializeWithStream) {
    mCurrentPreviewHandlerStream = File.Open(filename, FileMode.Open);
    StreamWrapper stream = new StreamWrapper(mCurrentPreviewHandlerStream);
    ((IInitializeWithStream)mCurrentPreviewHandler).Initialize(stream, 0);
}
else if (mCurrentPreviewHandler is IInitializeWithItem) {
    IShellItem shellItem;
    SHCreateItemFromParsingName(
        filename,
        IntPtr.Zero,
        new Guid(GUID_ISHELLITEM),
        out shellItem
    );
    ((IInitializeWithItem)mCurrentPreviewHandler).Initialize(shellItem, 0);
}

The reason for defining a constant for the GUID of the IShellItem interface is that it must be passed to the SHCreateItemFromParsingName() function. (There are two versions of the interface, one for compatibility with Windows XP SP1 and the other for Vista and onwards, the parameter determines which one to create.)

Final words

So, what does this enable us to do? Well, to date i’ve only come across one preview handler that insists on being initialised in this manner; the MAPI Mail Preview Handler {435fdba0-964c-43a7-8aff-cc94e21b2249}. However, in the business software I write, being able to preview Outlook messages is a significant requirement unto itself. Since Windows 7 ships with several more preview handlers than Vista did, it follows that the likelihood of encountering more which are initialised from shell items will increase.

Download

If you find my code useful, please consider making a donation.

PreviewHandlers3.zip – Visual Studio 2012 solution (includes test app) – .NET Framework 4.0 (updated 2013/10/14)

Regarding MSG Files

Several people have reported problems with the preview handler for Outlook mail messages (.msg files). To ensure that e-mail previews are displayed correctly, you will need to:

  • Ensure that the solution platform matches the architecture of the preview handler; i.e. x64 on 64-bit OS with 64-bit Office, or x86 on 32-bit OS with 32-bit Office. If you get this wrong, you will get an E_FAIL or CO_E_SERVER_EXEC_FAILURE error.
  • Use the included app.manifest file when building the project. This manifest instructs the runtime to load version 6 of the Windows Common Controls library (instead of the default version 5).
  • Run the project without attaching the debugger. In my build environment, this was the only way I could get it to work.

88 thoughts on “Preview Handlers Revisited

  1. +3
    vote

    Hi Bradley, Thanks for this.

    I also have a problem previewing PDF files, I’m using a 64 bit machine. I seem to get an E_FAIL error. I have tried to compile the application in both 64 and 32 bits. I have Adobe reader 32bit installed and I get the correct previewhandler guid from the registry (Same you gave in a previous comment).

    I also tried changing the Marshal.WriteInt64 to 32 as Fabske said. But that didn’t change anything.

    The weird thing is: I have another project which is an Outlook plug-in where it does work (Office 32bit). I compared both projects but I can’t find a difference.

    Do you have any idea what the problem is here?

    Reply
  2. vote

    Hello Bradley,

    how would I implement interface IPreviewHandlerVisuals? It is needed for font previews else it’s all black blocks.

    Thanks a lot,
    Don

    Reply
    • vote

      According to the documentation on MSDN, it should not be necessary to use this interface: “It is not compulsory for this interface to be called. The preview handlers must be able to make their own decisions.”

      In any case, you wouldn’t implement the interface yourself; rather, you would query the IPreviewHandler instance to see if it also implements IPreviewHandlerVisuals – in much the same way as we query whether the instance implements IInitializeWithFile, IInitializeWithStream, etc.

      In order to do this, you would need an imported definition of the COM interface – I found one here: https://github.com/dahall/Vanara/blob/master/PInvoke/Shell32/ShObjIdl.IPreviewHandler.cs (scroll down to IPreviewHandlerVisuals)

      Bear in mind, I have never tried this myself. Best of luck!

      Reply
      • vote

        That’s the theory but if you load a TTF font file into your previewer you will see the black blocks (at least in Win8.1). The only way to get rid if them is using IPreviewHandlerVisuals.

        Anyway, thanks a lot for your fast help, it worked out perfectly the way you described it! And thanks even more or this whole project, it has been a big help! (I will donate)

        Don

        Reply
  3. vote

    Hi Bradley,
    I don’t understand how marshaling from manages Message to unmanaged MSG works in IPreviewHandler.TranslateAccelerator() interface? The same question about marshaling Rectangle to RECT.
    C# WinForm Message has different count of fields than unmanaged MSG structure that is required by unmanaged IPreviewHandler.TranslateAccelerator() method.

    Reply
    • vote

      The fields that are common to MSG and Message have compatible types and are in the same order; therefore the .NET Framework is able to marshal them. Managed structures don’t need to have an exact 1-to-1 mapping in order for marshalling to work. The same is true of RECT and Rectangle, although Width and Height become ‘right’ and ‘bottom’, which have different semantics.

      This is fine for example code. You could re-declare the native structures if you wanted to, and use those instead. I’d recommend that if you actually intend to invoke TranslateAccelerator.

      Reply

Leave a reply

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> 

required