Introduction
Many times, I have read posts on forums asking for help with adding a panning feature to the .NET PictureBox
control. After decompiling the class from the framework, I realized it was a rather simple control and that the best thing to do was to inherit from Control
and write one from scratch. After posting dozens of replies to users about this topic, I decided to write a very simple example of how it can be done, without getting into anything complicated so that it can be built upon a basic understanding. Please don't make a point of how this article is *missing* any important features as this article isn't showing off a finished feature-rich control; its only purpose is to help with the concept of panning an image using GDI+.
Background
The .NET Framework PictureBox
control uses a simple call to GDI+'s DrawImage
method. This article suggests using the same method, but using a polymorphism accepting a rectangle representing part of the image to to be drawn instead of it in its entirety. Moving this rectangle along the image will result in that part of the image being drawn to the destination rectangle and thus providing a simple panning effect.
How it works
I feel it is important not to cover every line of code as most of it is very basic, but I will try to outline some important sections that make up the concept, and clear up some basic questions I seem to be always answering in forums.
Inheriting from Control
The first basic step in writing a custom control is to decide where to start. If there is a control in the framework that has the functionality you want and you just want to add something to it, it might be a good idea to inherit from that control. But in many cases, you want to do something new, or make it on your own. And, in that case, you will want to inherit from the Windows.Forms.Control
base class.
class DDPanBox : System.Windows.Forms.Control
{
public DDPanBox()
{
...
}
...
}
Drawing the image
Assuming creating a variable to hold an image is basic enough to not be covered here, the idea of drawing the image is simple. Just use the Control
's Graphics
object in the Paint
event to call the DrawImage
() method, and pass to it where we want the image to be drawn, what image to draw, and what area of the image needs to be drawn.
protected override void OnPaint(PaintEventArgs e)
{
if (_Image != null)
{
e.Graphics.InterpolationMode =
System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
e.Graphics.SmoothingMode =
System.Drawing.Drawing2D.SmoothingMode.HighQuality;
e.Graphics.DrawImage(_Image, ClientRectangle,
DrawRect, GraphicsUnit.Pixel);
if(zoom)
e.Graphics.DrawString("Zoom 2:1", this.Font,
Brushes.White, new PointF(15F, 15F));
else
e.Graphics.DrawString("Zoom 1:1", this.Font,
Brushes.White, new PointF(15F, 15F));
}
base.OnPaint(e);
}
I set the Interpolationmode
and SmoothingMode
here just to make the scaled images look better; it is not needed, and removing those lines will speed up the application as well.
e.Graphics.DrawImage(_Image, ClientRectangle, DrawRect, GraphicsUnit.Pixel);
The DrawImage
method here takes _Image
, the image to be drawn, ClientRectangle
, the rectangle to draw the image to (in this case, the entire surface of our control), DrawRect
, the rectangle representing the part of the image that needs to be drawn, and then GraphicsUnit.Pixel
, the scale format of the image to be drawn.
Moving the DrawRect (source rectangle)
The basic idea here is to capture the mouse-down event and get the location of the click and save it to a variable and set a flag showing that the dragging is active. Then, in the MouseMove
event, check for that flag. If true
, get the difference of the current mouse position and the saved one, then apply that difference to drawRect
. Afterwards, check if drawRect
is outside of the image, and if it is, move it to the edge.
bool dragging = false;
Point start = new Point();
void panBox_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
dragging = true;
start = new Point(e.Location.X + DrawRect.Location.X,
e.Location.Y + DrawRect.Location.Y);
...
}
else if (e.Button == MouseButtons.Right)
{
...
}
}
void panBox_MouseMove(object sender, MouseEventArgs e)
{
if (dragging)
{
DrawRect.Location = new Point(start.X - e.Location.X,
start.Y - e.Location.Y);
if (DrawRect.Location.X < 0 -Padding.Left)
DrawRect.Location = new Point(0 - Padding.Left,
DrawRect.Location.Y);
if (DrawRect.Location.Y < 0 - Padding.Top)
DrawRect.Location = new Point(DrawRect.Location.X, 0 - Padding.Top);
if (DrawRect.Location.X > _Image.Width - DrawRect.Width + Padding.Right)
DrawRect.Location = new Point(_Image.Width - DrawRect.Width +
Padding.Right, DrawRect.Location.Y);
if (DrawRect.Location.Y > _Image.Height - DrawRect.Height +
Padding.Bottom)
DrawRect.Location = new Point(DrawRect.Location.X,
_Image.Height - DrawRect.Height +
Padding.Bottom);
this.Refresh();
}
}
void panBox_MouseUp(object sender, MouseEventArgs e)
{
dragging = false;
...
}
Keeping things looking good!
When the form is resized, you will notice that the control doesn't take it very well. So, we need to handle the resized event and make sure that the drawRect
doesn't dance off the image.
void panBox_Resize(object sender, EventArgs e)
{
if (_Image != null)
{
if (zoom)
{
DrawRect = new Rectangle(DrawRect.Location.X,
DrawRect.Location.Y, ClientRectangle.Width / 2,
ClientRectangle.Height / 2);
}
else
DrawRect = new Rectangle(DrawRect.Location.X,
DrawRect.Location.Y, ClientRectangle.Width, ClientRectangle.Height);
if (DrawRect.Location.X < 0 - Padding.Left)
DrawRect.Location = new Point(0 - Padding.Left, DrawRect.Location.Y);
if (DrawRect.Location.Y < 0 - Padding.Top)
DrawRect.Location = new Point(DrawRect.Location.X, 0 - Padding.Top);
if (DrawRect.Location.X > _Image.Width - DrawRect.Width + Padding.Right)
DrawRect.Location = new Point(_Image.Width - DrawRect.Width +
Padding.Right, DrawRect.Location.Y);
if (DrawRect.Location.Y > _Image.Height - DrawRect.Height + Padding.Bottom)
DrawRect.Location = new Point(DrawRect.Location.X,
_Image.Height - DrawRect.Height + Padding.Bottom);
this.Refresh();
}
}
Something important to note here. If the image is smaller than the client rectangle, then the image will be justified to the bottom right. This is because the image is being drawn from the full source image rectangle, but drawn to the larger client rectangle. The best solution here in my opinion is to center the image. But that is outside the scope of this article. The source code doesn't reflect this, but a simple solution to keep the image in the upper left corner as usually expected would be to add the following block above the this.Refresh()
line in the resize event handler.
if (_Image.Width < ClientRectangle.Width)
{
DrawRect.Location = new Point(0, DrawRect.Location.Y);
}
if (_Image.Height < ClientRectangle.Height)
{
DrawRect.Location = new Point(DrawRect.Location.X, 0);
}
Although, it may come with its own problems as you build your custom solution. That is why it has been left out of the source. I feel the behavior here should be as per the application and the developer's prefernce, so it has been left to you.
Using the code
The source is given as a simple to use drop-in control that can be used with Visual Studio, or just instantiated as you would any other control.
I do not feel this is a complete control, nor do I share it as a complete control. It works, serves a purpose, and is ready to be used with no modifications. But it is only intended as a stepping stone towards a fuller, more robust solution. I do hope it helps those struggling with the basics behind a panning picture box.
Points of interest
Something important to note is that, in the mouse down event, when you capture the mouse position to pan the source rectangle across the image, you must offset the mouse location by the current source rectangle. This ensures that when the origin of the image isn't the same as the origin of the control's client rectangle, the image doesn't jump around like crazy when you pan. An excerpt from the code is shown below, and this trick is illustrated in the setting of the start variable after the comment.
void panBox_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
dragging = true;
start = new Point(e.Location.X + DrawRect.Location.X,
e.Location.Y + DrawRect.Location.Y);
Cursor = Cursors.SizeAll;
}
...
}
History