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:
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:
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();
}
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)
{
if (this.InvokeRequired)
{
this.Invoke(Delegate , object[] );
}
else
{
}
}
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