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