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

A scrollable, zoomable, and scalable picture box

0.00/5 (No votes)
10 Dec 2012 1  
An enhanced version of scrollable, zoomable, and scalable picture box

Introduction   

This is based on Bingzhe Quan's project "A scrollable, zoomable, and scalable picture box" at: http://www.codeproject.com/Articles/15373/A-scrollable-zoomable-and-scalable-picture-box. We were working on some designing software which involves scaling image as background and find the original project very useful. However there are a few functions that our project requires but unfortunately are missing in the original project:

  1. The image can be scaled to any size and the missing “bigger zoom-in rate” function has been acknowledged by Quan in the Improvement section. The main drive behind this is in our project small image should be scaled to larger size so the image can be viewed in detail, or a rather large image should be scaled to fit the window so that the whole image can be viewed at once. This requires the original image to be scaled and possibly extra pixels to be interpolated.
  2. The original image can be customised and this also requires the image to be redrawn. In our code, we have shown how to add a simple gray frame around the original image. Further image manipulation could be implemented in similar ways given it is not too complicated.
  3. Besides scaling image using pop-up menu, it would be good to be able to scale using mouse wheel like in Google Map. This is a minor UI enhancement but nevertheless can prove to be useful because it also provides wider range of scaling choices.

Background

The original author designs the project using the Facade and Mediator design patterns, and this enhanced version sees no need to change that design. Therefore we quote Quan’s original text and diagram to explain the project structure as follows.

ScalablePictureBox is a facade of the ScalablePictureBox control. It also mediates ScalablePictureBoxImp and PictureTracker. ScalablePictureBoxImp is the core implementation of the scrollable, zoomable, and scalable picture box. PictureTracker is a tracker, a scroller, and a thumbnail viewer of the current picture. TransparentButton is a tiny user control used as a close button in PictureTracker. Util  provides some helper functions for the controls.” 

This article is by no means written as a self-contained one. Therefore you should refer to Quan's article for more information about how the original project is designed and works.

Using the code

The major code change happens in the ScalablePictureBoxImp class. We have introduced a property named OriginalPicture to store the original picture so that the new scaled picture can be produced from the original picture rather than based on the previous scaled picture. This proves to improve the display quality of scaled picture by avoiding possible pixel loss caused by zooming out action further affecting scaled pictures afterwards.

// A copy of the original picture 
private Bitmap originalPicture;
public Bitmap OriginalPicture
{
   get { return originalPicture; 
   set
   {
     originalPicture = value;
 
     if (value == null)
       return;
 
     LeftX = 0;
     UpperY = 0;
     RightX = value.Width;
     LowerY = value.Height;
}

In order to keep reference of the scaled image dimensions, two more properties ScaledPictureWidth and ScaledPictureHeight are used. Also a few more properties, LeftX, UpperY, RightX and LowerY have been added to keep reference of the logic locations of the scaled image within the PictureBox because the image no longer always occupies the whole PictureBox.

// Keep record of the dimensions of original & scaled images for fast reference and calculation
public int ScaledPictureWidth { get; set; }
public int ScaledPictureHeight { get; set; }  
// Keep record of the logic positions of the image corner points for fast reference and calculation
private int leftX;
public int LeftX { get { return leftX; } set { leftX = value; } }
private int upperY;
public int UpperY { get { return upperY; } set { upperY = value; } }
private int rightX;
public int RightX { get { return rightX; } set { rightX = value; } }
private int lowerY;
public int LowerY { get { return lowerY; } set { lowerY = value; } }

Here we define the term of scale percentage as the ratio of the scaled image to the original  image, i.e., the scale percentage of original image is 100% and an enlarged image has its scale percentage larger than 100% while a shrunk one has its scale percentage smaller than 100%.

In order to use both mouse wheel and pop-up menu to scale the image and to avoid unnecessary redrawing when there is no scaling change, we keep record of the previous scale percentage (as in PreviousScalePercent) so that it can be compared with the newly assigned scale percentage (as in CurrentScalePercent). As the original scale percentage is 100%, the first zooming in action by mouse wheel scrolling increases the original image size by 1%, i.e. the new scale percentage will be 100%*1.01 = 101%. Similarly the first zooming out action by mouse wheel scrolling decreases the size of original image by 1%, i.e. the new scale record will be 100%/1.01 = 99.01%. Any further mouse wheel scrolling increases or decreases the previous scale percentage by 1% respectively.

While this preset scaling by mouse wheel scrolling works reasonably well in general, it is not that brilliant for scale percentages near 100%. The reason is simple: if the current scale percentage is 400%, then a further zooming in action will increase that to 400% * 1.01 = 404% and the difference between two scale percentages will be 4%. However if the current scale percentage is 100%, a further zooming in action will only make a difference of 1%. Not very noticeable if the original image size is small.

// Previous scale percentage for the picture box
public int PreviousScalePercent { get; set; }
 
// Scale percentage of picture box in zoom mode
private int currentScalePercent = Common.ORIGINALSCALEPERCENT;
// Scale percentage for the picture box
public int CurrentScalePercent
{
  get { return currentScalePercent; }
  set
  {
    // No image or the scale remains the same, no need to redraw
    if (PictureBox.Image == null || PreviousScalePercent == value)
      return;
 
    // Calculate the minimum and maximum scale percentages allowed by predefined values for the
    // width and height respectively to make sure neither of them exceeds the preset upper and lower scale limits
    int minScalePercent = (int)(100 * Math.Max((float)Common.PICTUREWIDTHMIN / (float)OriginalPicture.Width,
      (float)Common.PICTUREHEIGHTMIN / (float)OriginalPicture.Height));
    int maxScalePercent = (int)(100 * Math.Min((float)Common.PICTUREWIDTHMAX / (float)OriginalPicture.Width,
      (float)Common.PICTUREHEIGHTMAX / (float)OriginalPicture.Height));
 
    // Set the previous scale percent and the current one
    PreviousScalePercent = CurrentScalePercent;
    currentScalePercent = Math.Max(Math.Min(value, maxScalePercent), minScalePercent);
 
    // Set the parent control scale percent to pass the value to the PictureTracker
    scalablePictureBoxParent.ScalePercent = CurrentScalePercent;
 
    // Calculate the scaled picture dimensions
    ScaledPictureWidth = (int)(OriginalPicture.Width * (float)currentScalePercent / (float)Common.ORIGINALSCALEPERCENT);
    ScaledPictureHeight = (int)(OriginalPicture.Height * (float)currentScalePercent / (float)Common.ORIGINALSCALEPERCENT);
 
    // Redraw scaled picture 
    ReDrawPicture();
  }
}

As a simple fix to the problem, we introduce a threshold of 10 pixels between scale percentage changes. Therefore when zooming in/out the picture, the new picture is made noticeably different from the previous one by increase its dimensions by 1% or 10 pixels, whichever is larger.

// Zoom in the picture
private void ZoomInPicture()
{
 // Make sure the zoom in ratio is noticeable but also within reasonable range
 PreviousScalePercent = CurrentScalePercent;
 CurrentScalePercent = Math.Max(PreviousScalePercent + 10, (int)(Common.ZOOMINRATIO * CurrentScalePercent));
}
 
// Zoom out the picture
private void ZoomOutPicture()
{
 // Make sure the zoom out ratio is noticeable but also within reasonable range
 PreviousScalePercent = CurrentScalePercent;
 CurrentScalePercent = Math.Min(PreviousScalePercent - 10, (int)(Common.ZOOMOUTRATIO * CurrentScalePercent));
}

Also we have added a reference to the parent ScalablePictureBox control so that the current scale percentage CurrentScalePercent in ScalablePictureBoxImp can be passed back to ScalablePictureBox and in turn synchronises the variable ScalePercent in PictureTracker.

// The referenece to the parent ScalablePictureBox
public ScalablePictureBox scalablePictureBoxParent { get; set; } 
...
// Set the parent control scale percent to pass the value to the PictureTracker
scalablePictureBoxParent.ScalePercent = CurrentScalePercent;

A boolean flag mouseWheelEventHandled has been introduced to make sure the function body of pictureBox_MouseWheel()is executed only once between mouse wheel being pressed down and mouse wheel goes up again.

// The flag to stop the pictureBox_MouseWheel event handler being triggered more than once during one scrolling
private bool mouseWheelEventHandled = true;
 
public void pictureBox_MouseWheel(object sender, System.Windows.Forms.MouseEventArgs e)
{
  // Check the flag. Early out if the flag is true
  if (!mouseWheelEventHandled)
    return;
 
  ZoomRate = e.Delta / 120;
  // Set the flag to false to mark the mouse wheel event has not been handled
  mouseWheelEventHandled = false;
}
 
public void pictureBox_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
  // Mouse button up means the wheel scrolling has finished - Set the flag to false
  mouseWheelEventHandled = true;
}

The last variable we introduced is ZoomList, which is used to populate the pop-up menu as an alternative way to scale the picture of mouse wheel scrolling.

public class ZoomItem
{
  public int ZoomRate { get; set; }
  public string ZoomName { get; set; } 

  ...
}
public List<ZoomItem> ZoomList { get; set; }
/// <summary>
/// Initialize ZoomList
/// </summary>
private void InitialiseZoomList()
{
  if (ZoomList == null)
    ZoomList = new List<ZoomItem>();
  else
    ZoomList.Clear();
 
  /// Fit width zoom rate is unknow at this stage since the image is NOT loaded yet.
  ZoomList.Add(new ZoomItem(Common.FITWIDTHMENUITEMNAME));
  /// Fit height zoom rate is unknow at this stage since the image is NOT loaded yet.
  ZoomList.Add(new ZoomItem(Common.FITHEIGHTMENUITEMNAME));
  // Add a separator
  ZoomList.Add(new ZoomItem("-"));
 
  ZoomList.Add(new ZoomItem(Common.MINSCALEPERCENT));
 
  for (int scale = 25; scale <= 150; scale += 25)
  {
    ZoomList.Add(new ZoomItem(scale));
  }
 
  for (int scale = 200; scale <= Common.MAXSCALEPERCENT; scale += 200)
  {
    ZoomList.Add(new ZoomItem(scale));
  }
}

Points of Interest

Tweaking the original image  

One of the major improvement of this version is the original picture can be modified rather than just being zoomed out and this is done in the function ReDrawPicture().

First of all, we create a new Bitmap named pictureWithFrame. The idea is to use it as a canvas and draw the modified picture on it in a number of steps. Currently what we do is to position the scaled image in the centre of pictureWithFrame and add four stripes around it as the picture frame.

In order to add the background strips, we use some unsafe code in FillBitMapRegion() so that the pixels in the areas are filled with specific colour. Bitmap.LockBits() and Bitmap.UnlockBits() are employed to lock and unlock the Bitmap to system memory so that the Bitmap pixels can be quickly set by according to their address in memory. Meanwhile a new struct Overlay has been defined and employed to fill the 32-bit colour to 4 bytes. Fill a number of rectangular areas as the picture frame is nothing worth raising your eyebrows but what is important is the idea of using the Bitmap variable as a canvas.

/// <summary>
/// Generic function to fill BitMap region on screen or in PictureBox control
/// </summary>
/// <param name="bmap"></param>
/// <param name="colour"></param>
/// <param name="leftX"></param>
/// <param name="upperY"></param>
/// <param name="rightX"></param>
/// <param name="lowerY"></param>
/// <returns></returns>
unsafe private bool FillBitMapRegion(ref Bitmap bmap, 
       Color colour, int leftX, int upperY, int rightX, int lowerY)
{
  leftX--; upperY--; rightX--; lowerY--;
  BitmapData bmd = bmap.LockBits(new Rectangle(leftX, 
      upperY, rightX - leftX + 1, lowerY - upperY + 1),
      ImageLockMode.ReadOnly, bmap.PixelFormat);
  int PixelSize = 4;
  Overlay overlay = new Overlay();
  for (int y = 0; y < bmd.Height; y++)
  {
    byte* row = (byte*)bmd.Scan0 + (y * bmd.Stride);
    for (int x = 0; x < bmd.Width; x++)
    {
      overlay.u32 = (uint)colour.ToArgb();
      row[x * PixelSize] = overlay.u8_0;
      row[x * PixelSize + 1] = overlay.u8_1;
      row[x * PixelSize + 2] = overlay.u8_2;
      row[x * PixelSize + 3] = overlay.u8_3;
    }
  }
  bmap.UnlockBits(bmd);

  return true;
}

The second step is to fill the centre of pictureWithFrame with the scaled image, we use System.Drawing.Graphics.DrawImage() to draw OriginalPicture to the specified area within pictureWithFrame rather than to draw the previous scaled image. As we have mentioned before, this is to improve the image quality. Meanwhile Graphics.InterpolationMode is set to InterpolationMode.HighQualityBilinear to interpolate pixels between the pixels in OriginalPicture if it is enlarged.

The last step is to set this tweaked image to pictureBox.Image and refresh the pop-up menu.

/// <summary>
/// Reraw the changed image in memory and assign its value to Picture
/// </summary>
/// <returns></returns>
private bool ReDrawPicture()
{
  Bitmap pictureWithFrame;
 
  // Get the four corner point coordinates of the scaled image
  switch (GetZoomedPicturePosition(ref leftX, ref upperY, ref rightX, ref lowerY))
  {
    case Common.SUCCESSBUTNOTDONE:
      return true;
    case Common.GENERALERROR:
      return false;
    case Common.GENERALSUCCESS:
      break;
    default:
      throw new NotImplementedException();
  }
 
  // The background frames need to be filled every time the foreground is zoomed
  if (!FillBackground(out pictureWithFrame, leftX, upperY, rightX, lowerY))
    return false;
 
  // Draw the zoomed image
  System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(pictureWithFrame);
  g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBilinear;
  g.DrawImage(OriginalPicture, leftX, upperY, rightX, lowerY);
 
  // Set the picture dimensions
  PictureBox.Width = ScaledPictureWidth;
  PictureBox.Height = ScaledPictureHeight;
 
  // Set the picture
  Picture = pictureWithFrame;
 
  return true;
}

We have included a simple demo project to show how to use this control. Let us know if you experience any problem with the control itself or the demo.

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