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:
- 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.
- 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.
- 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.
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
.
public int ScaledPictureWidth { get; set; }
public int ScaledPictureHeight { get; set; }
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.
public int PreviousScalePercent { get; set; }
private int currentScalePercent = Common.ORIGINALSCALEPERCENT;
public int CurrentScalePercent
{
get { return currentScalePercent; }
set
{
if (PictureBox.Image == null || PreviousScalePercent == value)
return;
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));
PreviousScalePercent = CurrentScalePercent;
currentScalePercent = Math.Max(Math.Min(value, maxScalePercent), minScalePercent);
scalablePictureBoxParent.ScalePercent = CurrentScalePercent;
ScaledPictureWidth = (int)(OriginalPicture.Width * (float)currentScalePercent / (float)Common.ORIGINALSCALEPERCENT);
ScaledPictureHeight = (int)(OriginalPicture.Height * (float)currentScalePercent / (float)Common.ORIGINALSCALEPERCENT);
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.
private void ZoomInPicture()
{
PreviousScalePercent = CurrentScalePercent;
CurrentScalePercent = Math.Max(PreviousScalePercent + 10, (int)(Common.ZOOMINRATIO * CurrentScalePercent));
}
private void ZoomOutPicture()
{
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
.
public ScalablePictureBox scalablePictureBoxParent { get; set; }
...
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.
private bool mouseWheelEventHandled = true;
public void pictureBox_MouseWheel(object sender, System.Windows.Forms.MouseEventArgs e)
{
if (!mouseWheelEventHandled)
return;
ZoomRate = e.Delta / 120;
mouseWheelEventHandled = false;
}
public void pictureBox_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
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; }
private void InitialiseZoomList()
{
if (ZoomList == null)
ZoomList = new List<ZoomItem>();
else
ZoomList.Clear();
ZoomList.Add(new ZoomItem(Common.FITWIDTHMENUITEMNAME));
ZoomList.Add(new ZoomItem(Common.FITHEIGHTMENUITEMNAME));
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.
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.
private bool ReDrawPicture()
{
Bitmap pictureWithFrame;
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();
}
if (!FillBackground(out pictureWithFrame, leftX, upperY, rightX, lowerY))
return false;
System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(pictureWithFrame);
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBilinear;
g.DrawImage(OriginalPicture, leftX, upperY, rightX, lowerY);
PictureBox.Width = ScaledPictureWidth;
PictureBox.Height = ScaledPictureHeight;
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.