Zoom and pan a picture.

I seem to have an almost personal vendetta against PictureBox but this is not the case. The fact is that the component brings so little to the table and is so misunderstood anyway that it's not worth messing around with in many cases.

A big question in the Windows Forms world is "How can I zoom and scroll a PictureBox" and the answer is "Why bother?" To get the PictureBox to do the zooming and scrolling of an image you'd have to override the paint routine and do all the drawing yourself, add a zoom property, add some code to figure out how the zoom affected the drawing and so what exactly does PictureBox provide in that case? All the functionality of this misbegotten component is overridden so the simplest and cleanest thing to do is throw it out and start from scratch.

The logical choice for any scrolling surface is the ScrollableControl so using this as a basis I created the ZoomPicBox which enables you to make a simple zooming and panning image. If you read the article on AutoScroll you'll remember how the AutoScrollMinSize affects the size of the scrollbars and the values that are returned by the X and Y sliders. We can take advantage of this behaviour by telling the ScrollableControl the apparent size of the image after it's been zoomed. This size might be larger or smaller than the client area of the control but no-matter, ScrollableControl will send us the right signals.

When an image is zoomed, all you need to do is set up a Matrix object with a suitable transform to get the magnification right. This is as simple as creating a Matrix in the following manner.

Matrix mx=new Matrix(zoom,0,0,zoom,0,0);
Dim mx As New Matrix( zoom , 0 , 0 , zoom ,0 ,0)

When an image is zoomed its apparent size changes from the 1:1 appearance of the image. You might see this as overstating the obvious but the ScrollableControl can't know this without being explicitly told so this factor has to be plumbed in to the AutoScrollMinSize values. Whenever the zoom changes the AutoScrollMinSize is updated to be the image size times the zoom factor. This enables the scrollbars to give the correct range to enable the user to access both sides of what could be a very large picture. The great thing about zooming using the Matrix is that the zoom level can be arbitrarily large or small, that is as long as it's not zero or negative. The AutoScrollMinSize can be adjusted quite simply using the following code.

this.AutoScrollMinSize=new Size(
    (int)(this._image.Width*_zoom),
    (int)(this._image.Height*_zoom)
    );
 

Me.AutoScrollMinSize = _

    New Size(CInt(Me._image.Width * _zoom), _

    CInt(Me._image.Height * _zoom))
 

Panning is now made possible by the values returned by the AutoScrollPosition property. Because we're lying about the real size of the image and telling AutoScrollMinSize about the apparent size of the image, when the scroll positions are reported they will also be multiplied by the zoom factor. So that the correct position within the image can be ascertained for the top-left corner of the picture and hence the pan setting, the AutoScrollPosition values must be divided by the zoom factor again. In the OnPaint override, just before the image is drawn the transform can be initialised in the following manner.

Matrix mx=new Matrix(_zoom,0,0,_zoom,0,0);
mx.Translate(this.AutoScrollPosition.X/_zoom, this.AutoScrollPosition.Y/_zoom);
 

Dim mx As New Matrix(_zoom, 0, 0, _zoom, 0, 0)
mx.Translate(Me.AutoScrollPosition.X / _zoom, Me.AutoScrollPosition.Y / _zoom)
 

The business of adding an Image property and Zoom property to the component is very simple. In addition the way images are drawn might be important. For example if you want to zoom in on pixels the interpolation mode may be important so in the example a property for setting the interpolation mode is included.

The following listings show a fully zoomable pannable PictureBox replacement that is short and sweet.

namespace bobpowell.net

{

  /// <summary>

  /// ZoomPicBox does what it says on the wrapper.

  /// </summary>

  /// <remarks>

  /// PictureBox doesn't lend itself well to overriding. Why not start with something basic and do the job properly?

  /// </remarks>

  public class ZoomPicBox : ScrollableControl

  {

 

    Image _image;

    [

    Category("Appearance"),

    Description("The image to be displayed")

    ]

    public Image Image

    {

      get{return _image;}

      set

      {

        _image=value;

        UpdateScaleFactor();

        Invalidate();

      }

    }

 

    float _zoom=1.0f;

    [

    Category("Appearance"),

    Description("The zoom factor. Less than 1 to reduce. More than 1 to magnify.")

    ]

    public float Zoom

    {

      get{return _zoom;}

      set

      {

        if(value<0 || value<0.00001)

          value=0.00001f;

        _zoom=value;

        UpdateScaleFactor();

        Invalidate();

      }

    }

 

    /// <summary>

    /// Calculates the effective size of the image

    ///after zooming and updates the AutoScrollSize accordingly

    /// </summary>

    private void UpdateScaleFactor()

    {

      if(_image==null)

        this.AutoScrollMinSize=this.Size;

      else

      {

        this.AutoScrollMinSize=new Size(

          (int)(this._image.Width*_zoom+0.5f),

          (int)(this._image.Height*_zoom+0.5f)

          );

      }

    }

 

    InterpolationMode _interpolationMode=InterpolationMode.High;

    [

    Category("Appearance"),

    Description("The interpolation mode used to smooth the drawing")

    ]

    public InterpolationMode InterpolationMode

    {

      get{return _interpolationMode;}

      set{_interpolationMode=value;}

    }

 

 

    protected override void OnPaintBackground(PaintEventArgs pevent)

    {

      // do nothing.

    }

 

    protected override void OnPaint(PaintEventArgs e)

    {

      //if no image, don't bother

      if(_image==null)

      {

        base.OnPaintBackground(e);

        return;

      }

      //Set up a zoom matrix

      Matrix mx=new Matrix(_zoom,0,0,_zoom,0,0);

      //now translate the matrix into position for the scrollbars

      mx.Translate(this.AutoScrollPosition.X / _zoom, this.AutoScrollPosition.Y / _zoom);

      //use the transform

      e.Graphics.Transform=mx;

      //and the desired interpolation mode

      e.Graphics.InterpolationMode=_interpolationMode;

      //Draw the image ignoring the images resolution settings.

      e.Graphics.DrawImage(_image,new Rectangle(0,0,this._image.Width,this._image.Height),0,0,_image.Width, _image.Height,GraphicsUnit.Pixel);

      base.OnPaint (e);

    }

 

    

    public ZoomPicBox()

    {

      //Double buffer the control

      this.SetStyle(ControlStyles.AllPaintingInWmPaint |

        ControlStyles.UserPaint |

        ControlStyles.ResizeRedraw |

        ControlStyles.UserPaint |

        ControlStyles.DoubleBuffer, true);

 

      this.AutoScroll=true;

    }

  }

}

 

 

Imports System

Imports System.Collections

Imports System.ComponentModel

Imports System.Drawing

Imports System.Drawing.Drawing2D

Imports System.Windows.Forms

 

 

Namespace bobpowell.net

   '/ <summary>

   '/ ZoomPicBox does what it says on the wrapper.

   '/ </summary>

   '/ <remarks>

   '/ PictureBox doesn't lend itself well to overriding. Why not start with something basic and do the job properly?

   '/ </remarks>

  

   Public Class ZoomPicBox

    Inherits ScrollableControl

    

    Private _image As Image

    

    <Category("Appearance"), Description("The image to be displayed")>  _

    Public Property Image() As Image

     Get

      Return _image

     End Get

     Set

      _image = value

      UpdateScaleFactor()

      Invalidate()

     End Set

    End Property

    

    Private _zoom As Single = 1F

    

    <Category("Appearance"), Description("The zoom factor. Less than 1 to reduce. More than 1 to magnify.")>  _

    Public Property Zoom() As Single

     Get

      Return _zoom

     End Get

     Set

      If value < 0 OrElse value < 1E-05 Then

         value = 1E-05F

      End If

      _zoom = value

      UpdateScaleFactor()

      Invalidate()

     End Set

    End Property

    

    

    Private Sub UpdateScaleFactor()

     If _image Is Nothing Then

      Me.AutoScrollMargin = Me.Size

     Else

      Me.AutoScrollMinSize = New Size(CInt(Me._image.Width * _zoom + 0.5F), CInt(Me._image.Height * _zoom + 0.5F))

     End If

    End Sub 'UpdateScaleFactor

    

    Private _interpolationMode As InterpolationMode = InterpolationMode.High

    

    <Category("Appearance"), Description("The interpolation mode used to smooth the drawing")>  _

    Public Property InterpolationMode() As InterpolationMode

     Get

      Return _interpolationMode

     End Get

     Set

      _interpolationMode = value

     End Set

    End Property

     

    

    Protected Overrides Sub OnPaintBackground(pevent As PaintEventArgs)

    End Sub 'OnPaintBackground

    

    ' do nothing.

    

    Protected Overrides Sub OnPaint(e As PaintEventArgs)

     'if no image, don't bother

     If _image Is Nothing Then

      MyBase.OnPaintBackground(e)

      Return

     End If

     'Set up a zoom matrix

     Dim mx As New Matrix(_zoom, 0, 0, _zoom, 0, 0)

     mx.Translate(Me.AutoScrollPosition.X / _zoom, Me.AutoScrollPosition.Y / _zoom)

     e.Graphics.Transform = mx

     e.Graphics.InterpolationMode = _interpolationMode

     e.Graphics.DrawImage(_image, New Rectangle(0, 0, Me._image.Width, Me._image.Height), 0, 0, _image.Width, _image.Height, GraphicsUnit.Pixel)

     MyBase.OnPaint(e)

    End Sub 'OnPaint

    

 

    Public Sub New()

     'Double buffer the control

     Me.SetStyle(ControlStyles.AllPaintingInWmPaint Or ControlStyles.UserPaint Or ControlStyles.ResizeRedraw Or ControlStyles.UserPaint Or ControlStyles.DoubleBuffer, True)

     

     Me.AutoScroll = True

    End Sub 'New

   End Class 'ZoomPicBox

End Namespace 'bobpowell.net

To test this little control the following application provides a ZoomPicBox on a form with an accompanying TrackBar control that enables you to zoom in and out and pan anywhere at any zoom level.

using System;

using System.Drawing;

using System.Collections;

using System.ComponentModel;

using System.Windows.Forms;

using System.Data;

 

namespace TestZoomPicBox

{

  /// <summary>

  /// Summary description for Form1.

  /// </summary>

  public class Form1 : System.Windows.Forms.Form

  {

    private System.Windows.Forms.TrackBar trackBar1;

    private bobpowell.net.ZoomPicBox zoomPicBox1;

    /// <summary>

    /// Required designer variable.

    /// </summary>

    private System.ComponentModel.Container components = null;

 

    public Form1()

    {

      //

      // Required for Windows Form Designer support

      //

      InitializeComponent();

 

      //

      // TODO: Add any constructor code after InitializeComponent call

      //

    }

 

    /// <summary>

    /// Clean up any resources being used.

    /// </summary>

    protected override void Dispose( bool disposing )

    {

      if( disposing )

      {

        if (components != null)

        {

          components.Dispose();

        }

      }

      base.Dispose( disposing );

    }

 

    #region Windows Form Designer generated code

    /// <summary>

    /// Required method for Designer support - do not modify

    /// the contents of this method with the code editor.

    /// </summary>

    private void InitializeComponent()

    {

      this.trackBar1 = new System.Windows.Forms.TrackBar();

      this.zoomPicBox1 = new bobpowell.net.ZoomPicBox();

      ((System.ComponentModel.ISupportInitialize)(this.trackBar1)).BeginInit();

      this.SuspendLayout();

      //

      // trackBar1

      //

      this.trackBar1.Dock = System.Windows.Forms.DockStyle.Bottom;

      this.trackBar1.Location = new System.Drawing.Point(0, 221);

      this.trackBar1.Maximum = 500;

      this.trackBar1.Minimum = 1;

      this.trackBar1.Name = "trackBar1";

      this.trackBar1.Size = new System.Drawing.Size(292, 45);

      this.trackBar1.TabIndex = 0;

      this.trackBar1.Value = 1;

      this.trackBar1.ValueChanged += new System.EventHandler(this.trackBar1_ValueChanged);

      //

      // zoomPicBox1

      //

      this.zoomPicBox1.AutoScroll = true;

      this.zoomPicBox1.Dock = System.Windows.Forms.DockStyle.Fill;

      this.zoomPicBox1.Image = null;

      this.zoomPicBox1.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;

      this.zoomPicBox1.Location = new System.Drawing.Point(0, 0);

      this.zoomPicBox1.Name = "zoomPicBox1";

      this.zoomPicBox1.Size = new System.Drawing.Size(292, 221);

      this.zoomPicBox1.TabIndex = 1;

      this.zoomPicBox1.Text = "zoomPicBox1";

      this.zoomPicBox1.Zoom = 1F;

      //

      // Form1

      //

      this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);

      this.ClientSize = new System.Drawing.Size(292, 266);

      this.Controls.Add(this.zoomPicBox1);

      this.Controls.Add(this.trackBar1);

      this.Name = "Form1";

      this.Text = "Form1";

      this.Load += new System.EventHandler(this.Form1_Load);

      ((System.ComponentModel.ISupportInitialize)(this.trackBar1)).EndInit();

      this.ResumeLayout(false);

 

    }

    #endregion

 

    /// <summary>

    /// The main entry point for the application.

    /// </summary>

    [STAThread]

    static void Main()

    {

      Application.Run(new Form1());

    }

 

    private void Form1_Load(object sender, System.EventArgs e)

    {

      OpenFileDialog dlg=new OpenFileDialog();

      dlg.Filter="Image files|*.BMP;*.JPG;*.TIF;*.GIF";

      if(dlg.ShowDialog()==DialogResult.OK)

        this.zoomPicBox1.Image=Image.FromFile(dlg.FileName);

      else

        Application.Exit();

    }

 

    private void trackBar1_ValueChanged(object sender, System.EventArgs e)

    {

      this.zoomPicBox1.Zoom=0.01f*this.trackBar1.Value;

    }

  }

}

 

Public Class Form1

  Inherits System.Windows.Forms.Form

 

#Region " Windows Form Designer generated code "

 

  Public Sub New()

    MyBase.New()

 

    'This call is required by the Windows Form Designer.

    InitializeComponent()

 

    'Add any initialization after the InitializeComponent() call

 

  End Sub

 

  'Form overrides dispose to clean up the component list.

  Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)

    If disposing Then

      If Not (components Is Nothing) Then

        components.Dispose()

      End If

    End If

    MyBase.Dispose(disposing)

  End Sub

 

  'Required by the Windows Form Designer

  Private components As System.ComponentModel.IContainer

 

  'NOTE: The following procedure is required by the Windows Form Designer

  'It can be modified using the Windows Form Designer. 

  'Do not modify it using the code editor.

  Friend WithEvents TrackBar1 As System.Windows.Forms.TrackBar

  Friend WithEvents ZoomPicBox1 As ZoomPicBoxVB.bobpowell.net.ZoomPicBox

  <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()

    Me.TrackBar1 = New System.Windows.Forms.TrackBar

    Me.ZoomPicBox1 = New ZoomPicBoxVB.bobpowell.net.ZoomPicBox

    CType(Me.TrackBar1, System.ComponentModel.ISupportInitialize).BeginInit()

    Me.SuspendLayout()

    '

    'TrackBar1

    '

    Me.TrackBar1.Dock = System.Windows.Forms.DockStyle.Bottom

    Me.TrackBar1.Location = New System.Drawing.Point(0, 221)

    Me.TrackBar1.Maximum = 500

    Me.TrackBar1.Minimum = 1

    Me.TrackBar1.Name = "TrackBar1"

    Me.TrackBar1.Size = New System.Drawing.Size(292, 45)

    Me.TrackBar1.TabIndex = 0

    Me.TrackBar1.Value = 1

    '

    'ZoomPicBox1

    '

    Me.ZoomPicBox1.AutoScroll = True

    Me.ZoomPicBox1.Dock = System.Windows.Forms.DockStyle.Fill

    Me.ZoomPicBox1.Image = Nothing

    Me.ZoomPicBox1.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High

    Me.ZoomPicBox1.Location = New System.Drawing.Point(0, 0)

    Me.ZoomPicBox1.Name = "ZoomPicBox1"

    Me.ZoomPicBox1.Size = New System.Drawing.Size(292, 221)

    Me.ZoomPicBox1.TabIndex = 1

    Me.ZoomPicBox1.Text = "ZoomPicBox1"

    Me.ZoomPicBox1.Zoom = 1.0!

    '

    'Form1

    '

    Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)

    Me.ClientSize = New System.Drawing.Size(292, 266)

    Me.Controls.Add(Me.ZoomPicBox1)

    Me.Controls.Add(Me.TrackBar1)

    Me.Name = "Form1"

    Me.Text = "Form1"

    CType(Me.TrackBar1, System.ComponentModel.ISupportInitialize).EndInit()

    Me.ResumeLayout(False)

 

  End Sub

 

#End Region

 

  Private Sub TrackBar1_ValueChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles TrackBar1.ValueChanged

    Me.ZoomPicBox1.Zoom = Me.TrackBar1.Value / 100

  End Sub

 

 

  Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load

    Dim dlg As New OpenFileDialog

    dlg.Filter = "Image files|*.BMP;*.JPG;*.TIF;*.GIF"

    If (dlg.ShowDialog() = DialogResult.OK) Then

      Me.ZoomPicBox1.Image = Image.FromFile(dlg.FileName)

    Else

      Application.Exit()

    End If

  End Sub

End Class

 

Figures 1 through 3 shows the test application in operation with the ZoomPicBox in various states of zoom.

Figure 1. Zoomed out.

Figure 2. Normal

Figure 3. Zoomed right in and panned to position.

Return to Windows Forms Tips and Tricks

Return to the GDI+ FAQ

Copyright © Ramuseco Limited 2004-2005 All Rights Reserved.