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.

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:

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:

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.

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.

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:

Comments and Discussions

Hello Guy, I create picture box in the main class and declared string array. And then, I coded to fill pictures in picture boxes and create doubleclick eventhandler in button click. In DoubleClick event, I code to open the picture with photo viewer but it returns last index of pictures. I want to show photo when I click on each photo. Plz help how to fix it. Thank a lot

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

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.

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.

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?

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.

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!

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?

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?

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
Thanks again

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 ?

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

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 . (but comparing to Picasa is not fair because there are using a database).

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?

"(...) 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

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

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.

Hi,
The same thing only I told you in my previous comment . 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 ). so It will try to access the main object that time you will get the above error that I mentioned in the post 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

I agree it may happen on your system, I just cannot reproduce it with huge folders and debug mode on a 4 years old laptop...

Anyway, the error message tells us that the thread is still running but the mainform is already disposed, I think the fix should be then in the controller thread or in the dispose method of the mainform, I found for example something that should not be:

Well, I only disagree with your solution, I think that one should respect patterns and docs as much as possible, background thread is the correct solution and if there are some issues, then I will look for them because I find the problem interesting

for example, in Managed Threading Best Practices, as advice, "Don't use Thread.Abort to terminate other threads. Calling Abort on another thread is akin to throwing an exception on that thread, without knowing what point that thread has reached in its processing."

So, in conclusion, thanks for pointing out the problem, GUI multithreading is never an easy topic.

- set FlowLayoutPanel.WrapContents to false, then the control will not wrap the childs anymore, because you are going from left to right, you simulate Filmstrip, the horizontal scrollbar appears.

- you can order the thumbnails from top to the bottom with FlowLayoutPanel.FlowDirection = TopDown and set imageViewer.Dock = DockStyle.Left, then you will have something like a Filmstrip with more than one line of images.

Nice demo, but I did discover a crossthreading issue. Every time you use asynchronous functionality, you must always be aware when setting properties on objects in another thread. This is always the base in Form operations. There is an easy fix to this and it is just to check the InvokeRequired property of the control. Like so:

Once more small bug. When you scroll down to few more images... when a thumbnail is double clicked, it just scrolls back up to the top and does not display a message. The displayed messages only appear when a thumbnail from the 'top-view' is double clicked.