
Introduction
DrawTools sample shows how to create a Windows Forms application for drawing graphic objects in a Windows client area using mouse and drawing tools. Drawing tools implemented in this sample are: Rectangle, Ellipse, Line, and Pencil. There are well-known techniques for creating such type of applications, like: interaction with mouse, flicker-free drawing, implementing of drawing and selection tools, objects selection, managing of objects Z-order etc. MFC developers may learn all this stuff from MFC sample DRAWCLI. DrawTools C# program reproduces some of DRAWCLI functionality and uses some design decisions from this sample.
DrawTools solution contains two projects: DrawTools Windows Forms application and DocToolkit Class Library. DrawTools implements specific application stuff, and DocToolkit contains some standard classes for file managing.
Main features of the DrawTools solution are described below.
DrawTools classes

DrawArea
- user control which fills main application window client area. Contains instance of the GraphicsList
class. Draws graphic objects, handles mouse input passing commands to GraphicsList
.
GraphicsList
- list of graphic objects. Contains ArrayList
of graphic objects. Talks with each graphic object by generic way using DrawObject
methods.
DrawObject
- abstract base class for all graphic objects.
DrawRectangle
- rectangle graphic object.
DrawEllipise
- ellipse graphic object.
DrawLine
- line graphic object.
DrawPolygon
- polygon graphic object.
Tool
- abstract base class for all drawing tools.
ToolPointer
- pointer tool (neutral tool). Contains implementation for selection, moving, resizing of graphic objects.
ToolObject
- abstract base class for all tools which create new graphic object.
ToolRectangle
- rectangle tool.
ToolEllipse
- ellipse tool.
ToolLine
- line tool.
ToolPolygon
- polygon tool.
DocToolkit Library
DocToolkit Library contains a set of classes which may be used for creation of document-centric Windows Forms applications. Instances of classes exported from the DocToolkit Library are kept in the main form of the DrawTools project and is used for general file-related operations.
Handling of Windows controls state at application idle time
Every Windows Forms application has a number of controls like menu items, buttons, toolbar buttons etc. Depending on current situation and user commands, these controls may have different states: enabled/disabled, checked/unchecked, visible/invisible etc. Every user action may change this state. Setting of controls' state in every message handler may be error-prone. Instead of this, it is better to manage controls' state in some function which is called after every user action. MFC has the great ON_UPDATE_COMMAND_UI
feature which allows to update toolbar buttons' state at application idle time. Such a feature may be implemented also in .NET programs.
Consider the situation when user clicks the Rectangle toolbar button. This button should be checked, and previously active tool should be unchecked. Rectangle button message handler doesn't change form controls' state, it just keeps current selection in some variable. Idle
message handler selects active tool and unselects inactive tool.
private void Form1_Load(object sender, System.EventArgs e)
{
Application.Idle += delegate(object o, EventArgs a)
{
SetStateOfControls();
};
}
public void SetStateOfControls()
{
tbPointer.Pushed = (drawArea.ActiveTool == DrawArea.DrawToolType.Pointer);
tbRectangle.Pushed = (drawArea.ActiveTool==DrawArea.DrawToolType.Rectangle);
tbEllipse.Pushed = (drawArea.ActiveTool == DrawArea.DrawToolType.Ellipse);
tbLine.Pushed = (drawArea.ActiveTool == DrawArea.DrawToolType.Line);
tbPolygon.Pushed = (drawArea.ActiveTool == DrawArea.DrawToolType.Polygon);
menuDrawPointer.Checked =
(drawArea.ActiveTool == DrawArea.DrawToolType.Pointer);
menuDrawRectangle.Checked =
(drawArea.ActiveTool == DrawArea.DrawToolType.Rectangle);
menuDrawEllipse.Checked =
(drawArea.ActiveTool == DrawArea.DrawToolType.Ellipse);
menuDrawLine.Checked = (drawArea.ActiveTool == DrawArea.DrawToolType.Line);
menuDrawPolygon.Checked =
(drawArea.ActiveTool == DrawArea.DrawToolType.Polygon);
}
private void CommandRectangle()
{
drawArea.ActiveTool = DrawArea.DrawToolType.Rectangle;
}
Hit Test
DrawObject
class has virtual
HitTest
function which detects whether a point belongs to graphic object:
public virtual int HitTest(Point point)
{
return -1;
}
Derived classes use virtual
PointInObject
to make hit test. This function is called from HitTest
. DrawRectangle
class implements this function by a simple way:
protected override bool PointInObject(Point point)
{
return rectangle.Contains(point);
}
DrawLine
implementation of this function is more complicated:
protected override bool PointInObject(Point point)
{
GraphicsPath areaPath;
Pen areaPen;
Region areaRegion;
AreaPath = new GraphicsPath();
AreaPen = new Pen(Color.Black, 7);
AreaPath.AddLine(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y);
AreaPath.Widen(AreaPen);
AreaRegion = new Region(AreaPath);
return AreaRegion.IsVisible(point);
}
DrawPolygon
function works by the same way, but AreaPath
contains all lines in the polygon.
Serialization
GraphicList
class implements ISerializable
interface which allows to make binary serialization of the class object. DrawObject
class has two virtual
functions which are used for serialization:
public virtual void SaveToStream(SerializationInfo info, int orderNumber)
{
}
public virtual void LoadFromStream(SerializationInfo info, int orderNumber)
{
}
These functions are implemented in every derived class. Binary file has the following format:
Number of objects
Type name
Object
Type name
Object
...
Type name
Object
This allows to write generic serialization code in the GraphicList
class without knowing any details about serialized objects:
private const string entryCount = "Count";
private const string entryType = "Type";
[SecurityPermissionAttribute(SecurityAction.Demand,
SerializationFormatter=true)]
public virtual void GetObjectData(SerializationInfo info,
StreamingContext context)
{
info.AddValue(entryCount, graphicsList.Count);
int i = 0;
foreach ( DrawObject o in graphicsList )
{
info.AddValue(
String.Format(CultureInfo.InvariantCulture,
"{0}{1}",
entryType, i),
o.GetType().FullName);
o.SaveToStream(info, i);
i++;
}
}
protected GraphicsList(SerializationInfo info, StreamingContext context)
{
graphicsList = new ArrayList();
int n = info.GetInt32(entryCount);
string typeName;
object drawObject;
for ( int i = 0; i < n; i++ )
{
typeName = info.GetString(
String.Format(CultureInfo.InvariantCulture,
"{0}{1}",
entryType, i));
drawObject = Assembly.GetExecutingAssembly().CreateInstance(
typeName);
((DrawObject)drawObject).LoadFromStream(info, i);
graphicsList.Add(drawObject);
}
}
Undo - Redo
Undo-Redo functionality is added to the program using the article The Command Pattern and MVC Architectures by David Veeneman. Class Command
is abstract base class for all commands representing user activity: CommandAdd
, CommandChangeState
, CommandDelete
, CommandDeleteAll
. Every Command
-derived class can make two basic operations: Undo which returns draw area to the state before executing this command, and Redo which executes this command again. CommandChangeState
is used for resizing, moving of objects and changing objects properties. Class UndoManager
keeps list of executed commands (history) and makes operations Undo, Redo and Add command to History. Undo-Redo functionality is implemented only in 2005 version of DrawTools project.