Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / multimedia / GDI+

A Simple Panning PictureBox

4.75/5 (7 votes)
19 Feb 2010CPOL5 min read 70.8K   3.8K  
A basic example of how to build a PictureBox control with simple pan and zoom features.

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.

C#
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.

C#
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.

C#
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.

C#
bool dragging = false; //Tells us if our image has been clicked on.

Point start = new Point(); //Keep initial click for accurate panning.

void panBox_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        dragging = true;
        //offset new point by original one so
        //we know where in the image we are.
        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.

C#
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.

C#
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.

C#
void panBox_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        dragging = true;
        //offset new point by original one so
        //we know where in the image we are.
        start = new Point(e.Location.X + DrawRect.Location.X, 
                          e.Location.Y + DrawRect.Location.Y);
        Cursor = Cursors.SizeAll; //just for looks.
    }
    ...
}

History

  • 1.0: First release.

License

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