JAL Computing

C++COMProgramming .NET Mac Palm CPP/CLI Hobbies

 

Home
Up

Chapter 19 "An Object Oriented Draw Program"

Congratulations! If you have survived these past 18 chapters, it is finally time to put together a working program that demonstrates many of the concepts presented in this "Twisted Tutorial on Object Oriented Programming". 

The following "Draw" program dynamically populates a "Shape" menu with DrawableShape choices and draws the shapes on mouse up. The program then adds the DrawableShape object to an ArrayList named drawHistory. Each DrawableShape object in the ArrayList stores its own state including color, size, brush width and position. Each concrete DrawableShape object such as Circle, Triangle and Square, knows how to draw itself. On Invalidate(), the screen is refreshed by iterating over the ArrayList of DrawableShapes and calling DrawYourself(g) on each object. Through the magic of polymorphism, and object state, the screen is automagically repainted! Pretty cool.

As simple as this project may seem, it implements a number of object oriented concepts discussed in this tutorial including:

bulletPolymorphism
bulletComposite Extensible Interfaces
bulletEncapsulation
bulletAbstract Skeletal Base Class
bullet"Growable" Class Factory
bulletExtensible Classes
bulletInheritance
bulletContainment
bulletAbstraction
bulletTypes
bulletObject State and Behavior
bulletDynamic Class Loading
bulletExplicit Preconditions and Exceptions
bulletIndirection

As a drawing program, the code naturally makes use of the GDI+ (Graphics Design Interface Plus). To queue up a repaint of the entire screen you can call Invalidate() and then redraw the screen by overriding the OnPaint method.

Add Shape Sub Menu Items at Run Time

The user can choose a DrawableShape from a menu that is created dynamically at run time. The Triangle class was loaded from a "plugins" folder at run time.

Here is the code that dynamically creates the Shape menu sub items at runtime. The typeFactory contains a Hashtable of IDrawableShape Types added statically and at run time.

// Done loading factory
// Populate Shape Menu by
// iterating over objects in our type based factory
ICollection collection= typeFactory.Keys();
IEnumerator enumerator= collection.GetEnumerator();
while (enumerator.MoveNext())
{
	String menuName= (string)enumerator.Current;
	menuItemShape.MenuItems.Add(menuName,
		new EventHandler(this.shape_Click));
}

All Shape menu sub item events are passed to a single event handler shape_Click. The shape_Click method simply sets a class String variable currentShape to the selected Shape. Clicking on a menu item appears to invalidate the menu rectangle, forcing a repaint of the area occupied by the menu.

private String currentShape= "Circle"; // default value
private void shape_Click(object sender, System.EventArgs e)
{
	currentShape= ((MenuItem)sender).Text;
}

Add Support for User Selected Shape Properties

The user can also select Color, Size and Brush Width.

The properties of the current Shape object are also stored in class variables.

// default DrawableShape properties
private Color color= Color.Black;
private int brushWidth= 2;
private int size= 50;
private Point position= new Point(0,0);
private String currentShape= "Circle";

The Shape property menu event handlers simply set the appropriate class properties as in:

private void menuItemColorRed_Click(object sender, System.EventArgs e)
{
	this.color= Color.Red;
}

The framework will call Invalidate(rect) since the Menu drawing may overwrite a DrawableShape image. Invalidate(rect) tells the frame to redraw part of itself. This eventually leads to a call to OnPaint.

Trap the OnMouseUp Event

Here is the working application that traps the mouse up event and draws the appropriate shape at the mouse up coordinates! Even if the screen is "dirtied" by a menu event or frame resize, the screen is restored on Invalidate by iterating over the ArrayList of DrawableShapes and calling DrawYourself in the OnPaint method.

Here is the Form1_MouseUp event handler that draws the currentShape using the mouse coordinates and the current Shape properties:

// trap mouse up and draw shape at mouse position
// add DrawableShape to ArrayList drawHistory
// use this ArrayList to refresh screen on Invalidate
private void Form1_MouseUp(object sender, MouseEventArgs e)
{
	Graphics g= Graphics.FromHwnd(this.Handle);
	if (e.X < 0 || e.Y < 0) 
	{
		g.DrawString("Invalid Mouse Coordinates.",
			new Font("Arial",10),
			new SolidBrush(Color.Red),
			10,
			10,
			StringFormat.GenericDefault);
		return;
	}
	IDrawableShape ds= typeFactory.GetInstance(currentShape);
	ds.Position= new Point(e.X,e.Y);
	ds.Size= this.size;
	ds.Color= this.color;
	ds.BrushWidth= this.brushWidth;
	ds.DrawYourself(g);
	drawHistory.Add(ds);
}

Note: The method validates e.X and e.Y since the Position property explicitly ASSERTS that e.X and e.Y must be equal or greater than zero.

Here is the code for creating the Triangle class:

using System;
namespace DrawablePlugIns
{
	// Separate DLL used to create a new concrete class Triangle
	// This DLL can then be added to a "plugins" folder
	// An application can then dynamically add the Triangle class
	// at runtime!
	using System;
	using System.Drawing; // project references System.Drawing.dll
	using JALInterfaces; // project references JALInterfaces.dll
	// Create a new concrete class Triangle to be loaded dynamically
	public class Triangle : AbstractDrawableShape
	{
		public override void DrawYourself(Graphics g)
		{
			Pen p= new Pen(Color,BrushWidth);
			Point point1= new Point(Position.X+Size/2,Position.Y);
			Point point2= new Point(Position.X,Position.Y+Size);
			Point point3= new Point(Position.X+Size,Position.Y+Size);
			Point[] points= {point1,point2,point3};
			g.DrawPolygon(p,points);
		}		
	}
}

The Triangle class extends from the AbstractDrawableShape class defined in a separate dll.

using System;
using System.Drawing;
namespace JALInterfaces
{
	// Refactored Drawable Interface
	public interface IDrawable
	{
		void DrawYourself(Graphics g);
	}
	// ASSERT P.X>=0 and P.Y>=0;
	public interface IPositional
	{
		Point Position
		{
			get;
			set;
		}
	}
	public interface ISizeable
	{
		int Size
		{
			get;
			set;
		}
	}
	public interface IColorable 
	{
		Color Color 
		{
			get;
			set;
		}
	}
	public interface IBrushWidth 
	{
		int BrushWidth
		{
			get;
			set;
		}
	}
	// Extensable Composite Interface IDrawableShape
	public interface IDrawableShape : 
				IDrawable, 
				IPositional, 
				IColorable, 
				ISizeable, 
				IBrushWidth {}
	
	// Factor repeated code into Abstract Class with Default Implementations
	// Concrete classes of IDrawableShape should inherit from this class
	// and override DrawYourself
	public abstract class AbstractDrawableShape : IDrawableShape 
	{
		private int size=1;
		private Point position= new Point(0,0);
		private Color color= Color.Black;
		private int brushWidth= 1;
		public int BrushWidth 
		{
			get {return brushWidth;}
			set 
			{
				if (value>0) {brushWidth= value;}
			}
		}
		public int Size
		{
			get {return size;}
			set 
			{
				if (value>0) {size= value;}
			}
		}
		// ASSERT p.x and p.y >=0
		public Point Position
		{
			get {return position;}
			set 
			{
				if ((value.X >=0) && (value.Y >=0))
				{
					position= value;
				}
				else 
				{
					throw new ArgumentException();
				}
			}
		}
		public Color Color
		{
			get{return color;}
			set 
			{
				color= value;
			}
		}
		virtual public void DrawYourself(Graphics g)
		{
			g.DrawString("Drawable",
				new Font("Arial",size),
				new SolidBrush(color),
				position.X, 
				position.Y,
				StringFormat.GenericDefault);
		}
	}
}

Clear the Screen

The screen is "cleared" by removing all of the DrawableShapes in the ArrayList and manually calling Invalidate(). This eventually forces a re-draw of the entire screen.

Here is the menuItemClear event handler.

private void menuItemClear_Click(object sender, System.EventArgs e)
{
	drawHistory.Clear();
	this.Invalidate(); // repaint entire screen!
}

The screen is repainted in the OnPaint method like this:

// Use array of DrawableShape objects to repaint
// and restore screen state on invalidate. Each object
// stores its own state and faithfully redraws itself! I now
// believe the framework is smart enough to limit repainting
// to the "dirty" area of the screen rather than repaint
// the entire screen.
protected override void OnPaint(PaintEventArgs e) 
{
	Graphics g= e.Graphics;
	foreach (IDrawableShape ds in drawHistory) 
	{
		ds.DrawYourself(g);
	}
}

Each object in the ArrayList "knows" its state and knows how to draw itself. The ArrayList drawHistory must contain only non null objects that implement the IDrawableShape interface or an exception will be thrown at run time.

Type Based Class Factory Revisited

Finally, here again is the type based class factory from Chapter 18:

// the class factory
class TypeBasedDrawableFactory 
{
	private Hashtable hash= new Hashtable();
	public bool Add(Type t) 
	{
		if ((t != null)&& 
			!hash.Contains(t.Name) 
			&& (typeof(IDrawableShape).IsAssignableFrom(t))
			&& !t.IsAbstract)
		{
			hash.Add(t.Name,t);
			return true;
		}
		else return false;
	}
	public int Count() 
	{
		return hash.Count;
	}
	public ICollection Keys() 
	{
		return hash.Keys;
	}
	public IDrawableShape GetInstance(string key) 
	{
		if (key == null || !hash.Contains(key)) 
		{
			return null;
		}
		else 
		{
			Type t= (Type)hash[key];
				// dynamically load this class
				return (IDrawableShape)Activator.CreateInstance(t);
			//else return null;
		}
	}
	public bool Contains(string key) 
	{
		if (key != null) 
		{
			return hash.Contains(key);
		}
		else {return false;}
	}
}

Download the Projects

You can download the three projects JALInterfaces, DrawablePlugIn, and TestDrawFactory by clicking here.

Congratulations! Through hard work and persistence, you have gone from a noob to a Twisted Expert in Object Oriented Programming in C#. Hopefully this journey has been enlightening, and fun!

Jeff

Send mail to [email protected] with questions or comments about this web site. Copyright © 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 © 
Last modified: 08/04/09
Hosted by www.Geocities.ws

1