Click here to Skip to main content
Click here to Skip to main content

Image Thumbnail Viewer with .NET 2.0

By , 30 Aug 2007
 
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:

// 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:

// 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.

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.

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.

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:

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)

About the Author

Marc Lievin
Canada Canada
Member
Marc is software engineer at Avid Technology, Montreal, Canada.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionAdd KeyDown event handlermemberDieter Vander Donckt18 Sep '12 - 11:29 
First of all thank you for this article.
It was really helpful to me.
 
I would like the possibility to navigate through the images with the left and right keys.
 
First i created the KeyDown event :
private void imageViewer_KeyDown(object sender, KeyEventArgs e)
        {
            if (m_ActiveImageViewer == null) 
                return;
            int Index = this.flowLayoutPanelMain.Controls.IndexOf(m_ActiveImageViewer);
            switch (e.KeyCode)
            {
                case Keys.Left:
                    //Check index
                    if (Index == 0)
                        return;
                    //Clear active thumbnail
                    m_ActiveImageViewer.IsActive = false;
                    //Set active thumbnail
                    m_ActiveImageViewer = (ImageViewer)this.flowLayoutPanelMain.Controls[Index -1];
                    m_ActiveImageViewer.IsActive = true;
                    m_ActiveImageViewer.Focus();
                    break;
                case Keys.Right:
                    //Check index
                    if (Index == this.flowLayoutPanelMain.Controls.Count - 1)
                        return;
                    //Clear active thumbnail
                    m_ActiveImageViewer.IsActive = false;
                    //Set active thumbnail
                    m_ActiveImageViewer = (ImageViewer)this.flowLayoutPanelMain.Controls[Index + 1];
                    m_ActiveImageViewer.IsActive = true;
                    m_ActiveImageViewer.Focus();
                    break;
            }
        }
 
Then i added a KeyDown eventhandler when adding an image to the FlowLayoutPanel.
ImageViewer imageViewer = new ImageViewer();
                imageViewer.Dock = DockStyle.Bottom;
                imageViewer.LoadImage(imageFilename, 256, 256);
                imageViewer.Width = size;
                imageViewer.Height = size;
                imageViewer.IsThumbnail = true;
                imageViewer.MouseClick += new MouseEventHandler(imageViewer_MouseClick);
                imageViewer.KeyDown += new KeyEventHandler(imageViewer_KeyDown);
                this.OnImageSizeChanged += new ThumbnailImageEventHandler(imageViewer.ImageSizeChanged);
                this.flowLayoutPanelMain.Controls.Add(imageViewer);
 
My problem is that all keys are handled correctly, except the up/down/left/right keys.
Any idea what could be the problem ?
QuestionCould not find type 'marlie.TumbnailDotnet.ThumbnailFlowLayoutPanel'memberTimothy Tandian4 Jul '12 - 23:32 
I just downloaded thumbnaildotnet_demo.zip, it contains MainForm.cs. I want to open MainForm.cs[design], but there was an error. It's said "Could not find type 'marlie.TumbnailDotnet.ThumbnailFlowLayoutPanel'. Please make sure that the assembly that contains this type is referenced. If this type is a part of your development project, make sure that the project has been successfully built using settings for your current platform or Any CPU."
 
Did I forget something? what is marlie.TumbnailDotnet?
 
regard, timothy t.
QuestionCan I add printing feature in this project like windows photo viewer?????memberA7mad_3 Mar '12 - 5:24 
Can I add printing feature in this project like windows photo viewer????? D'Oh! | :doh:
GeneralMy vote of 5membermanoj kumar choubey1 Mar '12 - 22:53 
nice
QuestionAutoScroll IssuesmembermLichy30 Jul '11 - 13:50 
Hello,
 
I've run into the issue you described above, with my scrollbar jumping to the top again after selecting an image. I am not docking to the bottom, but I am filling for the dock style/type. So would this cause the same issue?
 
I have the panel inside of a split container, and I need it to be constrained to that.
 
Does anyone have any ideas?
 
Thanks!
AnswerRe: AutoScroll IssuesmembermLichy6 Aug '11 - 18:41 
I fixed the issue. Inside the ImageViewer OnClick function, I had to remove the anchor to bottom line of code.
GeneralNewbie: Binding to databasemembermanuel ale28 Mar '11 - 4:42 
Could you give some tips (help me) about how to: 1) display images stored in a database
2) Is an ImageList right for doing that?
3) I don't want the flying window, just a picturebox showing the selected image
4) How can I stop/resume scanning, when resuming show only the new images.
QuestionModernize?memberdragonrose9 Feb '11 - 6:12 
Now that .net 4 is out, and visual studio 2010, do they bring anything new to the table which could be use din this to increase performance etc?
GeneralCreate checkboxesmemberLaserson24 Apr '10 - 5:18 
Hi,
 
is it possible to append a checkbox to every thumbnail to get some CheckBoxCollection (like in ListView control with enabled checkboxes)?
GeneralImageviewer with Previous and Next buttonsmemberMember 446046322 Jul '09 - 13:13 
Hi Marc,
 
Thanks for the Article. I am using your example to create a Form with Thumbnail Viewer and Image Dialog in the same window. The "Image Dialog" form needs to have "Prev" and "Next" buttons. I created a user control for "Image Dialog" and added these buttons. I used an ArrayList and loaded all the image names while loading images into Thumbnail viewer. I was able to load the image from Viewer to "Image Dialog" control on mouse click. But when I use "Prev" and "Next" buttons I couldn't highlight back the image in the Viewer. Any suggestions to achieve this functionality?
 
Thanks for any suggestions.
GeneralRe: Imageviewer with Previous and Next buttonsmemberMarc Lievin24 Jul '09 - 1:49 
Hi,
 
what I would do is:
- in private void imageViewer_MouseClick(object sender, MouseEventArgs e), after m_ImageDialog.SetImage(m_ActiveImageViewer.ImageLocation), I would add a reference to this.flowLayoutPanelMain in m_ImageDialog with Set...
- then in ImageDialog, you could simply know who is selected if you extend ThumbnailFlowLayoutPanel with enough information (ArrayList) when you add a control. e.g. GetControlFrom..., GetPreviousControl..., GetNextControl...
- Finally call Control.Select to select the control once you have a reference to it.
 
Hope this help.
 
Marc.
 
Marc Lievin

Newsthumbnail image viewermembertiffviewer15 Apr '09 - 7:14 
You can try this thumbnail image viewer which can be used as tiff viewer for silverlight , basically it is just a winform webform asp.net tiff viewer
Generalout of memorymemberXiaojun Lei16 Sep '08 - 17:35 
Hi there,
 
It is a nice appl. I tried it with a folder with more than 20,000 photos and it failed loading all of them. The error is "out of memory". Obviously, Picasa has no such an error. Could you figure out how to handle this error? Thanks alot!
 
Xiaojun Lei
GeneralRe: out of memorymemberramaluciano31 Mar '09 - 8:41 
Yes, I have the same error.
Can you correct this ????
 
Bye
GeneralRe: out of memorymemberMarc Lievin24 Jul '09 - 1:51 
Wow! That's not a one minute fix!
 
You will have to implement a dynamic cache of the images, why don't you try yourself and send us examples?
 
Marc Lievin

GeneralRe: out of memorymemberThomas-H.4 Aug '09 - 5:42 
A possible solution would be to compute the total number of images inside the directory and add placeholders with the final thumbnail size to get the correct scrollrange. Then just load and paint the current shown images. When the user scrolls, drop all images that left the viewable portion and load all new images shown.
 
Of course you could still run out of memory even if only 10 or less images are shown. but then who would try to watch 20000 pictures at once with less than 20 GB of real physical ram?
Questionpixellation [modified]memberMember 41553029 Aug '08 - 19:16 
Hi, i have edited your thumbnail viewer to make the pictures show on the same form (ie don't open a new window). However, they dont seem to use the best resizing method and are quite low quality.
 
All i have done is create a panel in the main form and use the methods from ImageDialogue.cs
 
Any ideas on how to improve quality?
 
I can send you the source code if you like.
 
Cheers
 
Leigh van der Merwe
 
modified on Sunday, August 10, 2008 1:24 AM

AnswerRe: pixellationmemberMarc Lievin10 Aug '08 - 16:41 
If you're using the images already added to the layoutpanel, these images are downsized to (256,256). see MainForm.AddImage line 96. and that's for obvious performance reasons...
 
So, if you want to show one full size/quality image, you have to load it again without downsizing it.
 
Marc Lievin

QuestionKeyDown?memberAximili29 Jun '08 - 3:02 
This is very good! Thank you for sharing it.
How would you navigate through the images using keyboard?
ImageViewer seems to ignore the KeyDown event.
I tried using form's KeyPreview, which didn't work either, any idea?
AnswerRe: KeyDown?memberMarc Lievin10 Aug '08 - 16:45 
I would rather look in the layoutpanel, that one has control on the images.
FlowLayoutPanel is derived from Panel, derived from ScrollableControl, which has keydown if you want to override it...
 
Marc Lievin

GeneralRe: KeyDown?memberAximili11 Aug '08 - 13:20 
Thanks Marc,
 
I don't really remember but I think I tried that and the panel couldn't get focus.
Anyway, what I did was put a textbox behind the panel (so that it's invisible) and handle the keyboard event there.
Doesn't seem like the right way but it works Poke tongue | ;-P
Thanks again Smile | :)
QuestionRight click : contextmenumemberUltraWhack5 Jun '08 - 1:40 
Nice work ! Right now rightclicking on the control brings up the image viewer. What if we need to make leftclick return filepath+filename and rightclick should open contextmenu to preview imageviewer ?
QuestionMore features like in PicasamemberEylon Yogev.1 Nov '07 - 0:28 
This is very useful. But I'd like to have more features like Picasa has. For example, I'd like to be able to move the thumbnails manually with the mouse like in Picasa. That really would be great.
 
And another question:
Is there a way to set the FlowLayoutPanel to NOT autoset the location of the photos? I want to auto arrange them on my demand.
 
Thanks!
AnswerRe: More features like in PicasamemberMarc Lievin10 Aug '08 - 16:48 
"For example, I'd like to be able to move the thumbnails manually with the mouse like in Picasa. That really would be great."
 
I would like it too Smile | :)
 
you may succeed if you implement your own LayoutPanel... quite a lot of work...
 
Marc Lievin

GeneralNice Article & programmemberGeertD3 Sep '07 - 20:24 
Hi Marc,
 
Nice article & program....this is almost (it's C#, not VB.NET, but I'll fix that Laugh | :laugh: ) exactly what I was looking for in my app.
 

greetz
 
Geert

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

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130523.1 | Last Updated 30 Aug 2007
Article Copyright 2007 by Marc Lievin
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid