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

Image Viewer UserControl

0.00/5 (No votes)
12 Sep 2011 1  
The article is about a UserControl I wrote. Opposed to PictureBoxes and other methods of displaying images on your forms, this one provides a totally different approach.
ImageViewerUserControl/KpImageViewerV2.png

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 PictureBoxes 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(); // Rotates the Image 90 degrees clockwise
    kpImageViewer.Rotate180(); // Rotates the Image 180 degrees clockwise
    kpImageViewer.Rotate270(); // Rotates the Image 270 degrees clockwise
}

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)
   {
      // Left Shift or Right Shift pressed? Or is select mode one?
      if (this.IsKeyPressed(0xA0) || this.IsKeyPressed(0xA1) || selectMode == true)
      {
         // Fancy cursor
         pbFull.Cursor = Cursors.Cross;

         shiftSelecting = true;

         // Initial selection
         ptSelectionStart.X = e.X;
         ptSelectionStart.Y = e.Y;

         // No selection end
         ptSelectionEnd.X = -1;
         ptSelectionEnd.Y = -1;
      }
      else
      {
         // Start dragging
         drawing.BeginDrag(new Point(e.X, e.Y));

         // Fancy cursor
         if (grabCursor != null)
         {
            pbFull.Cursor = grabCursor;
         }
      }
   }
}

private void pbFull_MouseUp(object sender, MouseEventArgs e)
{
   // Am i dragging or selecting?
   if (shiftSelecting == true)
   {
      // Calculate my selection rectangle
      Rectangle rect = CalculateReversibleRectangle(ptSelectionStart, ptSelectionEnd);

      // Clear the selection rectangle
      ptSelectionEnd.X = -1;
      ptSelectionEnd.Y = -1;
      ptSelectionStart.X = -1;
      ptSelectionStart.Y = -1;

      // Stop selecting
      shiftSelecting = false;

      // Position of the panel to the screen
      Point ptPbFull = PointToScreen(pbFull.Location);

      // Zoom to my selection
      drawing.ZoomToSelection(rect, ptPbFull);

      // Refresh my screen & update my preview panel
      pbFull.Refresh();
      UpdatePanels(true);
   }
   else
   {
      // Stop dragging and update my panels
      drawing.EndDrag();
      UpdatePanels(true);

      // Fancy cursor
      if (dragCursor != null)
      {
         pbFull.Cursor = dragCursor;
      }
   }
}

private void pbFull_MouseMove(object sender, MouseEventArgs e)
{
   // Am I dragging or selecting?
   if (shiftSelecting == true)
   {
      // Keep selecting
      ptSelectionEnd.X = e.X;
      ptSelectionEnd.Y = e.Y;

      Rectangle pbFullRect = new Rectangle(0, 0, pbFull.Width - 1, pbFull.Height - 1);

      // Am I still selecting within my panel?
      if (pbFullRect.Contains(new Point(e.X, e.Y)))
      {
            // If so, draw my Rubber Band Rectangle!
         Rectangle rect = CalculateReversibleRectangle(ptSelectionStart, ptSelectionEnd);
         DrawReversibleRectangle(rect);
      }
   }
   else
   {
      // Keep dragging
      drawing.Drag(new Point(e.X, e.Y));
      if (drawing.IsDragging)
      {
         UpdatePanels(false);
      }
      else
      {
         // I'm not dragging OR selecting
         // Make sure if left or right shift is pressed to change cursor

         if (this.IsKeyPressed(0xA0) || this.IsKeyPressed(0xA1) || selectMode == true)
         {
            // Fancy Cursor
            if (pbFull.Cursor != Cursors.Cross)
            {
               pbFull.Cursor = Cursors.Cross;
            }
         }
         else
         {
            // Fancy Cursor
            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.

AvoidOutOfScreen() technique

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;

   // So, where did my selection start on the entire picture?
   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;

   // The selection width on the scale of the Original size!
   if (zoom < 1.0 || zoom > 1.0)
   {
      selectedWidth = Convert.ToInt32((double)width / zoom);
      selectedHeight = Convert.ToInt32((double)height / zoom);
   }

   // What is the highest possible zoomrate?
   double zoomX = ((double)panelWidth / (double)selectedWidth);
   double zoomY = ((double)panelHeight / (double)selectedHeight);

   double newZoom = Math.Min(zoomX, zoomY);

   // Avoid Int32 crashes!
   if (newZoom * 100 < Int32.MaxValue && newZoom * 100 > Int32.MinValue)
   {
      SetZoom(newZoom);

      selectedWidth = (int)((double)selectedWidth * newZoom);
      selectedHeight = (int)((double)selectedHeight * newZoom);

      // Center the selected area
      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
   {
      // Get The file(s) you dragged into an array.
      // (We'll just pick the first image anyway)
      string[] FileList = (string[])e.Data.GetData(DataFormats.FileDrop, false);

      Image newBmp = null;

      for (int f = 0; f < FileList.Length; f++)
      {
         // Make sure the file exists!
         if (System.IO.File.Exists(FileList[f]))
         {
            string ext = (System.IO.Path.GetExtension(FileList[f])).ToLower();

            // Checking the extensions to be Image formats
            if (ext == ".jpg" || ext == ".jpeg" || ext == ".gif" ||
		ext == ".wmf" || ext == ".emf" || ext == ".bmp" ||
		ext == ".png" || ext == ".tif" || ext == ".tiff")
            {
               try
               {
                  // Try to load it into a bitmap
                  newBmp = Bitmap.FromFile(FileList[f]);
                  this.Image = (Bitmap)newBmp;

                  // If succeeded stop the loop
                  break;
               }
               catch
               {
                  // Not an image?
               }
            }
         }
      }
   }
   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))
      {
         // Drop the file
         e.Effect = DragDropEffects.Copy;
      }
      else
      {
         // I'm not going to accept this unknown format!
         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.

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