Manipulating graphical objects.

This article was originally presented without the ability to transform graphics and backtrack the mouse. The revised passages add that ability and an example of a new demo showing the process of transforming and backtracking is included at the end of the article.

                                        Bob Powell.

 

Many people ask "I can draw a rectangle on the screen but how do I drag it somewhere else?" Unfortunately, the type of graphics system provided by Windows just doesn't do that right off the bat.

 

GDI and GDI+ are immediate mode graphics systems. This is to say that the effects of the drawing commands are seen instantly and only remain on screen for as long as the screen is left alone. Furthermore, the temporary change in pixels is the only record of that command so there is no object that you can get hold of to change the position or size of the rectangle you just drew.

 

In order to perform functions such as the selection, deletion or resize of a shape, you must create a retained mode graphics system that stores a collection of objects that know how to draw themselves and how to detect whether the mouse is in them or not.

 

Once you have this simple structure in place, you can get the object currently under the mouse position and do what you like to it depending on the users actions.

 

Listing 1 shows a typical retained mode object structure. It consists of a basic primitive object that encapsulates position, size and colour data. This object also provides two important methods, a Draw method that will paint the object and a hit-test method which simply returns true or false depending on the hit-test position provided.

 

The primitive objects are contained within a specialized collection in a simple list. The last ones in the list being the highest, or topmost, in the Z order.

 

Finally, a couple of specialized objects, derived from the primitive base class, provide the abilities to draw and hit-test real shapes.

 

using System;

using System.Drawing;

using System.Drawing.Drawing2D;

using System.Collections;

 

namespace RetainedMode

{

  /// <summary>

  /// Describes a simple primitive retained mode object

  /// Each primitive object has a location, size and colour.

  /// Specialized draw and hit test routines in derived classes

  /// customize the behaviour of the actual shapes

  /// </summary>

  public class Primitive

  {

    Size _size;

    Color _color;

    Point _location;

    bool _highlight;

 

    public bool Highlight

    {

      get{return _highlight;}

      set{_highlight=value;}

    }

   

    public Size Size

    {

      get{return _size;}

      set{_size=value;}

    }

 

    public Color Color

    {

      get{return _color;}

      set{_color=value;}

    }

 

    public Point Location

    {

      get{return _location;}

      set{_location=value;}

    }

 

    public Primitive()

    {

      //create a random color

      Random r = new Random((int)DateTime.Now.Millisecond);

      _color=Color.FromArgb(r.Next(255),r.Next(255),r.Next(255));

    }

 

    public virtual void Draw(Graphics g)

    {

      // overridden in the derived class

    }

 

    public virtual bool HitTest(Point p)

    {

      //default behaviour

      return new Rectangle(_location,_size).Contains(p);

    }

  }

 

  /// <summary>

  /// A specialized collection for storing primitive objects

  /// </summary>

  public class PrimitiveCollection : CollectionBase

  {

    public PrimitiveCollection() : base()

    {

    }

 

    /// <summary>

    /// Add a primitive object to the collection.

    /// </summary>

    /// <param name="o"></param>

    public void Add(Primitive o)

    {

      this.List.Add(o);

    }

 

    /// <summary>

    /// Get or set a primitive object by index

    /// </summary>

    public Primitive this[int index]

    {

      get

      {

        return (Primitive)List[index];

      }

      set

      {

        List[index]=value;

      }

    }

 

    /// <summary>

    /// Remove a primitive object from the collection.

    /// </summary>

    /// <param name="o"></param>

    public void Remove(Primitive o)

    {

      List.Remove(o);

    }

  }

 

  /// <summary>

  /// A square object derived from the Primitive

  /// </summary>

  public class Square : Primitive

  {

    public Square() : base()

    {

    }

 

    /// <summary>

    /// Overidden to draw the square object.

    /// </summary>

    /// <param name="g"></param>

    public override void Draw(Graphics g)

    {

      SolidBrush b = new SolidBrush(this.Color);

      g.FillRectangle(b,new Rectangle(this.Location, this.Size));

      b.Dispose();

      if(Highlight)

      {

        Pen p = new Pen(Color.Red,3);

        p.DashStyle=DashStyle.DashDot;

        g.DrawRectangle(p,new Rectangle(this.Location, this.Size));

        p.Dispose();

      }

    }

  }

 

  /// <summary>

  /// A circular object derived from the Primitive.

  /// </summary>

  public class Ellipse : Primitive

  {

    public Ellipse() : base()

    {

    }

 

    /// <summary>

    /// Overridden to draw the elliptical object

    /// </summary>

    /// <param name="g"></param>

    public override void Draw(Graphics g)

    {

      SolidBrush b = new SolidBrush(this.Color);

      g.FillEllipse(b,new Rectangle(this.Location, this.Size));

      b.Dispose();

      if(Highlight)

      {

        Pen p = new Pen(Color.Red,3);

        p.DashStyle=DashStyle.DashDot;

        g.DrawEllipse(p,new Rectangle(this.Location, this.Size));

        p.Dispose();

      }

    }

 

    /// <summary>

    /// overridden from the base class to provide exact hit testing of the ellipse, excluding the

    /// extra corners added by the rectangular nature of the location and size definitions

    /// </summary>

    /// <param name="p">The point to hit test</param>

    /// <returns>True if the point is in the ellipse</returns>

    public override bool HitTest(Point p)

    {

      GraphicsPath pth = new GraphicsPath();

      pth.AddEllipse(new Rectangle(Location,Size));

      bool retval = pth.IsVisible(p);

      pth.Dispose();

      return retval;

    }

  }

}

 

In a form, this simple yet effective collection of objects may be drawn like so:

 

    protected override void OnPaint(PaintEventArgs e)

    {

      foreach(Primitive p in _primitives)

        p.Draw(e.Graphics);

    }

 

Sometimes the view might need to be manipulated with a graphical transform. This is easily accomplished by setting up the transform before the objects are drawn like so:

 

    protected override void OnPaint(PaintEventArgs e)

    {

      e.Graphics.ScaleTransform(0.5f,0.5f);

      foreach(Primitive p in _primitives)

        p.Draw(e.Graphics);

    }

 

The adjustment shown scales the transform by 0.5 in each direction effectively zooming the view out to half size. Other transforms may also be used such as a translation which would pan the view by moving the origin to a different position or even a rotation. In these cases the objects drawn on screen will be shown in a different physical position even though they still draw themselves in the location, style and size that they have set in their properties.

 

 

In order for the user to click on an object and manipulate it in some way, the program must test each item against the current mouse position, checking to see if the mouse is in the object or not. For the purposes of this demonstration, the item selected is always the topmost in the Z order at a particular point. The OnMouseMove override looks like this:

 

    private void Form1_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)

    {

      _lastPos=_curPos;

      _curPos=new Point(e.X,e.Y);

 

      if(!_dragging)

      {

        _topPrimitive = null;

        bool needsInvalidate=false;

        foreach(Primitive p in _primitives)

        {

          if(p.Highlight==true)

          {

            needsInvalidate=true;

            p.Highlight=false;

          }

          if(p.HitTest(_curPos))

          {

            _topPrimitive = p;

          }

        }

 

        if(_topPrimitive!=null)

        {

          needsInvalidate=true;

          _topPrimitive.Highlight=true;

        }

 

        if(needsInvalidate)

          Invalidate();

      }

      else

      {

        //Move the primitive by the difference between the last mouse position and this mouse position

        _topPrimitive.Location=new Point(_topPrimitive.Location.X+(_curPos.X-_lastPos.X),_topPrimitive.Location.Y+(_curPos.Y-_lastPos.Y));

        Invalidate();

      }

   

    }

 

The MouseMove handler has two purposes. First, it detects whether the mouse is in an object by hit testing all objects on the page. If this returns true, the object will be highlighted for identification. Secondly, it performs the actual dragging of the objects on the page when the mouse button is pressed.

 

Other specialized shapes that know how to draw themselves and to hit-test their boundaries could easily be derived from the Primitive shape and used in the list.

 

The code that does the mouse button servicing is quite simple:

 

    private void Form1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)

    {

      //If the mouse is in a primitive, we drag it.

      if(_topPrimitive!=null)

        _dragging=true;

    }

 

    private void Form1_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)

    {

      _dragging=false;

    }

 

As you can see, if the mouse is inside an object when the mouse button is pressed, the _dragging flag is set true and the mouse move events are then used to update the position of the object according to the difference in the current and previous position of the mouse cursor.

 

When the button is released, _dragging is set false and the user manipulation stops.

 

Note that the code above assumes that the graphics are a 1:1 representation of whatever is stored in the object list. In the case of a Graphics object that has been scaled, translated or even rotated, the objects will be drawn in a different position on the screen to that of their positions and sizes in the virtual world of the graphical object model. In order to successfully detect the mouse in a graphical object there has to be a way of relating the mouse coordinates to that virtual world. GDI+ and the Matrix class provide a way for us to do this by using the same transform that was used to draw the objects and then inverting it. This has the effect of "backtracking" the mouse from the physical screen coordinates to the virtual model coordinates no matter what transform is used.

 

The code below shows the modified MouseMove routine that backtracks the mouse.

 

    private void Form1_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)

    {

      _lastPos=_curPos;

      _curPos=new Point(e.X,e.Y);

 

      Matrix mx=new Matrix(); //Create an identity matrix

      mx.Scale(0.5f,0.5f); //Apply the same transformation as we did for the drawing

      mx.Invert();  //Invert the matrix so that mouse positions are converted to the virtual space 

      Point[] pts=new Point[]{_curPos};

      mx.TransformPoints(pts); //Transform the current position by the inverted matrix

      _curPos=pts[0]; //Use the newly transformed point as the mouse position

 

      if(!_dragging)

      {

        _topPrimitive = null;

        bool needsInvalidate=false;

        foreach(Primitive p in _primitives)

        {

          if(p.Highlight==true)

          {

            needsInvalidate=true;

            p.Highlight=false;

          }

          if(p.HitTest(_curPos))

          {

            _topPrimitive = p;

          }

        }

 

        if(_topPrimitive!=null)

        {

          needsInvalidate=true;

          _topPrimitive.Highlight=true;

        }

 

        if(needsInvalidate)

          Invalidate();

      }

      else

      {

        //Move the primitive by the difference between the last mouse position and this mouse position

        _topPrimitive.Location=new Point(_topPrimitive.Location.X+(_curPos.X-_lastPos.X),_topPrimitive.Location.Y+(_curPos.Y-_lastPos.Y));

        Invalidate();

      }

   

    }

 

All mouse positions are now translated from the physical space on the screen to the virtual space of the object model.

 

For the purposes of this demonstration, primitives are placed at random by a simple button click.

 

    private void toolBar1_ButtonClick(object sender, System.Windows.Forms.ToolBarButtonClickEventArgs e)

    {

      Primitive p=null;

      switch((string)e.Button.Tag)

      {

        case "Ellipse":

          p = new Ellipse();

          break;

        case "Square":

          p=new Square();

          break;

      }

 

      Random r = new Random((int)DateTime.Now.Millisecond);

 

      p.Location=new Point(r.Next(400),r.Next(400));

      p.Size=new Size(5+r.Next(100),5+r.Next(100));

 

      this._primitives.Add(p);

 

      Invalidate();

 

    }

 

The application when compiled and running, looks like this:

 

 

The blue rectangle has been detected and can be dragged to a new position.

 

The full listing of the form which runs this application is included here:

 

using System;

using System.Drawing;

using System.Collections;

using System.ComponentModel;

using System.Windows.Forms;

using System.Data;

 

namespace RetainedMode

{

  /// <summary>

  /// Summary description for Form1.

  /// </summary>

  public class Form1 : System.Windows.Forms.Form

  {

    private System.Windows.Forms.ToolBar toolBar1;

    private System.Windows.Forms.ToolBarButton toolBarButton1;

    private System.Windows.Forms.ToolBarButton toolBarButton2;

    private System.ComponentModel.IContainer components;

 

    PrimitiveCollection _primitives=new PrimitiveCollection();

 

    bool _dragging;

    Primitive _topPrimitive;

 

    Point _lastPos=new Point(0,0);

    Point _curPos=new Point(0,0);

 

    public Form1()

    {

      //

      // Required for Windows Form Designer support

      //

      InitializeComponent();

 

      this.SetStyle(ControlStyles.AllPaintingInWmPaint|

        ControlStyles.UserPaint|

        ControlStyles.DoubleBuffer,true);

 

    }

 

    /// <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.toolBar1 = new System.Windows.Forms.ToolBar();

      this.toolBarButton1 = new System.Windows.Forms.ToolBarButton();

      this.toolBarButton2 = new System.Windows.Forms.ToolBarButton();

      this.SuspendLayout();

      //

      // toolBar1

      //

      this.toolBar1.Buttons.AddRange(new System.Windows.Forms.ToolBarButton[] {

                                            this.toolBarButton1,

                                            this.toolBarButton2});

      this.toolBar1.DropDownArrows = true;

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

      this.toolBar1.Name = "toolBar1";

      this.toolBar1.ShowToolTips = true;

      this.toolBar1.Size = new System.Drawing.Size(432, 42);

      this.toolBar1.TabIndex = 0;

      this.toolBar1.ButtonClick += new System.Windows.Forms.ToolBarButtonClickEventHandler(this.toolBar1_ButtonClick);

      //

      // toolBarButton1

      //

      this.toolBarButton1.Tag = "Ellipse";

      this.toolBarButton1.Text = "Ellipse";

      this.toolBarButton1.ToolTipText = "Drop an ellipse";

      //

      // toolBarButton2

      //

      this.toolBarButton2.Tag = "Square";

      this.toolBarButton2.Text = "Square";

      this.toolBarButton2.ToolTipText = "Drop a square";

      //

      // Form1

      //

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

      this.BackColor = System.Drawing.Color.White;

      this.ClientSize = new System.Drawing.Size(432, 325);

      this.Controls.Add(this.toolBar1);

      this.Name = "Form1";

      this.Text = "Retained Mode Graphics demo";

      this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.Form1_MouseDown);

      this.MouseUp += new System.Windows.Forms.MouseEventHandler(this.Form1_MouseUp);

      this.MouseMove += new System.Windows.Forms.MouseEventHandler(this.Form1_MouseMove);

      this.ResumeLayout(false);

 

    }

    #endregion

 

    /// <summary>

    /// The main entry point for the application.

    /// </summary>

    [STAThread]

    static void Main()

    {

      Application.Run(new Form1());

    }

 

    private void toolBar1_ButtonClick(object sender, System.Windows.Forms.ToolBarButtonClickEventArgs e)

    {

      Primitive p=null;

      switch((string)e.Button.Tag)

      {

        case "Ellipse":

          p = new Ellipse();

          break;

        case "Square":

          p=new Square();

          break;

      }

 

      Random r = new Random((int)DateTime.Now.Millisecond);

 

      p.Location=new Point(r.Next(400),r.Next(400));

      p.Size=new Size(5+r.Next(100),5+r.Next(100));

 

      this._primitives.Add(p);

 

      Invalidate();

 

    }

 

    protected override void OnPaint(PaintEventArgs e)

    {

      foreach(Primitive p in _primitives)

        p.Draw(e.Graphics);

    }

 

    private void Form1_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)

    {

      _lastPos=_curPos;

      _curPos=new Point(e.X,e.Y);

 

      if(!_dragging)

      {

        _topPrimitive = null;

        bool needsInvalidate=false;

        foreach(Primitive p in _primitives)

        {

          if(p.Highlight==true)

          {

            needsInvalidate=true;

            p.Highlight=false;

          }

          if(p.HitTest(new Point(e.X,e.Y)))

          {

            _topPrimitive = p;

          }

        }

 

        if(_topPrimitive!=null)

        {

          needsInvalidate=true;

          _topPrimitive.Highlight=true;

        }

 

        if(needsInvalidate)

          Invalidate();

      }

      else

      {

        //Move the primitive by the difference between the last mouse position and this mouse position

        _topPrimitive.Location=new Point(_topPrimitive.Location.X+(_curPos.X-_lastPos.X),_topPrimitive.Location.Y+(_curPos.Y-_lastPos.Y));

        Invalidate();

      }

    

    }

 

    private void Form1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)

    {

      //If the mouse is in a primitive, we drag it.

      if(_topPrimitive!=null)

        _dragging=true;

    }

 

    private void Form1_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)

    {

      _dragging=false;

    }

 

  }

}

 

Revised Example

 

Click here to see the code showing transformations and backtracking.

Back to the GDI+ FAQ.

Copyright Robert W. Powell 2003. All rights reserved.