Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

.NET Shell Extensions - Shell Thumbnail Handlers

4.85/5 (15 votes)
17 Mar 2013CPOL8 min read 125.3K   4.4K  
Create Shell Thumbnail Handler Extensions using .NET!

Introduction

Shell Thumbnail Handlers (or as they're sometimes known, Shell Thumbnail Providers) are COM servers that you can write to customize the appearance of the thumbnail icons in the Windows Shell. We're familiar with many of these - for example, we expect to see a thumbnail of a jpeg file, but it turns out that making your own is quite straightforward, especially with a little help from the library SharpShell.

In this article, I'll show you how to write a Shell Thumbnail Handler for a Text File. This is a great starting point if you want to create your own shell thumbnail providers.

Image 1

Above: A screenshot of an example Thumbnail Handler for text files. This thumbnail handler renders the first few lines of text.

The Series

This article is part of the series '.NET Shell Extensions', which includes:

  1. .NET Shell Extensions - Shell Context Menus
  2. .NET Shell Extensions - Shell Icon Handlers
  3. .NET Shell Extensions - Shell Info Tip Handlers
  4. .NET Shell Extensions - Shell Drop Handlers
  5. .NET Shell Extensions - Shell Preview Handlers
  6. .NET Shell Extensions - Shell Icon Overlay Handlers
  7. .NET Shell Extensions - Shell Thumbnail Handlers
  8. .NET Shell Extensions - Shell Property Sheets

Getting Started

Typically, writing Shell Extension handlers involves creating a class that implements a well defined set of COM interfaces. Now, as these COM interfaces are often not included in the .NET Framework, we have to implement them ourselves. However, the SharpShell library has all of these interfaces included - it also provides base classes for extensions, making it very straightforward to implement them. Let's get started with our text file thumbnail handler now.

Our Goal

We're going to build the extension step-by-step shortly, but first let's just see the end result, because I think it highlights very well how easy SharpShell makes it to implement these handlers. Below is the entire code for the extension - as you can see, it is very simple and straightforward! Don't worry about the details too much now, because we'll build the extension from the ground up next.

C#
/// <summary>
/// The TxtThumbnailHandler is a ThumbnailHandler for text files
/// </summary>
[ComVisible(true)]
[COMServerAssociation(AssociationType.FileExtension, ".txt")]
public class TxtThumbnailHandler : SharpThumbnailHandler
{
    /// <summary>
    /// Initializes a new instance of the <see cref="TxtThumbnailHandler"/> class
    /// </summary>
    public TxtThumbnailHandler()
    {
        //  Create our lazy objects
        lazyThumbnailFont = new Lazy<Font>(() => new Font("Courier New", 12f));
        lazyThumbnailTextBrush = new Lazy<Brush>(() => new SolidBrush(Color.Black));
    }
 
    /// <summary>
    /// Gets the thumbnail image
    /// </summary>
    /// <param name="width">The width of the image that should be returned.</param>
    /// <returns>
    /// The image for the thumbnail
    /// </returns>
    protected override Bitmap GetThumbnailImage(uint width)
    {
        //  Attempt to open the stream with a reader
        try
        {
            using(var reader = new StreamReader(SelectedItemStream))
            {
                //  Read up to ten lines of text
                var previewLines = new List<string>();
                for (int i = 0; i < 10; i++)
                {
                    var line = reader.ReadLine();
                    if (line == null)
                        break;
                    previewLines.Add(line);
                }
 
                //  Now return a preview of the lines
                return CreateThumbnailForText(previewLines, width);
            }
        }
        catch (Exception exception)
        {
            //  Log the exception and return null for failure
            LogError("An exception occurred opening the text file.", exception);
            return null;
        }
    }
 
    /// <summary>
    /// Creates the thumbnail for text, using the provided preview lines
    /// </summary>
    /// <param name="previewLines">The preview lines.</param>
    /// <param name="width">The width.</param>
    /// <returns>
    /// A thumbnail for the text
    /// </returns>
    private Bitmap CreateThumbnailForText(IEnumerable<string> previewLines, uint width)
    {
        //  Create the bitmap dimensions
        var thumbnailSize = new Size((int) width, (int) width);
 
        //  Create the bitmap
        var bitmap = new Bitmap(thumbnailSize.Width, 
                                thumbnailSize.Height, PixelFormat.Format32bppArgb);
 
        //  Create a graphics object to render to the bitmap
        using (var graphics = Graphics.FromImage(bitmap))
        {
            //  Set the rendering up for anti-aliasing
            graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
 
            //  Draw the page background
            graphics.DrawImage(Properties.Resources.Page, 0, 0, 
                               thumbnailSize.Width, thumbnailSize.Height);
            
            //  Create offsets for the text
            var xOffset = width * 0.2f;
            var yOffset = width * 0.3f ;
            var yLimit = width - yOffset;
 
            graphics.Clip = new Region(new RectangleF(xOffset, yOffset, 
              thumbnailSize.Width - (xOffset * 2), thumbnailSize.Height - width*.1f));
 
            //  Render each line of text
            foreach (var line in previewLines)
            {
                graphics.DrawString(line, lazyThumbnailFont.Value, 
                                    lazyThumbnailTextBrush.Value, xOffset, yOffset);
                yOffset += 14f;
                if (yOffset + 14f > yLimit)
                    break;
            }
        }
 
        //  Return the bitmap
        return bitmap;
    }
 
    /// <summary>
    /// The lazy thumbnail font
    /// </summary>
    private readonly Lazy<Font> lazyThumbnailFont;
 
    /// <summary>
    /// The lazy thumbnail text brush
    /// </summary>
    private readonly Lazy<Brush> lazyThumbnailTextBrush;
}

This is not very much work to get a working shell extension! Now let's go into the details.

Step 1: Creating the Project

First, create a new C# Class Library project.

Tip: You can use Visual Basic rather than C# - in this article, the source code is C# but the method for creating a Visual Basic Shell Extension is just the same.

In this example, we'll call the project 'TxtThumbnailHandler'. Rename the 'Class1.cs' file to 'TxtThumbnailHandler.cs'.

Now add the following references:

  1. System.Windows.Forms
  2. System.Drawing

These references contain various useful bits and pieces that other parts of the SharpShell library will need, such as icons and context menus.

Tip: If you use Nuget to install SharpShell (see 'Step 2') you don't need to add these references - they'll be added automatically.

Step 2: Referencing SharpShell

We now need to add a reference to the core SharpShell library. You can do that in a few different ways:

Add Reference

Download the 'SharpShell Core Library' zip file at the top of the article and add a reference to the SharpShell.dll file.

Tip: The download on this article is correct at the time of writing - if you need the latest version, use Nuget (as described below) or get the library from sharpshell.codeplex.com.

Use Nuget

If you have Nuget installed, just do a quick search for SharpShell and install it directly - or get the package details at https://www.nuget.org/packages/SharpShell.

Image 2

IMPORTANT: SharpShell is being updated regularly, I get feature requests often and implement them quickly, so I strongly recommend using the Nuget package to reference SharpShell - it's easier to automatically keep it up-to-date.

Step 3: Deriving from SharpThumbnailHandler

Now that we've set up the project, we can derive the TxtThumbnailHandler class from SharpThumbnailHandler, which is the base class for Thumbnail Handler Shell Extensions.

C#
public class TxtThumbnailHandler : SharpThumbnailHandler
{
} 

For a thumbnail handler, there is one abstract function in the base class that we must implement in the derived class.

GetThumbnailImage

C#
protected abstract Bitmap GetThumbnailImage(uint width);

This function will be called to get the bitmap for your thumbnail. It's really important to understand the following points:

  1. Thumbnail handlers are initialized via streams - not file paths. This means you don't know the path of the file you are previewing, you only have its stream.
  2. The file stream is stored in the SelectedItemStream property. You can use this to read the file and build the thumbnail.
  3. Try and respect the width variable, its the width the system wants. However, worst comes to the worst, it will scale your image if needed.
  4. Typically, you set the height of the bitmap to the same as the width, but it can be less, the system will respect this. However, the thumbnail cannot be higher than it is wide.

Now let's see how we'd implement this function in our example.

C#
protected override Bitmap GetThumbnailImage(uint width)
{
    //  Create a stream reader for the selected item stream
    try
    {
        using(var reader = new StreamReader(SelectedItemStream))
        {
            //  Read up to ten lines of text
            var previewLines = new List<string>();
            for (int i = 0; i < 10; i++)
            {
                var line = reader.ReadLine();
                if (line == null)
                    break;
                previewLines.Add(line);
            }
 
            //  Now return a preview of the lines
            return CreateThumbnailForText(previewLines, width);
        }
    }
    catch (Exception exception)
    {
        //  Log the exception and return null for failure
        LogError("An exception occurred opening the text file.", exception);
        return null;
    }
}

Well, the first thing we do is make sure that we use an exception handler around the bulk of the logic.

Tip: SharpShell wraps all calls to abstract functions in exception handlers and logs exceptions to the windows event log if the server is compiled in debug mode. But it's a good practice to deal with exceptions ourselves if we expect them. Don't forget, you can call 'Log' or 'LogError' to write to the SharpShell Windows Application event log.

Next, we create a stream reader for the SelectedItemStream (which is the stream of the shell item we're previewing). From this, we read a few lines of text, and pass onto the function CreateThumbnailForText.

So, we also need to create the CreateThumbnailForText function:

C#
private Bitmap CreateThumbnailForText(IEnumerable<string> previewLines, uint width)
{
    //  Create the bitmap dimensions
    var thumbnailSize = new Size((int) width, (int) width);
 
    //  Create the bitmap
    var bitmap = new Bitmap(thumbnailSize.Width, thumbnailSize.Height, 
                            PixelFormat.Format32bppArgb);
 
    //  Create a graphics object to render to the bitmap
    using (var graphics = Graphics.FromImage(bitmap))
    {
        //  Set the rendering up for anti-aliasing
        graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
 
        //  Draw the page background
        graphics.DrawImage(Properties.Resources.Page, 0, 0, thumbnailSize.Width, 
                           thumbnailSize.Height);
        
        //  Create offsets for the text
        var xOffset = width * 0.2f;
        var yOffset = width * 0.3f ;
        var yLimit = width - yOffset;
 
        graphics.Clip = new Region(new RectangleF(xOffset, yOffset, 
                thumbnailSize.Width - (xOffset * 2), thumbnailSize.Height - width*.1f));
 
        //  Render each line of text
        foreach (var line in previewLines)
        {
            graphics.DrawString(line, lazyThumbnailFont.Value, 
                                lazyThumbnailTextBrush.Value, xOffset, yOffset);
            yOffset += 14f;
            if (yOffset + 14f > yLimit)
                break;
        }
    }
 
    //  Return the bitmap
    return bitmap;
} 

Again, we're dealing with some fairly straightforward logic here. We create a bitmap of the requested size, making sure that we use 32 bit ARGB mode (i.e., we make sure we have an alpha-channel). Then we create a graphics object to draw onto it.

Following that, we render a *.png resource file onto the bitmap (the resource file is in the sample project, it's an image of a page). Now we just render some of the text on top of the page image (making sure we keep within the bounds of the image we just drew).

That's it - we can return the bitmap and we're done! Well, actually, there are two member variables used in this function, here's their declaration:

C#
public TxtThumbnailHandler()
{
    //  Create our lazy objects
    lazyThumbnailFont = new Lazy<Font>(() => new Font("Courier New", 12f));
    lazyThumbnailTextBrush = new Lazy<Brush>(() => new SolidBrush(Color.Black));
}

/// <summary>
/// The lazy thumbnail font
/// </summary>
private readonly Lazy<Font> lazyThumbnailFont;

/// <summary>
/// The lazy thumbnail text brush
/// </summary>
private readonly Lazy<Brush> lazyThumbnailTextBrush;

Step 4: Handling the COM Registration

There are just a few things left to do. First, we must add the ComVisible attribute to our class. This because our class is a COM server and must be visible to other code trying to use it.

C#
[ComVisible(true)]
public class TxtThumbnailHandler : SharpThumbnailHandler

Next, we must give the assembly a strong name. There are ways around this requirement, but generally this is the best approach to take. To do this, right click on the project and choose 'Properties'. Then go to 'Signing'. Choose 'Sign the Assembly', specify 'New' for the key and choose a key name. You can password project the key if you want to, but it is not required:

Image 3

Finally, we need to associate our extension with some of the types of shell items we want to use it for. We can do that with the COMServerAssociation attribute:

C#
[ComVisible(true)]
[COMServerAssociation(AssociationType.FileExtension, ".txt")]
public class TxtThumbnailHandler : SharpThumbnailHandler

So what have we done here? We've told SharpShell that when registering the server, we want it to be associated with *.txt files.

You can associate with files, folders, classes, drives and more - full documentation on using the association attribute is available on the CodePlex site at COM Server Associations.

Tip: Many SharpShell servers support the 'AssociationType.ClassOfExtension' association. With this, you provide something like *.txt and the server is registered for all text files (which might be txt, text, txtfile, dat, etc.). Please be aware that Thumbnail handlers do not support the ClassOfExtension association type!

We're done! Building the project creates the TxtThumbnailHandler assembly, which can be registered as a COM server to add the extension to the system, allowing you to quickly have a preview of the contents of text files.

Debugging the Shell Extension

If you have seen any of my other articles on .NET Shell Extensions, you may recognize the 'Server Manager' tool. This is a tool in the SharpShell source code that can be used to help debug Shell Extensions.

Tip: If you want the latest version of the tools, they're available pre-built from the CodePlex site.

Open the Sever Manager tool and use File > Load Server to load the TxtThumbnailHandler.dll file. You can also drag the server into the main window. Selecting the server will show you some details on it. Select the server.

Image 4

Now you can choose 'text server in test shell'. All files associated with the server are highlighted in bold, select the file and the thumbnail tab on the right - your thumbnail handler will be used to generate a thumbnail preview.

Image 5

Installing and Registering the Shell Extension

You can check the 'Installing and Registering the Shell Extension' section of the .NET Shell Extensions - Shell Context Menus for details on how to install and register these extensions - the process is the same.

Troubleshooting

Troubleshooting shell extensions is very hard, so I've got a page I'm keeping up to date with some tips, it's on the CodePlex site at:

History

  • 17th March, 2013: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)