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:
- Pan
- Zoom
- Navigation from image to image
- 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.
The coordinate translation is either from or to one another.
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)
{
switch (previewMode)
{
case PreviewMode.REGIONSELECTION:
if (e.Button==MouseButtons.Left)
{
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)
{
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:
displayCenter=new Point(displayCenter.X+(int)((e.X-this.Width/2)/mZoom),
displayCenter.Y+(int)((e.Y-this.Height/2)/mZoom));
mZoom *= 2;
ZoomImage();
break;
case PreviewMode.ZOOMOUT:
displayCenter = new Point(displayCenter.X +
(int)((e.X - this.Width / 2) / mZoom),
displayCenter.Y + (int)((e.Y - this.Height / 2) / mZoom));
mZoom /= 2;
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:
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:
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
{
if (cmbZoom.Text.IndexOf('%') != -1)
{
img.ZoomImage(double.Parse(cmbZoom.Text.Trim('%')) / 100.0);
}
else
{
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 switch … case
or if … else
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:
public interface IZImage
{
int ImageCount{ get;}
int CurrentIndex { get;}
Image GetNextImage();
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
{
string m_DirectoryName;
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.
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.