Click here to Skip to main content
15,113,855 members
Articles / Desktop Programming / Windows Forms
Posted 24 Oct 2009


378 bookmarked


Rate me:
Please Sign up or sign in to vote.
4.93/5 (145 votes)
18 Feb 2010Apache10 min read
A .NET ListView like control for displaying image files with asynchronously loaded thumbnails.
ImageListView demo


ImageListView is a .NET 2.0 control for displaying a list of image files. It looks and operates similar to the standard ListView control. Image thumbnails are loaded asynchronously with a separate background thread. The look of the control can be completely customized using custom renderers.


This project actually started as an owner-drawn ListView. However, this first version required way too many hacks. Determining the first/last visible items especially proved to be a challenge. Halfway through, I decided to roll my own control. Thus was born the ImageListView.

Using the Code

To use the control, add the ImageListView to your control toolbox and drag it on the form. You can then customize the appearance of the control by changing the view mode (Thumbnails, Gallery, Pane or Details), thumbnail size, column headers, etc.

Custom Rendering

The ImageListViewRenderer class is responsible for drawing the control. This is a public class with virtual functions that can be overridden by derived classes. Derived classes can modify the display size of items and column headers and draw any or all parts of the control.

ImageListView with custom renderer

Here is the renderer that produces this appearance:

public class DemoRenderer : ImageListView.ImageListViewRenderer
    // Returns item size for the given view mode.
    public override Size MeasureItem(View view)
        if (view == View.Thumbnails)
            Size itemPadding = new Size(4, 4);
            Size sz = ImageListView.ThumbnailSize + ImageListView.ItemMargin +
                      itemPadding + itemPadding;
            return sz;
            return base.MeasureItem(view);
    // Draws the background of the control.
    public override void DrawBackground(Graphics g, Rectangle bounds)
        if (ImageListView.View == View.Thumbnails)
            g.Clear(Color.FromArgb(32, 32, 32));
            base.DrawBackground(g, bounds);
    // Draws the specified item on the given graphics.
    public override void DrawItem(Graphics g, ImageListViewItem item,
        ItemState state, Rectangle bounds)
        if (ImageListView.View == View.Thumbnails)
            // Black background
            using (Brush b = new SolidBrush(Color.Black))
                Utility.FillRoundedRectangle(g, b, bounds, 4);
            // Background of selected items
            if ((state & ItemState.Selected) == ItemState.Selected)
                using (Brush b = new SolidBrush(Color.FromArgb(128,
                    Utility.FillRoundedRectangle(g, b, bounds, 4);
            // Gradient background
            using (Brush b = new LinearGradientBrush(
                Color.FromArgb(96, SystemColors.Highlight),
                Utility.FillRoundedRectangle(g, b, bounds, 4);
            // Light overlay for hovered items
            if ((state & ItemState.Hovered) == ItemState.Hovered)
                using (Brush b =
                       new SolidBrush(Color.FromArgb(32, SystemColors.Highlight)))
                    Utility.FillRoundedRectangle(g, b, bounds, 4);
            // Border
            using (Pen p = new Pen(Color.FromArgb(128, SystemColors.Highlight)))
                Utility.DrawRoundedRectangle(g, p, bounds.X, bounds.Y, bounds.Width - 1,
                                       bounds.Height - 1, 4);
            // Image
            Image img = item.ThumbnailImage;
            if (img != null)
                int x = bounds.Left + (bounds.Width - img.Width) / 2;
                int y = bounds.Top + (bounds.Height - img.Height) / 2;
                g.DrawImageUnscaled(item.ThumbnailImage, x, y);
                // Image border
                using (Pen p = new Pen(Color.FromArgb(128, SystemColors.Highlight)))
                    g.DrawRectangle(p, x, y, img.Width - 1, img.Height - 1);
            base.DrawItem(g, item, state, bounds);
    // Draws the selection rectangle.
    public override void DrawSelectionRectangle(Graphics g, Rectangle selection)
        using (Brush b = new HatchBrush(
            Color.FromArgb(128, Color.Black),
            Color.FromArgb(128, SystemColors.Highlight)))
            g.FillRectangle(b, selection);
        using (Pen p = new Pen(SystemColors.Highlight))
            g.DrawRectangle(p, selection.X, selection.Y,
                selection.Width, selection.Height);

Once you write your own renderer, you need to assign it to the ImageListView.

imageListView1.SetRenderer(new DemoRenderer());

Asynchronous Operation

ImageListView generates thumbnail images asynchronously with a background thread. Generated thumbnails are kept in a cache, which is managed by the ImageListViewCacheManager class. There are two modes in which the cache manager operates. The control can be switched between the two modes using the CacheMode property.

In the OnDemand mode, thumbnail images are generated only after they are requested. For example, when the user scrolls the view, items newly made visible will request their thumbnail images from the ImageListViewCacheManager. The cache manager will then add those items to a queue, which is monitored and exhausted by the worker thread. The user can limit the number of thumbnail images to be kept in the cache. When this limit is reached, the cache manager will remove some thumbnails from the cache to free up space. This mode is useful for using the control with many (thousands) of image files.

The other cache mode is Continuous. In this mode, the control will continuously generate and cache image thumbnails, regardless of item visibility. In this mode, is not possible to limit the cache size. This mode is probably best suited for using the control with a moderate number of items.

Points of Interest


ImageListView was designed to be used with a large number of images. To maintain smooth operation with thousands of image files, I had to make a number of optimizations.

Consolidating Control Paint

The ImageListViewRenderer class mentioned above is responsible for drawing the client area of the control. I had made sure that the renderer drew only the visible items when the control needed a refresh. One optimization I made afterwards was to add the functions: SuspendPaint and ResumePaint. They are used to consolidate render requests when the control is refreshed multiple times in a row. The following example should clarify their usage:

// Adds a range of items to the ImageListViewItemCollection.
public void AddRange(ImageListViewItem[] items)
    // Suspend the renderer while items are being added.

    // Each item addition will request the control to refresh itself.
    // But since the renderer is suspended, the control will not be
    // refreshed at all.
    foreach (ImageListViewItem item in items)

    // Resume the renderer. This will also refresh the control if any
    // refresh requests were made between SuspendPaint/ResumePaint
    // calls.

The implementation is quite simple as shown below:

// Suspends painting until a matching ResumePaint call is made.
internal void SuspendPaint()
    if (suspendCount == 0) needsPaint = false;
// Resumes painting. This call must be matched by a prior
// SuspendPaint call.
internal void ResumePaint()
    // Render the control if we received refresh requests
    // between SuspendPaint/ResumePaint calls.
    if (needsPaint)
// Redraws the control.
internal void Refresh()
    // Render the control only after we exit the final
    // suspend block.
    if (suspendCount == 0)
        needsPaint = true;

The suspendCount variable above is incremented when SuspendPaint is called and decremented when ResumePaint is called. This allows the suspend calls to be nested and the control will be refreshed only after the outermost ResumePaint is called and suspendCount is decremented to zero.

Caching File Properties

In details mode, ImageListView displays detailed information of the image files: such as the modification date, file size, file type, etc. File properties are read and cached when the item is created. They are updated only if the filename is changed. One bottleneck I identified here was the file type retrieval code. The .NET Framework does not have a native function to get the file type, so I had to use platform invoke. Here is what I had:

// Get file type via platform invoke
    out shinfo,
typeName = shinfo.szTypeName;

And here is the time it takes to add 1000 items with this:

Added 1000 items in 1282 milliseconds.

One second to load a thousand items actually doesn't sound that bad. But going through the above code, I realized that file types could be memorized. Most of the time, all images added to the control will be JPEG images, and the file type retrieval need only be called once. Here is the modified code:

// cachedFileTypes is the dictionary to memorize
// file types.
if (!cachedFileTypes.TryGetValue(Extension, out typeName))
    SHFILEINFO shinfo = new SHFILEINFO();
        out shinfo,
    typeName = shinfo.szTypeName;
    cachedFileTypes.Add(Extension, typeName);

Once I added a dictionary to memorize the file types, this is what I got:

Added 1000 items in 138 milliseconds.

Reading Embedded EXIF Thumbnails

Modern digital cameras embed thumbnail images of each shot taken. ImageListViewCacheManager can extract those embedded images to speed up thumbnail loading time. For this, I needed a fast method to extract embedded thumbnails. I tried the GetThumbnailImage method, I also tried manually reading the ThumbnailData Exif tag; both methods were too slow for my needs. The bottleneck was the Image.FromStream method. Here is the average time required to load a 3472x2604 1 MB JPEG file:

Reading a 3472x2604 JPEG file: 320.2 milliseconds.

Going from here, the time required to cache a thousand thumbnails (which is my minimum performance goal for ImageListView) would take 300 seconds, or 5 full minutes. I needed a faster method to read the embedded thumbnails. Searching further, I came across one particular overload of the Image.FromStream function:

public static Image FromStream(
    Stream stream,
    bool useEmbeddedColorManagement,
    bool validateImageData

With this overload, setting validateImageData to false results in the image being loaded much faster, since the framework does not validate image data. Here is the above experiment repeated with this overload:

Reading a 3472x2604 JPEG file: 0.47 milliseconds.

You read that right, 0.47 milliseconds. Caching a thousand thumbnails using this method would take 0.5 seconds. Although this (almost a thousand-fold) performance increase is astoundingly attractive, there are some issues to consider before using this method:

  • As the parameter name suggests, you are using invalidated image data with this method, which may result in errors if the image data happened to be corrupt. For example, GDI will likely throw an exception if you use an invalid image with any of the Graphics.DrawImage functions. This was a non-issue for me because I did not need the image data at all, just the ThumbnailData Exif tag.
  • The second issue is not related to this method in particular but to the usage of Image.FromStream in general. You must hold on to the source stream for the life time of your image; which may not be practical in some cases. Again this was not an issue for me, because I copied the contents of the ThumbnailData Exif tag and disposed of both the source image and stream immediately afterwards.
  • You may be tempted to use this method in a try/catch block to safely benefit from the performance increase. However my intuition is that, not getting exceptions may not mean that the image data is valid. If you must be sure that you get a valid image, the only way is to let the framework validate the image data.

To conclude, use this method if you need a fast way to read image properties: dimensions, Exif tags, etc. If you need the actual image data, use the slow method and let the framework validate the image.

Custom CodeDom Serializer

During the course of this project, I learned a lot of things. In the source code, you will find a custom editor for column headers, a custom designer, and a designer serializer. I consider the designer serializer the most interesting of those, so I will write a few words about it.

You may have noticed that when you drag a control onto your form, the initialization code magically appears in InitializeComponent. Most of the time, the default serialization behavior is sufficient. For ImageListView, this was not the case. The column header collection of the ImageListView is a read-only list without the Add method. The user cannot add or remove the columns, but she can show/hide the columns, change the display order, column texts and widths. I have the following method for letting the user customize all properties of a column at once.

public void SetColumnHeader(ColumnType type, string text,
       int width, int displayIndex, bool visible)
    // ....

I wanted the designer to generate my column initialization code by using this function, instead of the standard Add method of the collection. In order to do that, I wrote a new designer serializer class derived from CodeDomSerializer and assigned it to the ImageListView using the DesignerSerializer attribute, like follows:

[DesignerSerializer(typeof(ImageListViewSerializer), typeof(CodeDomSerializer))]

My CodeDomSerializer derived class overrides the Serialize method and adds my custom column initialization code.

internal class ImageListViewSerializer : CodeDomSerializer
    public override object Serialize
	(IDesignerSerializationManager manager, object value)
        CodeDomSerializer baseSerializer = (CodeDomSerializer)manager.GetSerializer(
        // Let the base class do its work first.
        object codeObject = baseSerializer.Serialize(manager, value);

        // Let us now add our own initialization code.
        if (codeObject is CodeStatementCollection)
            CodeStatementCollection statements = (CodeStatementCollection)codeObject;
            // This is the code reference to our ImageListView instance.
            CodeExpression imageListViewCode =
			base.SerializeToExpression(manager, value);
            if (imageListViewCode != null && value is ImageListView)
                // Walk through columns...
                foreach (ImageListViewColumnHeader column in
                    // Create a line of code that will invoke SetColumnHeader.
                    // Generated code will be something like this:
                    // myImageListView.SetColumnHeader(ColumnType.Name,
                    //            "Column Name", 120, 1, true);
                    CodeMethodInvokeExpression columnSetCode =
                                    new CodeMethodInvokeExpression(
                        new CodeFieldReferenceExpression(
                            new CodeTypeReferenceExpression(typeof(ColumnType)),
                            Enum.GetName(typeof(ColumnType), column.Type)),
                        new CodePrimitiveExpression(column.Text),
                        new CodePrimitiveExpression(column.Width),
                        new CodePrimitiveExpression(column.DisplayIndex),
                        new CodePrimitiveExpression(column.Visible)
                    // Add to the list of code statements.
            return codeObject;

        return base.Serialize(manager, value);

    public override object Deserialize(IDesignerSerializationManager manager,
                                       object codeObject)
        // Let the base class handle deserialization.
        CodeDomSerializer baseSerializer = (CodeDomSerializer)manager.GetSerializer(
        return baseSerializer.Deserialize(manager, codeObject);

This walks through the column collection, and for each column, it calls the SetColumnHeader method of the ImageListView instance with the parameters set by the user. If this looks complicated, here are some basic examples to get you started.

Creating a one-line comment:

CodeCommentStatement commentCode = new CodeCommentStatement("This is a comment");

will result in:

// This is a comment

A simple declaration with initialization:

CodePrimitiveExpression valueCode = new CodePrimitiveExpression("hello");
CodeVariableDeclarationStatement declarationCode =
  new CodeVariableDeclarationStatement(typeof(string), "myString", valueCode);

will result in:

string myString = "hello";

The conditional:

CodeVariableReferenceExpression testCode =
   new CodeVariableReferenceExpression("check");
CodeStatement[] trueBlock =
   new CodeStatement[] { new CodeCommentStatement("check is true") };
CodeStatement[] falseBlock =
   new CodeStatement[] { new CodeCommentStatement("check is false") };
CodeConditionStatement ifCode =
   new CodeConditionStatement(testCode, trueBlock, falseBlock);

will result in:

if (check)
    // check is true
    // check is false

The property access:

CodeThisReferenceExpression thisCode = new CodeThisReferenceExpression();
CodePropertyReferenceExpression propCode =
    new CodePropertyReferenceExpression(thisCode, "MyProperty");
CodePropertyReferenceExpression otherPropCode =
    new CodePropertyReferenceExpression(thisCode, "MyOtherProperty");
CodeAssignStatement assignCode = new CodeAssignStatement(propCode, otherPropCode);

will result in:

this.MyProperty = this.MyOtherProperty;

For further information, here are some references from the MSDN:

Built-In Renderers

Writing custom renderers for ImageListView is an involved task. Instead of writing a renderer from scratch, you can use one of the built-in renderers or use a built-in renderer as a starting point for your custom renderer. The following built-in renderers are currently available:

Built-in renderers



  • 25 October 2009 - Initial release
  • 26 October 2009 - Updated demo and source files
  • 29 October 2009 - Added the capability to read embedded thumbnails
  • 01 November 2009 - Added drag&drop support and minor bug fixes
  • 04 November 2009 - Article updated and minor bug fixes
  • 09 November 2009 - .NET 2.0 version added and minor bug fixes
  • 12 November 2009
    • .NET 3.5 version discontinued
    • Items can now be reordered by dragging them in the control
    • Item properties are now fetched by a background thread. Adding items should be much faster
    • Item details added for Image Dimensions and Resolution
  • 15 November 2009 - Cached item indices to speed-up item lookups
  • 16 December 2009
    • Added the Gallery view mode
    • Added new column types for common image metadata
    • Added the BeginEdit() and EndEdit() methods to ImageListViewItem. They should be used while editing items to prevent collisions with cache threads.
    • Added the GetImage() method to ImageListViewItem
    • Added the new overridable method, OnLayout to the ImageListViewRenderer. It can be used to modify the size of the item area by custom renderers.
    • Added Clip, ItemAreaBounds and ColumnHeaderBounds properties to ImageListViewRenderer
    • Renderers can now draw items in a specific order using the new ImageListViewRenderer.ItemDrawOrder property. A finer control is also possible using the new ImageListViewItem.ZOrder property.
    • Added built-in renderers
    • Maximum size of the thumbnail cache can now be (approximately) set by the user using the new ImageListView.CacheLimit property
    • Default column texts are now loaded from resources to allow localization
    • Cached images are now properly disposed
    • Custom renderers now use the central thumbnail cache instead of their own worker threads
  • 29 December 2009
    • Adjustable properties of built-in renderers are now public
    • Removed ImageListView.ItemMargin property in favor of the new overridable ImageListViewRenderer.MeasureItemMargin method
    • Gallery image is now updated after editing an item
    • Moved column sort icons to the neutral resource
    • Cleaned up the utility class
    • Fixed a bug where updating an item did not update the item thumbnail
    • Removed the ImageListViewRenderer.GetSortArrow function. Sort arrow is now drawn in the DrawColumnHeader method
    • Fixed the issue about the missing semicolon in GIF files
    • Removed the SortOrder enum, it was a duplicate of Windows.Forms.SortOrder
    • Fixed the issue where double clicking on a separator raised a column click event
    • Added the NewYear2010Renderer. You need to define the preprocessor symbol BONUSPACK to include in the binary. Happy new year people!
  • 4 January 2010
    • Added the new Pane view mode, removed PanelRenderer
    • Added NoirRenderer
    • Renamed ImageListViewRenderer.OnDispose to Dispose
    • Removed ImageListViewRenderer.DrawScrollBarFiller virtual method
  • 17 February 2010
    • Added support for virtual items
    • The control is now scrolled while dragging items to the edges of the client area
    • Added the RetryOnError property. When set to true, the cache thread will continuously poll the control for a thumbnail, until it gets a valid image. When set to false, the cache thread will give up after the first error and display the ErrorImage.
    • Added the ItemHover and ColumnHover events
    • Added the DropFiles event
    • Added the CacheMode property to support continuous caching
    • Added Mono support (tested with Mono 2.6)


This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0


About the Author

Ozgur Ozcitak
Turkey Turkey
No Biography provided

Comments and Discussions

GeneralRe: ImageListView Width and Height Pin
Ozgur Ozcitak16-May-10 14:24
MemberOzgur Ozcitak16-May-10 14:24 
QuestionZoomingRenderer Pin
roshihans15-Apr-10 7:40
Memberroshihans15-Apr-10 7:40 
AnswerRe: ZoomingRenderer Pin
Ozgur Ozcitak15-Apr-10 9:38
MemberOzgur Ozcitak15-Apr-10 9:38 
GeneralTebrik ederim Pin
Muhammed Şahin27-Mar-10 12:38
MemberMuhammed Şahin27-Mar-10 12:38 
Generalsuggestion Pin
zyccld22-Mar-10 6:54
Memberzyccld22-Mar-10 6:54 
GeneralThank you for the real proffesional work Pin
farouk11-Mar-10 22:49
Memberfarouk11-Mar-10 22:49 
GeneralRe: Thank you for the real proffesional work Pin
Ozgur Ozcitak12-Mar-10 1:06
MemberOzgur Ozcitak12-Mar-10 1:06 
GeneralI Like the Control, but I am stuck with legacy images Pin
MicroImaging10-Mar-10 17:31
MemberMicroImaging10-Mar-10 17:31 
I tried downloading the current rev from Google code and spent 2 days going through the code.
Working in a method to use pre-existing shrunk down bmp images for the thumbnails.
I managed to load the external thumbnails just fine, but then i managed to break the gallery, and Pane view where they present a larger view of the original image.
I am stuck with 1300 x 1000 or greater JPG images with no EXIF information, and the previous code would present a preshrunk bmp file as the thumbnail.

I finally hacked together something that worked, but it is a very gross distortion of your work.

I tried to include a thumbnail path into the add function through overloading, and overloading the item creation and various other classes, like the request but as I said the thumbnails would load fine, but the gallery, and pane view would only shown the small thumbnail, instead of the larger view.

i was wondering if you are willing to look at what I did, and show me where the problem is.
My impression is that the DoWork Function in the ImageListViewCacheManager bounces between
if (request.IsVirtualItem) and the else statement which performs the Utility.ThumbnailFromFile
operation but somehow I broke the connection that allowed teh Utility.ThumbnailFromFile( call with the larger request.size.

For now my Hack Works, but it is ugly, and definitely not worthy of your workmanship.

In any case thank you, for saving me a lot of work and heart ache.
GeneralRe: I Like the Control, but I am stuck with legacy images [modified] Pin
Ozgur Ozcitak11-Mar-10 1:52
MemberOzgur Ozcitak11-Mar-10 1:52 
GeneralRe: I Like the Control, but I am stuck with legacy images Pin
MicroImaging11-Mar-10 4:33
MemberMicroImaging11-Mar-10 4:33 
GeneralExcellent Work. 1 small bug [modified] Pin
tonyt9-Mar-10 22:44
Membertonyt9-Mar-10 22:44 
GeneralRe: Excellent Work. 1 small bug Pin
Ozgur Ozcitak10-Mar-10 11:27
MemberOzgur Ozcitak10-Mar-10 11:27 
GeneralRe: Excellent Work. 1 small bug Pin
tonyt10-Mar-10 19:36
Membertonyt10-Mar-10 19:36 
GeneralRe: Excellent Work. 1 small bug [modified] Pin
Ozgur Ozcitak11-Mar-10 0:38
MemberOzgur Ozcitak11-Mar-10 0:38 
GeneralGreat control ! Pin
henur28-Feb-10 4:00
Memberhenur28-Feb-10 4:00 
GeneralRe: Great control ! Pin
Ozgur Ozcitak28-Feb-10 11:28
MemberOzgur Ozcitak28-Feb-10 11:28 
GeneralRe: Great control ! Pin
henur28-Feb-10 20:57
Memberhenur28-Feb-10 20:57 
GeneralRe: Great control ! Pin
Ozgur Ozcitak1-Mar-10 1:47
MemberOzgur Ozcitak1-Mar-10 1:47 
GeneralRe: Great control ! Pin
henur1-Mar-10 10:02
Memberhenur1-Mar-10 10:02 
GeneralRe: Great control ! Pin
Ozgur Ozcitak2-Mar-10 1:18
MemberOzgur Ozcitak2-Mar-10 1:18 
GeneralRe: Great control ! Pin
henur2-Mar-10 2:51
Memberhenur2-Mar-10 2:51 
GeneralRe: Great control ! Pin
henur2-Mar-10 10:34
Memberhenur2-Mar-10 10:34 
GeneralRe: Great control ! Pin
Ozgur Ozcitak2-Mar-10 10:57
MemberOzgur Ozcitak2-Mar-10 10:57 
GeneralNice Article Pin
Khaniya24-Feb-10 18:32
professionalKhaniya24-Feb-10 18:32 
GeneralVery cool! + suggestion Pin
philippe dykmans24-Feb-10 9:35
Memberphilippe dykmans24-Feb-10 9:35 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.