GDI+ FAQ: Manipulating graphical
objects.
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);
}
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(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();
}
}
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.
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.R |