
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); 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 Copyright © Ramuseco Limited 2004-2005 All Rights Reserved.
|