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

Multi-Image Viewer

, 29 Dec 2004
Rate this:
Please Sign up or sign in to vote.
A multi-image viewer with drag and drop source and sink capability.

Introduction

The multi-image viewer is a follow-up on my previous article, the single image viewer. This applet demonstrates:

  • receiving multiple files from a drag-drop operation
  • being a drag-drop source
  • optimizing the viewer for performance--creating thumbnails on the fly and owner draw issues
  • usability issues--changing image and form sizes and utilizing a non-paging scrollbar (see Understanding Scrollbars)

This applet is part of the prototypes in the Yet Another Photo Organizer (YAPO) open source project hosted by Wdevs here.

This code is prototype code intended to explore performance and usability issues. Keep in mind some of it is a bit hacked.

Receiving Multiple Files From A Drag-Drop

This is very simple. In particular, the application receives only filenames and directory names. You can drop a folder onto the viewer or individual files. The current implementation does recurse into sub-folders.

When a drag operation is completed, the application calls GetFiles which parses the DragEventArgs data and adds only files of "jpg", "png", or "bmp" extension. (There isn't any error checking to make sure that those files are actually real image files.)

protected ArrayList GetFiles(DragEventArgs e)
{
  ArrayList files=new ArrayList();

  if ( (e.AllowedEffect & DragDropEffects.Copy) == DragDropEffects.Copy)
  {
    Array data=((IDataObject)e.Data).GetData("FileDrop") as Array;
    if (data != null)
    {
      foreach(string fn in data)
      {
        string ext=Path.GetExtension(fn).ToLower();
        if ( (ext==".jpg") || (ext==".png") || (ext==".bmp") )
        {
          files.Add(fn);
        }
        else
        {
          string[] dirFiles=Directory.GetFiles(fn);
          foreach(string fn2 in dirFiles)
          {
            ext=Path.GetExtension(fn2).ToLower();
            if ( (ext==".jpg") || (ext==".png") || (ext==".bmp") )
            {
              files.Add(fn2);
            }
          }
        }
      }
    }
  }
  return files;
}

Why Cast To IDataObject?

This is something I discovered after I wrote my single image viewer article. When receiving a file or file list from say, Explorer, e.Data is of type DataObject and the data returned by the "FileDrop" data format type is an Array. However, if I create my own string array for sourcing a drag-drop event and try dropping it onto my single image viewer, e.Data is of type System.__ComObject. After posting a question on the C# forum and poking around some more, I discovered that System.__ComObject implements IDataObject (this is not something that's documented in IDataObject). Therefore, casting to IDataObject properly handles both System.Windows.Forms.DataObject and System.__ComObject object types.

Being A Drag-Drop Source

I wanted to be able to drag an image in the multi-image viewer to my already created single image viewer. Based on the code example for the DoDragDrop method, the primary consideration is to pay attention to the SystemInformation.DragSize value. This is a system value that the user can set to establish how much the mouse has to move while the left button is down before the motion is considered the beginning of a drag event.

On Mouse Down

On a mouse down event, the image index is obtained and the drag box, which the mouse has to move outside of, is established.

private void OnMouseDown(object sender, MouseEventArgs e)
{
  if ((e.Button & MouseButtons.Left)==MouseButtons.Left)
  {
    // ignore SystemInformation.DragSize for now
    int col=e.X / panelWidth;
    int row=(e.Y+scrollBar.Value) / panelHeight;
    int imgIdx=row * cols + col;
    if (imgIdx < files.Count)
    {
      dragImageFilename=(string)files[imgIdx];
      Size dragSize=SystemInformation.DragSize;
      dragBox=new Rectangle(new Point(e.X - dragSize.Width/2,
         e.Y - dragSize.Height/2), dragSize);
      dragging=true;
    }
  }
}

On Mouse Up

On a mouse up event, the dragging flag is cleared.

private void OnMouseUp(object sender, MouseEventArgs e)
{
  if ((e.Button & MouseButtons.Left)==MouseButtons.Left)
  {
    dragging=false;
  }
}

On Mouse Move

On the mouse move event, the mouse position is checked to be outside of the minimum drag box window. If it is, the DoDragDrop method is called. This method does not return until the left mouse button is released on an application that either accepts or does not accept the data object. If dropping on an application that does accept the data object, then this method returns as soon as the GetData call is made on the DragEventARgs.Data object.

private void OnMouseMove(object sender, MouseEventArgs e)
{
  if (dragging)
  {
    if (!dragBox.Contains(e.X, e.Y))
    {
      string[] filenames=new string[] {dragImageFilename};
      DataObject data=new DataObject(DataFormats.FileDrop, filenames);
      ((Control)sender).DoDragDrop(data, DragDropEffects.Copy);
      dragging=false;
    }
  }
}

Since I'm only enabling the Copy effect, I don't particularly care what the return value is--whether the drop operation was successful or not. Note also that instead of putting the image into the data object, I'm storing the filename to the image. This is because the viewer doesn't actually preserve the source image and I don't want to spend CPU time loading a large image file before it's actually dropped--I'd rather let the receiving application deal with it.

Optimizing The Viewer

There are a few optimization issues to consider:

  • Loading the original image files
  • Creating thumbnails
  • Using an owner draw surface

Loading The Original Image Files

Once the drag-drop operation completes, it starts a thread to load the images:

private void OnDragDrop(object sender, System.Windows.Forms.DragEventArgs e)
{
  files=GetFiles(e);
  images.Clear();
  loadImagesThread=new Thread(new ThreadStart(LoadImages));
  loadImagesThread.Start();
}

private void LoadImages()
{
  foreach(string fn in files)
  {
    try
    {
      Bitmap bitmap=new Bitmap(fn);
      Image image=new Bitmap(bitmap, 256, 256*bitmap.Height / bitmap.Width);

      // Dispose of the large image right away rather than waiting
      // for the GC to do it.
      bitmap.Dispose(); 
      lock(images)
      {
        images.Add(image);
      }
      OnSizeChanged(null, EventArgs.Empty);
    }
    catch(Exception)
    {
    }
  }
}

Using a thread allows the application to start displaying the images as they are converted to thumbnails and the user can also start changing the viewer size, the image size, and dragging images to the single viewer.

Of note here is that after the original bitmap is loaded and converted to a thumbnail, I call the Dispose method right away. If I don't, the GC doesn't get around to collecting the images for quite a while (seconds to minutes) and memory fills up very quickly, slowing down the whole machine.

The images array is locked when adding an image because the main application thread might be in the middle of accessing it.

Creating Thumbnails

Notice that after the original image is loaded, a thumbnail is created:

Image image=new Bitmap(bitmap, 256, 256*bitmap.Height / bitmap.Width);

that is proportional to the master image dimensions, with the width fixed at 256 pixels across. This seems like a good value that creates presentable images across a variety of resolutions.

Resizing images on the fly is very time consuming for large images. I had toyed with the idea of a "smart" algorithm that might create three or so different thumbnails at different sizes. The idea here is that, when a large image is being displayed, there are fewer of them on the screen. Therefore, you can increase the resolution of the thumbnail. Essentially, an algorithm that balances the number of images with the quality of the image. However, I decided not to implement that yet.

Using An Owner Draw Surface

My original idea was that the viewing surface would be comprised of Panel controls with PictureBox child controls. The first thing I discovered is that Windows renders the Form very slowly--too many controls and it gets doggy very quickly. So that threw out the whole idea of using pre-canned controls.

The owner drawn control is derived from a Panel control anchored to the Form, leaving a little bit at the top for the image size slider. The usual initialization has to be done for double-buffering along with the Paint event:

SetStyle(ControlStyles.DoubleBuffer |
    ControlStyles.UserPaint | 
    ControlStyles.AllPaintingInWmPaint, true);

Paint+=new PaintEventHandler(OnPaint);

On Paint

The OnPaint method first draws the rectangle to indicate the image frame:

private void OnPaint(object sender, PaintEventArgs e)
{
  int imgIdx=imgOffset;

  Point p=new Point(0, -vOffset);
  for (int j=0; j<rows; j++)
  {
    for (int i=0; i<cols; i++)
    {
      e.Graphics.DrawRectangle(pen, p.X, p.Y, size.Width, size.Height);
      ...

The next set of calculations determines the width and height of the image scaled to the current dimensions of the image frame. The image inside the frame has to be proportional to the thumbnail, but it also has to be based on the size that the slider sets. The slider controls the width of the image, thus the height has to be proportional to the width. However, whether the image is in landscape or portrait mode has to be determined. If in portrait mode, the height is "master" (we don't want the height to exceed the image frame because the width is smaller), and in landscape, the width is "master" (the width of the image shouldn't exceed the width of the image frame). This is all done in the following calculations:

      ...
      if ( (images != null) && (imgIdx < images.Count) )
      {
        Image image;
        lock(images)
        {
          image=(Image)images[imgIdx];
        }
        ++imgIdx;
        float fw=size.Width;
        float fh=size.Height;
        float iw=imgWidth;
        float ih=imgWidth * image.Height / image.Width;

        // iw/fw > ih/fh, then iw/fw controls ih

        // frame width is always >= image width

        float rw=fw/iw; // ratio of frame width to image width
        float rh=fh/ih; // ratio of frame height to image height

        int width;
        int height;

        // determine which dimension takes precedence
        if (rw < rh)
        {
          width=(int)fw;
        }
        else
        {
          width=(int)(iw * rh);
        }

        // scale width based on the % of the image width is filling the
        // frame
        width=(int)(width * iw/fw);

        // adjust height to maintain aspect ratio
        height=width * image.Height / image.Width;
        ...

Finally, the image should be centered in the image frame, then drawn:

        int x=(size.Width-width)/2;
        int y=(size.Height-height)/2;
        // +1 provides better centering
        e.Graphics.DrawImage(image, new Rectangle(p.X+x+1, p.Y+y+1,
            width, height));
        }
      ...

Usability Issues

Of anything, this probably took the longest time to figure out how to get right. I looked at usability from the perspective of performance and the application "feel". The performance issues are adequately addressed with a couple options to improve performance as a balance between speed and image quality. As to application feel, I discovered that it is driven entirely by the idea that the user can change the image size with the slider control.

Image Size And Frame Size

Here's the issue: when the user changes the image size, this directly affects the number of images that can be displayed horizontally. Let's take an extreme example. Given a horizontal width of 300 and an image width of 100, three images (300/100) can be displayed. Now, change the image width to 101 pixels. Now only two images can be displayed, and there's 98 pixels of wasted space.

So if we go from three 100 pixel images to two 101 pixel images, what actually needs to happen is three things:

  1. the frame needs to change to fit the new image size, which affects the number of frames that can be displayed
  2. the number of frames that can be displayed on the form should determine the frame size rather than the image size determining the frame size
  3. the image has to be centered within this frame.

Point number 2 results in some interesting behavior. Let's say that our form size is also 300 pixels. When we go to two frames (because only two images of 101 pixels width can be displayed in a 300 pixel width form), the resulting frames are actually form width / 2 in size, or 150 pixels. This means that our image is 49 pixels smaller than our frame, and as long as the user increases the image size up to 150, the image will grow in the frame, but the frame will remain the same size. Conversely, when we shrink image sizes, the frame remains the same size while the images shrink, until the image size allows more frames to be displayed.

Explaining this takes a lot longer than the actual code to calculate it:

int imgWidth=tbarImageSize.Value;

// Get the # of columns required to display the images fit to the columns.
// +4 ensures a small margin within the image viewing rectangle.
cols=pnlImages.ClientSize.Width/(imgWidth+4);
if (cols==0)
{
  cols=1;
}

// Now get the actual width of the panels, which may be larger.
// Rounding will result in an unused edge on the right of the window.
// We're not going to deal with this minor issue.
panelWidth=pnlImages.ClientSize.Width/cols;

The result is an interesting visual effect, in which the frames take up as much of the form space as possible and the images grow and shrink within the frames. I find this display nicer than leaving large amounts of dead space along one edge, which would happen if images were just displayed from left to right based on image size. It's also nicer than centering images as this creates a strange ballooning expanding/contracting effect.

Scrolling

The next thing I wanted to do was to be able to scroll by pixel. This is aesthetically pleasing. On the other hand, when the user pages up or down by clicking on the scrollbar's track, I wanted to only scroll one image row at a time. To accomplish this, I had to override how the LargeChange value works so as to maintain a proper thumb to track height ratio while at the same time allowing sub-page large change and sub-row small changes in scrolling. You can read more about that solution in the article on scrollbars.

As to scrolling the owner draw area, the algorithm is designed to display the least amount of images required to display, accounting for rows partially truncated at the top and bottom of the viewer. This is handled very simply by calculating the vertical offset of the viewer modulus the frame height and also the actual image offset in the array for the first partially viewable image:

vOffset=scrollBar.Value % panelHeight;
imgOffset=cols * (scrollBar.Value / panelHeight);

Now go back up to the OnPaint method described above and note how the starting pixel row is determined by the negative vOffset.

Conclusion

The result is a nice multiple image viewer that you can use as a kind of "test strip" for a folder of images and that supports dragging single images to another application. It makes for a good prototype to iron out all the complexities of a viewer before writing a full-blown application.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Marc Clifton

United States United States
Marc is the creator of two open source projets, MyXaml, a declarative (XML) instantiation engine and the Advanced Unit Testing framework, and Interacx, a commercial n-tier RAD application suite.  Visit his website, www.marcclifton.com, where you will find many of his articles and his blog.
 
Marc lives in Philmont, NY.

Comments and Discussions

 
QuestionHow to get one of the images 's path? Pinmemberzhenkeyu13-Feb-10 23:57 
QuestionAnother solution for thumbnails? PinmemberMarc Lievin14-Aug-07 12:01 
Hi Marc (nice firstname by the way Wink | ;-) ),
 
I would be interested in your opinion about the following solution: Image Thumbnail Viewer with .NET 2.0. Do you see any drawbacks?
 
That was an idea that was simply in my mind after I saw the flow layout control coming with 2.0.
 
I hope you don't mind if I referenced you...
 
Cheers,
 
Marc.
QuestionIs it possible to develop something similar using the Listview control? Pinmemberrradi12-Feb-07 12:28 
Questionit doesnt work well?? Pinmemberstiko6-Feb-07 4:59 
AnswerRe: it doesnt work well?? Pinmemberinsleys20-Feb-07 21:58 
GeneralThis is just what I needed. Pinmemberdbrenth4-Jan-07 8:38 
GeneralRe: This is just what I needed. PinprotectorMarc Clifton4-Jan-07 9:36 
GeneralRe: This is just what I needed. Pinmemberdbrenth8-Jan-07 5:51 
GeneralRe: This is just what I needed. Pinmemberdbrenth8-Jan-07 5:53 
QuestionCan we zoom-in / out with bottom locked calculation ? PinmemberJigar M26-Apr-06 11:26 
GeneralGood control but dont see vertical scrollbar while executing in run mode.. PinmemberMehta Jigar17-Apr-06 10:54 
Generalgreat article Pinmembercherub3251-Aug-05 13:14 
Generaldatabase source Pinmemberpaul_p_c26-Apr-05 22:59 
GeneralAn exception happened Pinmemberzhu.zheng13-Apr-05 21:18 
GeneralRe: An exception happened PinmemberMehta Jigar17-Apr-06 10:46 
GeneralGreat Control! Pinmemberjaxterama30-Dec-04 8:23 
GeneralRe: Great Control! PinprotectorMarc Clifton30-Dec-04 13:46 
GeneralRe: Great Control! Pinmemberjaxterama1-Jan-05 19:30 
GeneralAny Update? Pinmemberjaxterama11-Jan-05 4:35 
GeneralRe: Great Control! Pinmemberjaxterama23-Feb-05 11:47 
GeneralRe: Great Control! PinmemberMehta Jigar17-Apr-06 10:55 

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140709.1 | Last Updated 29 Dec 2004
Article Copyright 2004 by Marc Clifton
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid