Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Image Thumbnail Viewer with .NET 2.0

4.61/5 (38 votes)
30 Aug 2007CPOL5 min read 4   10.6K  
A simple way to create a thumbnail viewer with .NET 2.0
Screenshot - thumbnaildotnet2.png

Screenshot - thumbnaildotnet2_image.png

Introduction

Ok, I know, you're thinking: another thumbnail viewer... and you are right!

But let me explain the purpose of this article: I saw a very good article by Marc Clifton Multi Image Viewer and I was just thinking that there must be a simple solution with .NET 2.0 to do a Picasa-like viewer.

So this article is about how to easily add thumbnail viewer functionality to an application with C# 2.0.

Requirements

Let's define some requirements:

  • Load asynchronously images from a folder and show them in a thumbnail viewer
  • Manage the memory consumption nicely and smoothly
  • Adapt the layout from left to right dynamically depending on the size of the application
  • Add scrollbars when required
  • Add a cancel during loading
  • Add thumbnail zooming
  • Detect if someone clicks on an image, show the selected image in the thumbnail and in an external viewer
  • Make it nice, Picasa look&feel
  • Everything with C# 2.0 and minimal code

How To

The main idea is to use the FlowLayoutPanel control. This panel dynamically lays out its contents horizontally or vertically, contents are of course controls. Now you get the point: just use FlowLayoutPanel, set the property FlowDirection to FlowDirection.LeftToRight and that's it.

If you want to know more about anchoring and docking, have a look at How to: Anchor and Dock Child Controls in a FlowLayoutPanel Control.

Now let's continue with an image viewer: PictureBox. This control is good at showing images with some options like SizeMode = PictureBoxSizeMode.Zoom. So it looks like we found the solution. We could work with these two controls like this:

C#
// for each image
PictureBox imageViewer = new PictureBox();
imageViewer.Image = Image.FromFile(imageFilename);
imageViewer.SizeMode = PictureBoxSizeMode.Zoom;
imageViewer.Dock = DockStyle.Bottom;
imageViewer.Height = 128;
imageViewer.Width = 128;
flowLayoutPanelMain.Controls.Add(imageViewer);

But I decided to write my own image viewer in order to improve the memory management (don't forget that we are working first with full size images) and we will be able to add some nice style (shadow, selection frame).

In summary, this is just a bitmap scaling in order to create the thumbnail (reduce memory size) and a GDI+ rendering:

C#
// load the image and scale it to the given width and height
// we don't use GetThumbnailImage because GDI does not always provide
// an optimal quality
public void LoadImage(string imageFilename, int width, int height)
{
    Image tempImage = Image.FromFile(imageFilename);

    int dw = tempImage.Width;
    int dh = tempImage.Height;
    int tw = width;
    int th = height;
    double zw = (tw / (double)dw);
    double zh = (th / (double)dh);
    double z = (zw <= zh) ? zw : zh;
    dw = (int)(dw * z);
    dh = (int)(dh * z);

    m_Image = new Bitmap(dw, dh);
    Graphics g = Graphics.FromImage(m_Image);
    g.InterpolationMode = InterpolationMode.HighQualityBicubic;
    g.DrawImage(tempImage, 0, 0, dw, dh);
    g.Dispose();

    tempImage.Dispose(); // do not forget to dispose and don't wait for GC
}

// render the image m_Image with a scaling to the size of the control
// for that we have to override the OnPaint of the usercontrol
protected override void OnPaint(PaintEventArgs e)
{
    Graphics g = e.Graphics;
    if (g == null) return;

    int dw = m_Image.Width;
    int dh = m_Image.Height;
    int tw = this.Width;
    int th = this.Height;
    double zw = (tw / (double)dw);
    double zh = (th / (double)dh);
    double z = (zw <= zh) ? zw : zh;

    dw = (int)(dw * z);
    dh = (int)(dh * z);
    int dl = (tw - dw) / 2;
    int dt = (th - dh) / 2;

    g.DrawImage(m_Image, dl, dt, dw, dh);
}

In the project demo, the class ImageViewer implements a user control for image rendering.

Model View Controller

In order to fulfill the asynchronous requirement, we will use a controller class that scans directories, looks for images and does its stuff in a thread.

The controller will simply start a thread with one parameter: the path to be scanned.

C#
public void AddFolder(string folderPath)
{
    Thread thread = new Thread(new ParameterizedThreadStart(AddFolder));
    thread.IsBackground = true;
    thread.Start(folderPath);
}

private void AddFolder(object folderPath)
{
    ...
}

In a Model-View-Controller product code, I would recommend to define a real model, for example a class containing enough information to be used by any source: disk, database or URL. Here, we will simply use a string, the path of the image.

The controller informs the View by using Event-Delegate each time an image is found. The View creates a new image control and adds it to the FlowLayoutPanel.

C#
m_Controller = new ThumbnailController();
m_Controller.OnStart += new ThumbnailControllerEventHandler(m_Controller_OnStart);
m_Controller.OnAdd += new ThumbnailControllerEventHandler(m_Controller_OnAdd);
m_Controller.OnEnd += new ThumbnailControllerEventHandler(m_Controller_OnEnd);

private void AddImage(string imageFilename)
{
    ImageViewer imageViewer = new ImageViewer();
    imageViewer.LoadImage( imageFilename, 128, 128);
    this.flowLayoutPanelMain.Controls.Add(imageViewer);
}

Threading Issues

If you are going to use threads in Windows Forms, be careful of threading issues. In short, your form is running in its own thread and a different thread interacts with it or its controls.

You cannot directly change properties of these controls without taking the chance of an ugly exception, it may happen or not. Luckily, .NET provides a simple way to check if you are running in the control thread and to invoke methods in a safe way.

Use InvokeRequired to perform the test: the property simply tests if the working thread Id is the same as the control thread Id.

Use Invoke to perform the call: the method posts a message in the queue of the control/window with your callback function (you therefore need a delegate), this is thread safe.

In this example, we used it whenever the controller sends an event to the view.

C#
private void AddImage(string imageFilename)
{
    // thread safe
    if (this.InvokeRequired)
    {
        this.Invoke(Delegate /* delegate */, object[] /* objects */);
    }
    else
    {
        // do what we have to do
    }
}

ScrollableControl Issues

It seems that the controls derived from ScrollableControl do not always act the way you would like them to. FlowLayoutPanel is derived from Panel, itself derived from ScrollableControl.

If you set AutoScroll = true, you will have nice scrollbars that will appear when required but also a strange bug: when you try to click on a thumbnail the scrollbars jump back to the origin.

As steven69 posted, you can remove Dock = DockStyle.Bottom and it works. But if you want to do it the tricky way, just override FlowLayoutPanel in a derived control ThumbnailFlowLayoutPanel with the following:

C#
protected override Point ScrollToControl(Control activeControl)
{
    return this.AutoScrollPosition;
}  

The control does not jump anymore because we are telling the panel to ignore any active control.

More Nice Features

Now that we have the basis of our application, we can easily add some nice features.

Thumbnail Zooming

The size of the thumbnails is hard-encoded in imageViewer.LoadImage(imageFilename, 256, 256) but the display size is defined by the size of the control.

So, we just have to dispatch the trackbar event ValueChanged to each ImageViewer and set the size according to the zoom value (e.g. 64, 128 or 256 pixels).

Thumbnail Selection

We just added a local member in order to store the selected thumbnail. Each time a thumbnail is clicked, the old one is inactivated and the new one is in activated status (blue border).

Summary

In order to fulfill our requirements, we used:

  • Thumbnail viewer: FlowLayoutPanel and ImageViewer
  • Add scrollbars: FlowLayoutPanel.AutoScroll = true and ThumbnailFlowLayoutPanel
  • Load asynchronously: Controller with thread and events.
  • Add a cancel: Add a flag in the controller
  • Detect if someone clicks on an image: Add an eventhandler to each instance of ImageViewer. We then show a dialog with the selected image.
  • Use a trackbar to simply set the size of the thumbnail control.

Conclusion

Nothing new here but a simple thread safe way to implement an asynchronous thumbnail viewer with C# 2.0, thanks to FlowLayoutPanel and some threading.

I hope this small demo will provide you with ideas. Any comment is welcome!

History

  • V1.3 - 30th August, 2007 - Added thumbnail zooming and selection - Code update
  • V1.2 - 21st August, 2007 - Fixed FlowLayoutPanel.AutoScroll issue - Code update
  • V1.1 - 16th August, 2007 - Added threading issues doc - Fixed memory usage - Code update - (thanks to the post from Anthony Yates)
  • V1.0 - 14th August, 2007 - First version

License

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