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 have to scale my images down
to make it fit inside my forms.
I have looked around and found solutions such as dragging PictureBox
es
inside of a panel, potentially good but it had its issues. With this article, I
want to share my work with 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. |
GifAnimation |
bool |
A property to enable or disable animations
of *.gif files. |
GifFPS |
double |
A property to adjust the frames per second
for the animations of *.gif files. Ranged between 1 and 30 FPS. |
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. |
NavigationPanelColor |
Color |
A property to adjust the color used by the
Navigation panel only. |
PanelWidth |
int |
A property that returns the width of the image
panel. |
PanelHeight |
int |
A property that returns the height of the image
panel. |
PreviewButton |
bool |
A property to enable or disable the Preview
toggle button on the UserControl Menu. |
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. |
Scrollbars |
bool |
A property to enable or disable scrollbars. |
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 is however not what you want, you can Set the Image programmatically 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 separate 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
an example, see 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()
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 in 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()
is 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) is 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 follows:
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).
- The preview image will not animate when opening an animated *.gif file (This
is intended!).
- Multi-page tiff images with JPEG compression are not supported. Microsoft
GDI+ does not offer any solutions to this problem.
Version History
Version 1.5.1: (September 12, 2011)
- Fixed an issue where the
FitToScreen
function would return
incorrect results
- Removed all
static
code from the project (that caused this
issue)
- Two additional properties added
PanelWidth
and PanelHeight
Version 1.5.0: (September 1, 2011)
- Gif animation reworked
ImageAnimator
has been removed and a custom locking system
has been implemented
- The frames per second can now be specified by the
GifFPS
property
- Fixed an issue where some *.gif images would no longer animate
when being rotated by 180 degrees
- Fixed a crash that occurred when dragging and dropping a multi-page tiff
image onto the control
- New Feature: Implemented full scrollbar support
Version 1.4.0: (August 15, 2011)
- Demo project re-upload. Drag and Drop is now available again in the demo.
Version 1.4.0: (August 2, 2011) Full change list:
- Implemented support for animated *.gif files
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 a lot 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 a lot of you
will find it as a useful control for your projects. While I believe that the control
works great, I also believe that there is plenty of room for improvement. 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 as well! 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 its flicker free drawing on the
panel.