Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Extended Image Viewer

0.00/5 (No votes)
4 Mar 2009 1  
A picture control with enhanced features.

img2.jpg

Contents

Introduction

A while ago, I was part of a team developing a digital archive system for a governmental land administration office. Basically, what the system attempts to do is organize and digitalize the physical files in the archive of the administration to provide a digital copy of the archive. This system targeted to solve many of the problems linked with the speed of document circulation, concurrent access, and many more.

One of the benefits revealed by this system is a detailed and thorough access to the document pages, where the user of the system is able to view a document better than viewing it physically while going through a pile of files. Since these documents age back to decades and most of them are hand written, this feature was one of the appealing facilities of the system. The PictureBox control included in the .NET package was perfect to display a single image in a simple form, but we required an enhanced one where a better preview of the image being displayed is achieved and also needed a control that could handle more than one image.

I went through my personal project, and found one that I started on my own that could be a starting point for this solution. The version finally implemented by the digital archive system was more capable and complex, but relied on the design I forwarded.

The very basic needs when viewing a group of images for thorough access, in elementary crude terms, are:

  1. Pan
  2. Zoom
  3. Navigation from image to image
  4. Rotate

I decided to design a control that could perform these tasks easily with a more elaborate detail. The implementation of this control involved minor calculations which are basically limited to coordinate translation and scaling. Let me clarify the translation and the coordinate systems involved.

img1.jpg

The coordinate translation is either from or to one another.

img3.jpg

The ImageViewer component is the very main component of the assembly, because that is where all the computations and commands are implemented. It has almost every command that is required when viewing a single image except rotating, all of which I have reduced to a function of translation of the center of display and scaling of the zoom value. Like for example, when grabbing and panning an image, we describe the operation as a function of the center and zoom.

Pan:

  • Zoom stays the same
  • Center of Display is translated by
      • Change in the x coordinate and
      • Change in the y coordinate

Here is how it is described for all the operations:

private void pic_MouseMove(object sender, MouseEventArgs e)
{
    //when a mouse is down and moving 
    //panning and region selection are posibilities
    switch (previewMode)
    {
        case PreviewMode.REGIONSELECTION:
            if (e.Button==MouseButtons.Left)
            {
                //display a rectangular region to the selected region 
                //to notify the user what he is selecting
                int w = Math.Abs(tempCenter.X - e.X), h = Math.Abs(tempCenter.Y - e.Y);
                if (w > 1 || h > 1)
                {
                    this.Refresh();
                    Graphics gr = pic.CreateGraphics();
                    gr.DrawString("(" + (tempCenter.X + e.X) / 2 + "," + 
                                 (tempCenter.Y + e.Y) / 2 + ")", this.Font, 
                                 Brushes.Khaki, new PointF((tempCenter.X + e.X) / 2, 
                                 (tempCenter.Y + e.Y) / 2));
                    gr.DrawRectangle(Pens.Red, new Rectangle((tempCenter.X + e.X - w) / 2, 
                                    (tempCenter.Y + e.Y - h) / 2, w, h));
                    gr.Dispose();
                }
            }
            break;
        case PreviewMode.PAN:
            if (e.Button==MouseButtons.Left&&(tempCenter.X != e.X || 
                                                      tempCenter.Y != e.Y))
            {
                displayCenter = new Point(displayCenter.X + 
                    (int)((tempCenter.X- e.X ) / mZoom),
                    displayCenter.Y + (int)((tempCenter.Y-e.Y) / mZoom));
                ZoomImage();
                tempCenter = e.Location;
            }
            break;
        default:
            break;
    }
}

private void pic_MouseUp(object sender, MouseEventArgs e)
{
    //when the mouse is up its when most of the previews are commited
    switch (previewMode)
    {
        case PreviewMode.REGIONSELECTION:
            displayCenter = new Point(displayCenter.X + 
                (int)(((double)(tempCenter.X + e.X - this.Width) / 2) / mZoom),
                displayCenter.Y + (int)(((double)(tempCenter.Y + 
                e.Y - this.Height) / 2) / mZoom));

    double z = mZoom * pic.Width / Math.Abs(tempCenter.X - e.X);
    if (mZoom * pic.Height / Math.Abs(tempCenter.Y - e.Y) < z)
        z = mZoom * pic.Height / Math.Abs(tempCenter.Y - e.Y);
    mZoom = z;
    ZoomImage();
    break;
        case PreviewMode.ZOOMIN:
            //when zoom in the zoom value is simply multiplied by two
            displayCenter=new Point(displayCenter.X+(int)((e.X-this.Width/2)/mZoom),
                displayCenter.Y+(int)((e.Y-this.Height/2)/mZoom));
            mZoom *= 2;
            //redraw the image with the new zoom value
            ZoomImage();
            break;
        case PreviewMode.ZOOMOUT:
            //when zoom out the zoom value is simply divided by two
            displayCenter = new Point(displayCenter.X + 
                (int)((e.X - this.Width / 2) / mZoom), 
                 displayCenter.Y + (int)((e.Y - this.Height / 2) / mZoom));
            mZoom /= 2;
            //redraw the image with the new zoom value
            ZoomImage();
            break;
        default:
            break;
    }
}

As shown in the the code, each of the case statements are dedicated to computing the two most important variables required to display the resulting image. Once these values are computed, if you call the method ZoomImage(), it effectively redraws the image with the new values. The class diagram partially looks like:

img4.jpg

Implementation

Hosting the Image Viewer Component

This component holds all the required commands and is ready for the consumer code, but to juice up and deliver a very usable and effective viewer control, you need to host it like in a user control and provide events to trigger each of these commands. Along with this article, I have included a slick host control that is easy to use, as shown in the picture:

img5.jpg

This control has all the basic tools implemented by the tool box as in the figure. Each of the available commands is exposed by the buttons and the drop down list. For example, the zooming ability of the component is exposed by the drop down list event handler, as in the sample code below:

private void cmbZoom_SelectedIndexChanged(object sender, EventArgs e)
{
    try
    {
        //those values with % value on them they are percent values
        //send this zoom value to the component
        if (cmbZoom.Text.IndexOf('%') != -1)
        {
            img.ZoomImage(double.Parse(cmbZoom.Text.Trim('%')) / 100.0);
        }
        else
        {
            //if its an enumeration then parse the text to get the enumeration
            img.ZoomImage((ZeeImaging.ZoomMode)
              (Enum.Parse(typeof(ZeeImaging.ZoomMode), cmbZoom.Text, true)));
        }
    }
    catch
    {
        cmbZoom.Text = "";
    }
}

To reduce the amount of code I should write to extract each of the zoom mode enumerations, I resorted to parsing the string to its enumeration value; otherwise, it’s a straightforward approach to expose the zoom image method of the image viewer component. The same story goes with the tool strip button except that I have used the tag of each button to store the respective enumeration values so that I won’t have to switchcase or ifelse all the cases and don’t have to handle the Click events of all the buttons. As described in the sample code below:

private void btn_Click(object sender, EventArgs e)
{
    img.ImagePreviewMode = (PreviewMode)Enum.Parse(typeof(PreviewMode), 
                           ((ToolStripButton)sender).Tag.ToString());
    btnPan.Checked = img.ImagePreviewMode == PreviewMode.PAN;
    btnRegionZoom.Checked = img.ImagePreviewMode == PreviewMode.REGIONSELECTION;
    btnZoomIn.Checked = img.ImagePreviewMode == PreviewMode.ZOOMIN;
    btnZoomOut.Checked = img.ImagePreviewMode == PreviewMode.ZOOMOUT;
    switch (img.ImagePreviewMode)
    {
        case PreviewMode.PAN:
            this.Cursor = Cursors.Hand;
            break;
        case PreviewMode.REGIONSELECTION:
            this.Cursor = Cursors.Cross;
            break;
        default:
            this.Cursor = Cursors.Default;
            break;

    }
}

Image Source

In most, not all, cases, the source of an image is the file system where a system attempts to display an image file. But in any case, the image viewer component accepts any source of an image as long as the object implements the IZImage interface, whose definition is as shown in the code below:

/// <summary>
/// a signiture interface that when implemented by any object
/// enables it to utilize the image viewer component
/// </summary>
public interface IZImage
{
    //gets the number of images that are available for display
    int ImageCount{ get;}
    //gets the current index of the image that is being displayed
    int CurrentIndex { get;}
    //retreives the next available image
    Image GetNextImage();
    //retrieves the previously displayed image
    Image GetPreviousImage();
}

In the sample codes included with this article, there are two sample implementations of the IZImage interface in the sample.cs and PictureBoxEx.cs files. In the sample implementation, I have written a simple code that displays the images in a directory. I created a class called “DirectoryImages” and its definition is as shown below:

public class DirectoryImages:IZImage
{
    //path of the directory
    string m_DirectoryName;
    //number of the image files that are found
    int m_ImageFilesCount;

    int m_CurrentIndex=-1;
    string[] ImageFiles;

    public DirectoryImages(string str)
    {
        m_DirectoryName = str;
        ImageFiles = Directory.GetFiles(str, "*.jpg");
        m_ImageFilesCount = ImageFiles.Length;
    }

    #region IZImage Members

    public int ImageCount
    {
        get { return m_ImageFilesCount; }
    }

    public int CurrentIndex
    {
        get { return m_CurrentIndex; }
    }

    public System.Drawing.Image GetNextImage()
    {
        m_CurrentIndex++;
        if (m_CurrentIndex >= m_ImageFilesCount)
            throw new Exception("No More Images");
        return System.Drawing.Image.FromFile(ImageFiles[m_CurrentIndex]);
    }

    public System.Drawing.Image GetPreviousImage()
    {
        m_CurrentIndex--;
        if (m_CurrentIndex <= 0)
            throw new Exception("No More Images");
        return System.Drawing.Image.FromFile(ImageFiles[m_CurrentIndex]);
    }

    #endregion
}

As shown in the code, this object takes the path of a directory and displays the images in that directory with a “jpg” extension, one by one. To display any image that an object fetches from any source, implement the “IZImage” interface and pass the object to the hosting control. In order to quickly use this image viewer, read through the next section.

Quick Implementation

One of the greatest achievements of computer OOP is the abstraction of details and the fact that in order to use a class you don’t need to know how it is done. And hence, I have provided a default implementation of the hosting control in order for it to behave as a picture box; this definition can be found in the “PictureBoxEx.cs” file.

When you go through the code, you will find that there are basically two implementations: the hosting control (PictureBoxEx) and the image source class (ImageFile).

Sample Application

The sample application that I have compiled hopefully best describes the power of using this enhanced image viewer. You can use the File menu to open a single image file or to navigate through images contained in a folder by specifying the folder.

img6.jpg

Conclusion

In my conclusion, you can build complex hosting controls for this component, without even worrying about the computations involved in the component; for example, incorporate the middle mouse to zoom in and out. I would hope this component will be of great assistance to whomever that wishes to utilize it.

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