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   
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
GeneralRe: Nice Article & programmemberMarc Lievin4 Sep '07 - 2:44 
Hi,
 
I am happy this helps you,
 
Marc.
GeneralSpeed up suggestionsmembertomi_231 Aug '07 - 5:10 
Hi,
 
Nice work here. I implemented a thumbviewer too (I used ListView and ImageList).
 
I was unsatisfied with it's speed, and tried to find a solution.
 
1. Loading the image:
Instead of Image.FromFile use the following:

Stream fs = new FileStream ( bla bla ); // or any stream
Image pic = Image.FromStream(fs, true, false);
 
//... other
fs.Close();

 
The third parameter of the Image.FromStream method is bool validateImageData. If it's false, no validation is done, but in return a slight speedup is gain.
This method seems to use asyncronous I/O, and this is why I don't close it right after the FromStream call (it happeded to me that for larger images it closed the stream before it could have been loaded.
 
2. Resize the Image:
If you don't use that big images (256x256), which, in my opinion are too big for a thumbviewer, you may gain some more speed with
 
Image thumb = pic.GetThumbnailImage(blablabla);
 
This will speed up a little bit more if the image size doesn't exceed 140x140 (according to MSDN).
 
Bye

 
Tomi

GeneralRe: Speed up suggestionsmemberMarc Lievin31 Aug '07 - 6:29 
Hi,
 
thx for the tips, especially the Stream technique.
 
Actually, I used FromFile with try...catch because the controller works asynchronously and is supposed to check if the path contains a valid Bitmap or not. It may be anything.
 
But I will certainly use it when I am sure that I am loading a Bitmap format (e.g. from Database).
 
I did not use GetThumbnailImage for the reason you pointed out: 256x256. I agree with you, it is not a thumbnail anymore but Picasa is showing thumbs from 64x64 to 256x256, so did I Smile | :) . (but comparing to Picasa is not fair because there are using a database).
 
Cheers,
 
Marc.
GeneralRe: Speed up suggestionsmemberrapid2k24 Sep '07 - 1:14 
Hi there,
 
Marc Lievin wrote:
Actually, I used FromFile with try...catch because the controller works asynchronously and is supposed to check if the path contains a valid Bitmap or not. It may be anything.

 
Because I was understanding how your appl works, I commented out that part. I wanted to speed up things a bit by not checking if images were valid (that would become later).
 
The point is that if I comment out that block, thumbnails are shown like 4 or 5 each time. Thumbs don't get rendered one by one, as before. I think that this could be because system is busy decoding/creating thumbnails, and there is no spare time for the UI to paint the new thumbs. But I don't know. Do you have any idea? How can the UI be forced to render the thumbnails one by one?
 
Thanks in advance.
 
Thanks in advance.
GeneralRe: Speed up suggestionsmemberMarc Lievin4 Sep '07 - 2:42 
Hi,
 
"(...) and there is no spare time for the UI to paint the new thumbs.",
 
I think you're right.
 
If you want to force the UI rendering, call Refresh(),
 
but if you call refresh for the FlowLayoutPanel, only the panel will be rendered, if you call this.Refresh() after adding one image, the whole Form will be rendered but I am not sure this will help for the interactivity, only for rendering.
 
The way I would to do it, would be with a Queue: you have a queue in your control and a thread will process it at its pace, currently the controller and the panel are working at the same speed, if you change that you get traffic jam Smile | :)
 
Cheers,
 
Marc.
GeneralRe: Speed up suggestionsmemberLewis Delport6 Nov '08 - 22:57 
Had the same problem.... the fix:
 
Private Sub FlowLayoutPanelMain_ControlAdded(ByVal sender As Object, ByVal e As System.Windows.Forms.ControlEventArgs) Handles FlowLayoutPanelMain.ControlAdded
e.Control.Refresh()
End Sub
 
thx for this great article.. i've managed to put a explorer to the left of it and then it shows all images on the right according to what folder i'm in... thx again
JokeError !memberajai808521 Aug '07 - 22:47 
Hi , This is a nice application , but since the thread is Background thread we can close the main thread by pressing the close button in the control box Big Grin | :-D . so the following error will come . better abandon the thread on close of the form, since this is background thread . if you avoid this just make it

Thread thread = new Thread(new ParameterizedThreadStart(AddFolder));
// thread.IsBackground = true;
thread.IsBackground = false;//Modified to

The above code will make the main thread to wait for the threads spun by it to get finish . "This will cause the application to run for a while in tak manager even if its window is closed " so I feel the better is to 'Abort()' on close of the form ...
 

 

private void AddImage(string imageFilename)
{
// thread safe
if (this.InvokeRequired)
{
this.Invoke(m_AddImageDelegate, imageFilename);//Show Error here if the I press Cancel button
 
System.ObjectDisposedException was unhandled
Message="Cannot access a disposed object.\r\nObject name: 'MainForm'."
Source="System.Windows.Forms"
ObjectName="MainForm"
StackTrace:
at System.Windows.Forms.Control.MarshaledInvoke(Control caller, Delegate method, Object[] args, Boolean synchronous)
at System.Windows.Forms.Control.Invoke(Delegate method, Object[] args)
at marlie.TumbnailDotnet.MainForm.AddImage(String imageFilename) in D:\Unearth\Thumbnail\ThumbnailDotnet\MainForm.cs:line 89
at marlie.TumbnailDotnet.MainForm.m_Controller_OnAdd(Object sender, ThumbnailControllerEventArgs e) in D:\Unearth\Thumbnail\ThumbnailDotnet\MainForm.cs:line 63
at marlie.TumbnailDotnet.ThumbnailController.AddFolderIntern(String folderPath) in D:\Unearth\Thumbnail\ThumbnailDotnet\ThumbnailController.cs:line 103
at marlie.TumbnailDotnet.ThumbnailController.AddFolderIntern(String folderPath) in D:\Unearth\Thumbnail\ThumbnailDotnet\ThumbnailController.cs:line 115
at marlie.TumbnailDotnet.ThumbnailController.AddFolder(Object folderPath) in D:\Unearth\Thumbnail\ThumbnailDotnet\ThumbnailController.cs:line 70
at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.ThreadHelper.ThreadStart(Object obj)
 
}

 

 

 
Warm Regards
Ajai NP

GeneralRe: Error !memberMarc Lievin21 Aug '07 - 23:02 
Hi,
 
thx for the suggestion, I will add your improvement it in the next version!
 
Marc.
GeneralRe: Error !memberMarc Lievin29 Aug '07 - 23:02 
Hi,
 
I have read carefully your comment and I would just like to explain why the thread is background:
 
in msdn, "A managed thread is either a background thread or a foreground thread. Background threads are identical to foreground threads with one exception: a background thread does not keep the managed execution environment running."
 
that's exactely what I want, I want to be able to close the application even if the thread is running.
 
If the application is then crashing when closing, it would be nice of you if you could give more details because I cannot reproduce it.
 
Thx,
 
Marc.
GeneralRe: Error !memberajai80852 Sep '07 - 22:02 
Hi,
The same thing only I told you in my previous comment Sleepy | :zzz: . If you wanna replicate the same you just need to have select a root folder which contains several sub directories each with 100's(to make the thread slow ) of images in it . Now just open that root folder to load the images in thumb nail view , so that the Back ground thread will run now press the close button in the control box (I hope you are in Debug mode) . There is a problem with back ground thread . It wont be knowing about whether the mail execution has finished (That is why you should abort it from the main execution before exiting from app Cry | :(( ). so It will try to access the main object that time you will get the above error that I mentioned in the post Smile | :) I know you knows it well .
 
Remember that this wont replicate all the times , if you choose a folder with lot of big size pictures and while loading that images if you press the close button then only it will replicate


 
Warm Regards
Ajai NP

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

Permalink | Advertise | Privacy | Mobile
Web03 | 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