Click here to Skip to main content
4.80 / 5, 46 votes

1

2
3 votes, 6.5%
3
3 votes, 6.5%
4
40 votes, 87.0%
5

Image Viewer UserControl

The article is about a UserControl I wrote. Opposed to PictureBoxes and other methods to displaying images on your forms this one provides a totally diffrent approach.

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 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(); // 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 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)
   {
      // 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 seleciton
         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() 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.

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() 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) 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
   {
      // 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).

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Jordy "Kaiwa" Ruiter


Software Developer
Kaiwa-Projects
Netherlands Netherlands

Member
Currently a student in Application Development. Is currently working towards the Game-Development business to program 3D applications.

Sign Up to vote for this article
Add a reason or comment to your vote:

Comments and Discussions

You must Sign In to use this message board.
FAQ FAQ 
 
Noise Tolerance  Layout  Per page   
 Msgs 1 to 25 of 68 (Total in Forum: 68) (Refresh)FirstPrevNext
GeneralVB.Net Pinmemberdragonrose14hrs 21mins ago 
GeneralRe: VB.Net PinmemberJordy "Kaiwa" Ruiter13hrs 22mins ago 
GeneralPath Question PinmemberJayveeJavier21:11 17 Aug '10  
GeneralRe: Path Question PinmemberJordy "Kaiwa" Ruiter0:27 18 Aug '10  
QuestionMulti page image rotation PinmemberSRaghu15:55 17 Jun '10  
AnswerRe: Multi page image rotation PinmemberJordy "Kaiwa" Ruiter22:33 17 Jun '10  
GeneralRe: Multi page image rotation PinmemberSRaghu8:17 18 Jun '10  
GeneralRe: Multi page image rotation PinmemberSRaghu8:26 18 Jun '10  
GeneralRe: Multi page image rotation PinmemberJordy "Kaiwa" Ruiter9:43 18 Jun '10  
GeneralRe: Multi page image rotation PinmemberSRaghu14:56 18 Jun '10  
GeneralRe: Multi page image rotation PinmemberJordy "Kaiwa" Ruiter22:36 18 Jun '10  
GeneralRe: Multi page image rotation PinmemberJordy "Kaiwa" Ruiter0:03 19 Jun '10  
GeneralRe: Multi page image rotation PinmemberSRaghu7:45 19 Jun '10  
GeneralRe: Multi page image rotation PinmemberJordy "Kaiwa" Ruiter8:05 19 Jun '10  
GeneralRe: Multi page image rotation PinmemberSRaghu7:48 21 Jun '10  
GeneralRe: Multi page image rotation PinmemberJordy "Kaiwa" Ruiter7:59 21 Jun '10  
GeneralRe: Multi page image rotation PinmemberJordy "Kaiwa" Ruiter8:16 21 Jun '10  
GeneralRe: Multi page image rotation PinmemberSRaghu12:06 21 Jun '10  
GeneralRe: Multi page image rotation PinmemberJordy "Kaiwa" Ruiter13:01 21 Jun '10  
GeneralRe: Multi page image rotation PinmemberSRaghu14:08 21 Jun '10  
GeneralCenter image Pinmemberamre3l4:54 20 May '10  
GeneralRe: Center image PinmemberJordy "Kaiwa" Ruiter5:33 20 May '10  
Generalcool control PinmemberJulian Ott3:24 7 May '10  
GeneralLooks nice PinmemberKenJohnson16:51 5 May '10  
GeneralRe: Looks nice PinmemberJordy "Kaiwa" Ruiter20:23 5 May '10  

General General    News News    Question Question    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+PgUp/PgDown to switch pages.

PermaLink | Privacy | Terms of Use
Last Updated: 21 Jun 2010

Copyright 2010 by Jordy "Kaiwa" Ruiter
Everything else Copyright © CodeProject, 1999-2010
Web21 | Advertise on the Code Project