Visual C# 2005 (Windows) Guide
Fractals: Zoom Feature

The RubberBand Class

One of the most natural ways to enable the user to select a portion of the image to zoom in on is to use a reversible selection rectangle rather like you might find in a graphics application. This is sometimes called a rubberband selection tool.

The bulk of the programming logic can be placed in a separate class. That allows you to export and reuse the class in another application.

Add a class to your project called RubberBand.cs.

Add the following using statements to the top of the class,

using System.Drawing;
using System.Windows.Forms;

Fields

The fields for this class are as follows,

public enum RubberBandState { Inactive, Starting, Moving };
private Point StartPoint;
private Point EndPoint;
private RubberBandState CurrentState = RubberBandState.Inactive;
private Control Surface;

The enumeration lists the 3 states our rubberband can be in. It is inactive when the user has not clicked on the picture box and attempted to drag the picture. It is starting when the user has first clicked on the picture box. It is moving when the user is dragging the selection area.

We are using the Point structure to store the start and end coordinates of the selection. The variable CurrentState keeps track of the state of the rubberband. Finally, the field Surface is used to store a reference to the control that we want to draw the rectangle on.

Constructor

public Rubberband(Control ControlSurface)
{
   Surface = ControlSurface;
}

The constructor method is quite simple - all we need to do is make the field Surface equal to the control (picture box) that we want to draw on.

Property

We will have one property in this class - the SelectedRectangle. To make this property read-only, we include no set statement. This property will be used to tell the user which part of the control has been selected.

public Rectangle SelectedRectangle
{
   get
   {
      Rectangle selectedRect = new Rectangle();
      selectedRect.X = StartPoint.X < EndPoint.X ? StartPoint.X : EndPoint.X;
      selectedRect.Y = StartPoint.Y < EndPoint.Y ? StartPoint.Y : EndPoint.Y;
      selectedRect.Width = Math.Abs(EndPoint.X - StartPoint.X);
      selectedRect.Height = Math.Abs(EndPoint.Y - StartPoint.Y);
      return selectedRect;
   }
}

The conditional operator (?:)has been used here because the rectangle we draw on the picture box is reversible - that means that the end of the selection could be left or up from the starting point. An if statement would have taken several lines of code. The conditional operator works by evaluating a boolean expression. If that evaluates to true, the expression after the ? is evaluated. If not, the expression after the : is evaluated.

Methods

The KeepInView method will help to ensure that any selection remains within the bounds of the control. It accepts 2 parameters by reference, meaning that adjustments to the parameters are written back to the variables passed to the method.

private void KeepInView(ref Point origin)
{
   if (origin.X < 0)
   {
      origin.X = 0;
   }
   if (origin.X > Surface.ClientSize.Width)
   {
      origin.X = Surface.ClientSize.Width - 1;
   }
   if (origin.Y < 0)
   {
      origin.Y = 0;
   }
   if (origin.Y > Surface.ClientSize.Height)
   {
      origin.Y = Surface.ClientSize.Height - 1;
   }
}

The Start method is called when the user first touches the mouse down onto the control.

public void Start(int x, int y)
{
   StartPoint.X = x;
   StartPoint.Y = y;
   EndPoint.X = x;
   EndPoint.Y = y;
   KeepInView(ref StartPoint);
   CurrentState = RubberBandState.Starting;
}

The Move method is called when the user drags the selection area across the control.

public void Move(int x, int y)
{
   Point newPoint = new Point(x, y);
   KeepInView(ref newPoint);
   switch (CurrentState)
   {
      case RubberBandState.Inactive:
         break;
      case RubberBandState.Starting:
         EndPoint = newPoint;
         DrawFrame();
         CurrentState = RubberBandState.Moving;
         break;
      case RubberBandState.Moving:
         DrawFrame();
         EndPoint = newPoint;
         DrawFrame();
         break;
   }
}

The Stop method is called when the user lifts up the mouse button to end the selection.

public void Stop()
{
   DrawFrame();
   CurrentState = RubberBandState.Inactive;
}

Finally, the DrawFrame method actually draws the rectangle on the control.

private void DrawFrame()
{
   Point exactStart = Surface.PointToScreen(StartPoint);
   Point exactEnd = Surface.PointToScreen(EndPoint);
   Size rectSize = new Size(exactEnd.X - exactStart.X, exactEnd.Y - exactStart.Y);
   Rectangle drawRect = new Rectangle(Surface.PointToScreen(StartPoint), rectSize);
   ControlPaint.DrawReversibleFrame(drawRect, Color.Black, FrameStyle.Dashed);
}

Although we haven't written any code in the application that will use this class, running the program at this point will expose any compiler errors in the code at this stage.

Using The RubberBand Class

We have a few things to do to implement the zoom feature.

Start by adding the following global variables to the form's code window.

bool IsDrawing = false;
bool IsRubberBand = false;
Rubberband SelectedArea;

Create a Form_Load event for the form and add the following lines of code.

SelectedArea = new Rubberband(picMandel);
CreateMandelbrotImage();

This creates an instance of the RubberBand class and plots the Mandelbrot image when the form is first loaded.

Adapt the CreateMandelbrotImage() procedure so that the first and last lines of the procedure are,

IsDrawing = true;

and

IsDrawing = false;

The MouseDown event for the picture box should read as follows,

private void picMandel_MouseDown(object sender, MouseEventArgs e)
{
   if (!IsDrawing)
   {
      SelectedArea.Start(e.X, e.Y);
      IsRubberBand = true;
   }
}

The MouseMove event should be adapted as follows,

private void picMandel_MouseMove(object sender, MouseEventArgs e)
{
   if (!IsDrawing)
   {
      string a = System.Convert.ToString((e.X * unitsPerPixel) + minA);
      string b = System.Convert.ToString(((bmpMandel.Height - e.Y) * unitsPerPixel) + minB);
      string coords = "(" + a + ", " + b + ")";
      toolStripStatusLabel1.Text = coords;
      if (IsRubberBand)
      {
         SelectedArea.Move(e.X, e.Y);
      }
   }
}

Before we write the MouseUp event which will end the selection, we need to think about how we adjust the rectangular shape that the user selects into a square. To do this we make adjust the rectangle into a square based on the longest side of the selection. We use the centre of the selected area to determine the positioning of the square.

private void CreateNewView()
{
   Rectangle newRect = new Rectangle();
   Point centrePoint = new Point(SelectedArea.SelectedRectangle.X + (SelectedArea.SelectedRectangle.Width / 2), SelectedArea.SelectedRectangle.Y + (SelectedArea.SelectedRectangle.Height / 2));
   //make into square the length of longest side
   if (SelectedArea.SelectedRectangle.Width > SelectedArea.SelectedRectangle.Height)
   {
      newRect.Width = SelectedArea.SelectedRectangle.Width;
      newRect.Height = newRect.Width;
   }
   else
   {
      newRect.Height = SelectedArea.SelectedRectangle.Width;
      newRect.Width = newRect.Height;
   }
   //shift so that the square is centred on the centre of the rectangle
   Point newOrigin = new Point();
   newOrigin.X = centrePoint.X - (newRect.Width / 2);
   newOrigin.Y = centrePoint.Y - (newRect.Height / 2);
   newRect.Location = newOrigin;
   //convert into plot values
   minA = minA + (newRect.X * unitsPerPixel);
   maxA = minA + (newRect.Width * unitsPerPixel);
   maxB = maxB - (newRect.Y * unitsPerPixel);
   minB = maxB - (newRect.Height * unitsPerPixel);
}

Finally, add a MouseUp event for the picture box.

private void picMandel_MouseUp(object sender, MouseEventArgs e)
{
   if (IsRubberBand)
   {
      if (!IsDrawing)
      {
         SelectedArea.Stop();
         IsRubberBand = false;
         CreateNewView();
         CreateMandelbrotImage();
      }
   }
}

You need to do some testing now. You should find that you can zoom in quite nicely on different sections of the image.

Improvements

The image you see does depend in some part on the number of iterations allowed in the application. Creating a simple dialog box to allow the user (and you) to change the maximum number of iterations would be quite useful. Be careful though, the more iterations allowed, the longer it takes to create the image. You can, however, get some interesting results by varying this number.