
Introduction
The ImageViewer UserControl is something I created to fill in a gap I experienced in displaying images on my forms. I wanted to be able to zoom in, zoom out, rotate my images and best of all, not having to scale my images down to make it fit inside my forms.
I have looked around and found solutions such as dragging PictureBoxes inside of a panel, potentially good but it had it's issues. With this article I want to share my work to those who might be having this very same issue.
Properties of the User Control
AllowDrop |
bool |
A property to enable or disable Drag and Drop onto the control. |
BackgroundColor |
Color |
A property to get or set the Background Color on the picture panel. |
Image |
Bitmap |
A property to get or set the Image Displayed by the control. |
ImagePath |
string |
A property to set the Physical path to an Image (C:\Image.jpg). |
MenuColor |
Color |
A property to adjust the color used by the entire Menu. |
MenuPanelColor |
Color |
A property to adjust the color used by the Menu panel only. |
OpenButton |
bool |
A property to enable or disable the Open button on the UserControl Menu. |
PreviewButton |
bool |
A property to enable or disable the Preview toggle button on the UserControl Menu. |
NavigationPanelColor |
Color |
A property to adjust the color used by the Navigation panel only. |
PreviewPanelColor |
Color |
A property to adjust the color used by the Preview panel only. |
PreviewText |
string |
A property to edit the text of the preview label. |
TextColor |
Color |
A property to adjust the color used by all labels. |
NavigationTextColor |
Color |
A property to adjust the color used by the navigation label. |
PreviewTextColor |
Color |
A property to adjust the color used by the preview label. |
Rotation |
int |
A property to get or set the Image rotation in degrees (0, 90, 180 or 270 degrees). |
ShowPreview |
bool |
A property to enable or disable to preview panel. |
Zoom |
int |
A property to get the amount of zoom in percent. |
OriginalSize |
Size |
A property to get the Original Size of the Image. |
CurrentSize |
Size |
A property to get the Current Size of the Image. |
Events of the User Control
AfterRotation |
An event that is fired after rotating the image.
| Available properties: |
Rotation |
int |
A property that gets the Image rotation in degrees (0, 90, 180 or 270 degrees). |
|
AfterZoom |
An event that is fired after zooming the image in or out.
| Available properties: |
Zoom |
int |
A property that gets the amount of zoom in percent. |
InOut |
KpZoom |
A property that returns if it was a ZoomIn action or ZoomOut action. |
|
Using the code
As with any UserControl it is as easy as dragging it onto your form. To get the control in your Toolbox perform the following steps:
- Step 1: Right click on the Toolbox and click Choose Items...
- Step 2: Inside the .NET Framework Components click on the Browse button.
- Step 3: Browse to the extracted folder and select "KP-ImageViewerV2.dll".
- Step 4: Make sure the KpImageViewer is checked and click Ok.
The ImageViewer has a built-in Open Image button which can be used. If this however not what you want you can Set the Image programmetically and Disable the Open button by setting the OpenButton property to false.
private void Form1_Load(object sender, EventArgs e)
{
kpImageViewer.OpenButton = false;
kpImageViewer.Image = new Bitmap(@"C:\chuckwallpaper.jpg");
}
Also there are 3 rotation functions that can be used. Pretty straight forward:
private void Form1_Load(object sender, EventArgs e)
{
kpImageViewer.Rotate90(); kpImageViewer.Rotate180(); kpImageViewer.Rotate270(); }
User Control Code
The KpImageViewer class is derived from the System.Windows.Forms.UserControl class. The class uses 2 seperate classes and another UserControl. The DrawEngine and the DrawObject are the classes used and the UserControl is a DoubleBufferedPanel.
public class PanelDoubleBuffered : System.Windows.Forms.Panel
{
public PanelDoubleBuffered()
{
this.DoubleBuffered = true;
this.UpdateStyles();
}
}
public partial class KpImageViewer : UserControl
{
private KP_DrawEngine drawEngine;
private KP_DrawObject drawing;
...
}
The DrawEngine is responsible for storing a bitmap in memory with the exact size of the panel. It will be used to render the image in memory and draw it to the panel. The DrawEngine will recreate the memory bitmap on resizes to keep the height and width equal to the panel.
public void InitControl()
{
drawEngine.CreateDoubleBuffer(pbFull.CreateGraphics(), pbFull.Width, pbFull.Height);
}
private void KP_ImageViewerV2_Resize(object sender, EventArgs e)
{
InitControl();
drawing.AvoidOutOfScreen();
UpdatePanels(true);
}
The DrawObject has all the actual functionality of the Viewer. It is responsible for storing the original image in memory, Zooming, Rotation, Dragging and Jumping to the origin (The position clicked on the Preview panel). These functions are called by events triggered inside of the KpImageViewer class. As example a snippet of the mouse functions. These are responsible for the dragging and selection of the image:
private void pbFull_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
if (this.IsKeyPressed(0xA0) || this.IsKeyPressed(0xA1) || selectMode == true)
{
pbFull.Cursor = Cursors.Cross;
shiftSelecting = true;
ptSelectionStart.X = e.X;
ptSelectionStart.Y = e.Y;
ptSelectionEnd.X = -1;
ptSelectionEnd.Y = -1;
}
else
{
drawing.BeginDrag(new Point(e.X, e.Y));
if (grabCursor != null)
{
pbFull.Cursor = grabCursor;
}
}
}
}
private void pbFull_MouseUp(object sender, MouseEventArgs e)
{
if (shiftSelecting == true)
{
Rectangle rect = CalculateReversibleRectangle(ptSelectionStart, ptSelectionEnd);
ptSelectionEnd.X = -1;
ptSelectionEnd.Y = -1;
ptSelectionStart.X = -1;
ptSelectionStart.Y = -1;
shiftSelecting = false;
Point ptPbFull = PointToScreen(pbFull.Location);
drawing.ZoomToSelection(rect, ptPbFull);
pbFull.Refresh();
UpdatePanels(true);
}
else
{
drawing.EndDrag();
UpdatePanels(true);
if (dragCursor != null)
{
pbFull.Cursor = dragCursor;
}
}
}
private void pbFull_MouseMove(object sender, MouseEventArgs e)
{
if (shiftSelecting == true)
{
ptSelectionEnd.X = e.X;
ptSelectionEnd.Y = e.Y;
Rectangle pbFullRect = new Rectangle(0, 0, pbFull.Width - 1, pbFull.Height - 1);
if (pbFullRect.Contains(new Point(e.X, e.Y)))
{
Rectangle rect = CalculateReversibleRectangle(ptSelectionStart, ptSelectionEnd);
DrawReversibleRectangle(rect);
}
}
else
{
drawing.Drag(new Point(e.X, e.Y));
if (drawing.IsDragging)
{
UpdatePanels(false);
}
else
{
if (this.IsKeyPressed(0xA0) || this.IsKeyPressed(0xA1) || selectMode == true)
{
if (pbFull.Cursor != Cursors.Cross)
{
pbFull.Cursor = Cursors.Cross;
}
}
else
{
if (pbFull.Cursor != dragCursor)
{
pbFull.Cursor = dragCursor;
}
}
}
}
}
Function: AvoidOutOfScreen() The function is used to avoid your Image floating off outside of the panel. It is programmed to make sure the Image is never leaving the top left corner (X: 0, Y: 0).
The main issue I had here is that as soon as you drag around your Image that you don't want the X or Y coordinates to become higher than zero. This on itself is no issue but it comes up as soon as you start looking at the boundingBox.Left and boundingBox.Top. These values will be negative opposed to the boundingBox.Width and boundingBox.Height. Adding these values together would end up in incorrect values and would make the image drag incorrectly.
I needed a function to make sure that the X, Y coordinates are never higher than zero and never lower than the (Image width - PanelWidth) - ((Image width - PanelWidth) * 2)
With a viewer of 480x320 and an image of 1024x768 you would get this formula:
(1024 - 480 - ((1024 - 480) * 2)) = -544
This would mean that the minimum X value would be -544 to avoid getting a floating image on the right side.
Here is another visual example of how it works. The image here is 512x384. (Note that this is merely a rectangle and that the actual image is not drawn off screen)
You can see here that the minimum value of X would be -234. If it would go lower than that you would end up with empty space on the right side of the panel.

public void AvoidOutOfScreen()
{
try
{
if (boundingRect.X >= 0)
{
boundingRect.X = 0;
}
else if ((boundingRect.X <= (boundingRect.Width - panelWidth) - ((boundingRect.Width - panelWidth) * 2)))
{
if ((boundingRect.Width - panelWidth) - ((boundingRect.Width - panelWidth) * 2) <= 0)
{
boundingRect.X = (boundingRect.Width - panelWidth) - ((boundingRect.Width - panelWidth) * 2);
}
else
{
boundingRect.X = 0;
}
}
if (boundingRect.Y >= 0)
{
boundingRect.Y = 0;
}
else if ((boundingRect.Y <= (boundingRect.Height - panelHeight) - ((boundingRect.Height - panelHeight) * 2)))
{
if((boundingRect.Height - panelHeight) - ((boundingRect.Height - panelHeight) * 2) <= 0)
{
boundingRect.Y = (boundingRect.Height - panelHeight) - ((boundingRect.Height - panelHeight) * 2);
}
else
{
boundingRect.Y = 0;
}
}
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show("ImageViewer error: " + ex.ToString());
}
}
Function: ZoomToSelection() A new feature in version 1.2, selecting an area to zoom in on. Here we calculate the position and the amount of zoom that fits with the selection we've passed in. We also pass in a Point variable to the X,Y coordinates of the Panel
PointToScreen()
public void ZoomToSelection(Rectangle selection, Point ptPbFull)
{
int x = (selection.X - ptPbFull.X);
int y = (selection.Y - ptPbFull.Y);
int width = selection.Width;
int height = selection.Height;
int selectedX = (int)((double)(((double)boundingRect.X - ((double)boundingRect.X * 2)) + (double)x) / zoom);
int selectedY = (int)((double)(((double)boundingRect.Y - ((double)boundingRect.Y * 2)) + (double)y) / zoom);
int selectedWidth = width;
int selectedHeight = height;
if (zoom < 1.0 || zoom > 1.0)
{
selectedWidth = Convert.ToInt32((double)width / zoom);
selectedHeight = Convert.ToInt32((double)height / zoom);
}
double zoomX = ((double)panelWidth / (double)selectedWidth);
double zoomY = ((double)panelHeight / (double)selectedHeight);
double newZoom = Math.Min(zoomX, zoomY);
if (newZoom * 100 < Int32.MaxValue && newZoom * 100 > Int32.MinValue)
{
SetZoom(newZoom);
selectedWidth = (int)((double)selectedWidth * newZoom);
selectedHeight = (int)((double)selectedHeight * newZoom);
int offsetX = 0;
int offsetY = 0;
if (selectedWidth < panelWidth)
{
offsetX = (panelWidth - selectedWidth) / 2;
}
if (selectedHeight < panelHeight)
{
offsetY = (panelHeight - selectedHeight) / 2;
}
boundingRect.X = (int)((int)((double)selectedX * newZoom) - ((int)((double)selectedX * newZoom) * 2)) + offsetX;
boundingRect.Y = (int)((int)((double)selectedY * newZoom) - ((int)((double)selectedY * newZoom) * 2)) + offsetY;
AvoidOutOfScreen();
}
}
Feature: Drag and Drop! (New in Version 1.2) It's now supported! When the AllowDrop is set to true the panel will accept the dragging and dropping of files onto it. For this I overloaded the existing AllowDrop property on the UserControl as following:
public override bool AllowDrop
{
get
{
return base.AllowDrop;
}
set
{
this.pbFull.AllowDrop = value;
base.AllowDrop = value;
}
}
Nothing fancy there, I just needed to make sure that the panel would have the same AllowDrop value as the UserControl itself. As for the actual Drag and Drop code:
private void pbFull_DragDrop(object sender, DragEventArgs e)
{
try
{
string[] FileList = (string[])e.Data.GetData(DataFormats.FileDrop, false);
Image newBmp = null;
for (int f = 0; f < FileList.Length; f++)
{
if (System.IO.File.Exists(FileList[f]))
{
string ext = (System.IO.Path.GetExtension(FileList[f])).ToLower();
if (ext == ".jpg" || ext == ".jpeg" || ext == ".gif" || ext == ".wmf" || ext == ".emf" || ext == ".bmp" || ext == ".png" || ext == ".tif" || ext == ".tiff")
{
try
{
newBmp = Bitmap.FromFile(FileList[f]);
this.Image = (Bitmap)newBmp;
break;
}
catch
{
}
}
}
}
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show("ImageViewer error: " + ex.ToString());
}
}
private void pbFull_DragEnter(object sender, DragEventArgs e)
{
try
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
e.Effect = DragDropEffects.Copy;
}
else
{
e.Effect = DragDropEffects.None;
}
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show("ImageViewer error: " + ex.ToString());
}
}
Points of Interest
This was my second attempt to creating an ImageViewer with these functionalities. My first try involved a PictureBox being dragged over a Panel. This originally seemed to work rather well up but couldn't provide a well working zooming system (Heap size issues) on larger images. This viewer can zoom endlessly without using extra memory.
Known Issues
- Scrolling to Zoom In or Zoom out will not work if the Control doesn't have focus. (Clicking on the Control or Image is enough to regain focus).
Version History
Version 1.3.5: (June 21, 2010) Full change list:
- Fixed further Multi-Page TIFF rotation issues.
Version 1.3.4: (June 19, 2010) Full change list:
- Fixed Multi-Page TIFF rotation.
- Fixed opening images through UNC paths.
- Fixed positioning of the Multi-Page menu.
- Fixed double Try and Catch when opening non-image formats (*.txt for example).
Version 1.3.3: (May 6, 2010) Full change list:
- Added Image support for EMF/WMF. (Thank you wsmwlh!)
- Drag and Drop now also accepts EMF/WMF.
- Try and Catch on the Multi-Page check (Crashed on WMF files).
- Fixed NullReference when opening non-image formats (*.txt for example) after opening a Multi-Page Tiff Image.
Version 1.3.2: (May 5, 2010) Full change list:
- Fixed some further issues with the navigation panel position.
Version 1.3.1: (May 5, 2010) Full change list:
- Fixed inappropriate Navigation panel position when hiding the preview panel. (Thank you wsmwlh for reminding me!)
Version 1.3: (May 5, 2010) Full change list:
- Added Multi-Page TIFF support (Request).
- Added additional properties for multi page navigation.
- Cleaned up the solution. (Removed duplicate images and their references).
Version 1.2: (April 26, 2010) Reuploaded demo project & source files because of too many resource images (Did work nonetheless but it wasn't very pretty!)
Version 1.2: (April 23, 2010) Full change list:
- Added Drag-and-Drop functionality (Request).
- Added the possibility to zoom in on a Selected Area (Selection Zoom).
- Added a new button and shortcut (Shift + MouseClick) for the use of Selection Zoom.
- Added single-page TIF support.
- Fixed a ReadOnly bug when opening Read Only images.
- Added comments to alot of code to make it more clear.
- Fixed several minor bugs.
Version 1.1.1: (April 14, 2010) Full change list:
- Slight bug fixed in drawing after preview panel has been hidden.
- Fixed bug on incorrect collapsing of the preview panel.
Version 1.1: (April 14, 2010) Full change list:
- Added addition Color properties for individual color changes
- Background Color (Picture panel)
- Preview Label Color
- Individual Color possibility for the Menu
- Fixed a bug in
AvoidOutOfScreen() when dealing with wide images.
- Optimized the rendering of the preview image.
- Dragging is now possible inside the preview panel.
- The preview panel can now be enabled or disabled.
- Button for users to control the preview panel.
- Can be forced inside your code (See property:
ShowPreview & PreviewButton)
- Zooming is now possible by entering a specific number inside the ComboBox and pressing Enter.
- Added some fancy hand & drag cursors.
Version 1.0: (April 7, 2010) First public release build.
Conclusion
Creating this was a really fun experience for me and I hope that alot of you will find it as usefull control for your projects. While I believe that the control works great I also believe that there is plenty of room for improvements. The source is also supplied for those who want to work with it. I do ask that if any bugs are found and/or improvements are made to the code to also submit it here so that everybody can enjoy your work aswell! Thank you for reading and maybe till the next article.
Credits
I'd like to thank NT Almond (Norm .net) for his article
Flicker free drawing using GDI+ and C#. The UserControl uses this technique for it's flicker free drawing on the panel.