There was a time, not too long ago, when browser-based user interfaces were considered both the status quo and the Next Great Thing.

The demand for Windows Forms-based applications started to dwindle as the developer community fully embraced browser/server applications with their centralized server components and ubiquitous user interfaces. .NET, however, brings a much more powerful library of distributed communication technologies (such as Web services and remoting). As a result, .NET developers are seeing some of these traditionally browser-based applications becoming, more simply, Web-enabled and less tied to a browser. In short, developers can now see a very real business case for building distributed applications on Windows Forms technology.

As it happens, this article stemmed out of just such a business case. We were recently presented with a project that required a portion of the application to generate a visual representation of some data onto a Windows Form. Users of the application would then be able to move these graphical data elements around a page, add new elements, save, print, etc. All of these activities were second nature to a Windows Forms application and the System.Drawing namespace.

Instead of examining the intricacies of the System.Drawing namespace classes, we are going to start with a basic set of requirements that a developer might face. We will then detail how you can use the .NET namespaces and associated classes to create a solution to meet these requirements. As it happens, this article remains pretty focused on the System.Drawing namespace. However, we present a lot of good code here from the System.Windows.Forms and System.IO namespaces to name a few.

Let's get started by looking at a basic set of requirements.

The Requirements

The first step is to determine what the basic requirements are for a drawing project. We started by examining a few drawing applications on our laptops in an attempt to visualize some of the problems these types of applications have to solve. We looked at Microsoft Word with its visual representation of a page. Some items of note included things like margins, scrolling, and zooming. Visio also contains many of the characteristics we are interested in (see Figure 1).

Figure 1: The Visio drawing page with its rulers, blue borders, and grid lines.
Figure 1: The Visio drawing page with its rulers, blue borders, and grid lines.

By examining Visio, it was evident that there are a lot of basic requirements that a drawing program will have to solve before it can be useful to its user base. We started listing these requirements and were at first overwhelmed. However, after trimming, we were able to cut the list back to a somewhat manageable subset (at least for the first release). The desired application will provide the following features:

  • A visual illustration of a page's dimensions based on actual physical size of a printed piece of paper
  • A visual indication of the page's margins
  • A visual grid that sits in the background and allows users to align objects
  • Draw shapes such as lines, rectangles, and ellipses
  • Add blocks of text to the page as well as embed those blocks inside a graphical shape
  • Select an object and move it about the form
  • Save documents to disk and open them at a later time
  • Print and print preview documents
  • Zoom in and out on the page and continue drawing
  • Rotate shapes and text on the page
  • Scale shapes and text elements

You can see that an application that sounds pretty straightforward can soon suffer from major scope creep (and this was the pared-down list). From this list we knew we had to further narrow the requirements to a more manageable scope if we were going to make our deadline. (If you can't extend the schedule, you have to cut features!). In the end we cut the list down to the following features:

  • A visual illustration of a page's dimensions based on actual physical size of a printed piece of paper
  • Draw rectangles and ellipses
  • Select an object and move it about the form
  • Save documents to disk and open them at a later time

This subset will define the scope for this multi-part article. At the end we will have illustrated, and solved, a number of issues that developers will encounter when designing and coding a drawing application.

The Drawing Page User Control

The first technical design decision we made was to encapsulate the concept of a page as a user control. This will allow developers to add a page to any form or application and quickly expose, to their users, items like drawing, selecting, and moving graphical elements.

This user control will have to be able to react to user input, capture it, and be able to store that input. To capture some design decisions we constructed an object model to support the user control. Figure 2 shows the public interface into this control.

Figure 2: The PageControl object model illustrates the classes and their public interfaces.
Figure 2: The PageControl object model illustrates the classes and their public interfaces.

Each class in the object model encapsulates a specific portion of the functionality that the user control will provide. The following describes each of these classes relative to their intended features:

  • PageControl: The PageControl class provides the visual representation of the control (or page). It is the actual user control that a user can drop onto another form. Users of the control will draw on it with a mouse, and therefore the control's main purpose is to respond to that type of input. Each instance of the PageControl class works directly with an instance of the Document class.

  • Document: The Document class, as its name implies, represents an actual document. In our case, a document is a physical file that can contain graphical elements, can be saved to disk, and can be re-opened at a later time. The Document class works with an instance of the Elements class to represent the group of graphical elements in the document.

  • Elements: The Elements class is simply a collection class (derived from CollectionBase) that represents all of the graphical elements in a given document. The Elements class groups Element items.

  • Element: The Element class is an abstract base class (‘MustInherit’ in Visual Basic .NET) that represents an element in a document (like a rectangle). This class defines the basic make-up of all elements that you can place into a document. A number of classes will derive from this base class. At the moment, only the ellipse and rectangle element classes are defined as concrete versions of Element. In the future, we may add a number of additional derived classes.

Now that we had a basic object model in place, we set about solving some of the other key requirements for the control.

Drawing Rectangles and Ellipses

To draw shapes onto the screen we needed more detail about our requirements. We again took inspiration from Visio:

  • The user should be able add shapes with the mouse (rather than via dialog boxes).
  • The user should be able to see what they are drawing as they draw it. That is, the application should draw an outline of the drawing as the mouse moves.
  • The user should be able to pick their fill color, pen color, and line thickness.
  • Drawing elements should imply a natural order (or zOrder). That is, an element drawn last would be on top of other elements that it might overlap. This will allow the developer to add layering functionality (send-to-back / bring-to-front) to the application.

Capturing Mouse Events

To satisfy the first requirement, we knew we had to write code that captures mouse movements and clicks. These mouse events have to provide functionality for the users to draw, select, and move graphical elements about the page. The mouse events that our application needs to capture include:

  • Mouse Down: Triggered when a user pushes the left mouse button down.
  • Mouse Move: Triggered when a user moves the mouse in any direction.
  • Mouse Up: Triggered when a user releases the left mouse button (after pressing down).

The MouseDown Event

For the MouseDown event you simply need to capture the fact that a user has clicked the mouse button. The MouseMove event will use this information to allow drawing based on dragging the mouse. You first expose a property of the PageControl class called Draw. This property is of the type DrawType: an enumeration created to indicate what a user might want to draw. The values for an enumeration are Rectangle, Ellipse, and Nothing. The next step is to initialize a local variable for the Draw property and set its default value to DrawType.Nothing. Before a user can draw anything, they will have to indicate to the control (via the Draw property) what exactly they want to draw.

The next step is to wire up the MouseDown event via a private method, OnMouseDown. Listing 1 shows the complete code for this event.

For drawing purposes you only need to capture two facts inside the event, mouse down and position. Set the local Boolean member variable, m_capture, to true to indicate the user had clicked their mouse. The MouseMove event uses this variable to know when it is in “capture” mode.

You also capture the position (or point) on the screen of the user's cursor where he/she pressed the mouse button. Store this in a member variable to the control called m_sP (for starting point) and make its type is a System.Drawing.Point object. This structure allows you to store both the x and y coordinates of where the mouse was pressed. These coordinates represent a fixed starting point on the form for whatever a user might draw.

The MouseMove Event

Now that you can tell when a user presses the mouse button down, you use this information inside of the MouseMove event to simulate drawing while a user drags their mouse. First, you capture another point to represent the position to where the mouse moved. This point is passed to you on the event's signature via the type MouseEventArgs (e). To store this value, create a new local variable as follows:

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

Once you have captured both the starting point (from the MouseDown event) and the moved-to point, you can use these points to calculate the distance that the mouse moved vertically (height) and horizontally (width) as follows:

int w = Math.Abs(mp.X - m_sP.X);
int h = Math.Abs(mp.Y - m_sP.Y);

Using this information you can paint an object (rectangle or ellipse) that represents the user's dragging. You use a member variable called m_captureR to contain the bounds of these two points as a System.Drawing.Rectangle instance. All that is left is to construct this Rectangle instance in the MouseMove event and then draw the same rectangle to the screen via the form's Paint event.

However, one challenge remains. A user can move the mouse in any direction from the fixed starting point captured in the MouseDown event. For instance, a user might drag the mouse down and to the right. This would result in a drawing that moved from left to right and top down. However, the user could just as easily move the mouse to the left and up (or any other combination across the x and y axis). To handle this, you need some basic code that will check which direction the mouse is moving (based on positive and negative comparisons) and then create the rectangle accordingly. For example, if the mouse is moving up and to the left relative to the fixed starting point, you need to set the bounding rectangle's upper left corner to the move point and its bottom right corner to the start point. You can see the full source code for these checks in Listing 2.

The Paint Event

Now that you know how to capture the bounds of the mouse's movement via a corresponding Rectangle (m_captureR), you need to display this rectangle to the user. To do so, force the control to re-paint using the following line of code inside the MouseMove event:

this.Invalidate();

The Invalidate method of the Control class allows a specific region of the control to be invalidated, which forces it to be repainted. You can intercept this repaint inside the control's Paint event. You can see a complete listing of this event in Listing 3.

The code to display the drawn shape to the user turns out to be pretty straightforward. You first have to verify that the application is in capture mode by checking the member variable, m_capture. You also have to make sure that the user has indicated that they intend to draw something (m_draw). The resulting If statement looks like this:

if (m_capture & m_draw != DrawType.Nothing)
{
DrawCaptureR(g);
}

Drawing the captured rectangle is just as simple. You create a private routine that takes a reference to a valid System.Drawing.Graphics instance as a parameter (passed in from the PaintEventArgs in the Paint event). This routine, called DrawCaptureR, renders the captured rectangle to the control.

To accomplish this rendering feat, first create an instance of the System.Drawing.Pen class. This class, as its name implies, represents a pen that has an ink color and line thickness. For the ink color, let's use a green brush provided by the System.Drawing.Brushes class. Set the pen's thickness (or width) to 1 point as follows:

Pen p = new Pen(Brushes.Green, 1);

Next use the System.Drawing.Drawing2D.DashStyle enumeration to make the pen look like a series of dashes:

p.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;

Finally, verify what the user intended to draw to the control (via the local m_draw) and then output the element to the control's surface using the appropriate method of the Graphics class (in this instance, DrawRectangle):

if (m_draw==DrawType.Rectangle)
g.DrawRectangle(p, m_captureR);
else
g.DrawEllipse(p, m_captureR);

The MouseUp Event

Now that you have successfully captured the drag effect of the mouse and rendered the results to the page control's surface, you need to lock these results to the page once the user releases their drag (by releasing the mouse button). In addition, you need to store this element so it could be re-drawn to the screen later if need be.

To handle these tasks, you intercept the control's MouseUp event. Inside this event (complete code in Listing 4), you turn off capture mode by setting m_capture to false. This stops the control from processing further mouse movement via the MouseMove event. Next, you need to store the resulting graphical element in the object. To do so, you create a variable of the type Element. Then, based on the current drawing type (rectangle or ellipse), you cast that variable as the concrete Element class.

el = new RectElement(
m_captureR.Location, m_captureR.Size,
new Pen(Brushes.Black, 2), m_fill);

Now you need to store this element inside the Document. If you remember from the object model, the Document class that you create maintains a reference to the Elements collection. And of course, the PageControl class holds a reference to the Document class. Therefore, from within the PageControl's MouseUp event you have to add the newly created Element instance to the Elements collection as follows:

m_doc.Elements.Add(el);

One requirement for each Element instance is that it can draw itself. Therefore, all that was left to do in the MouseUp event was force the control to repaint. The control's Paint event will then handle drawing any elements stored in the associated Document instance.

To draw these stored elements, look at the private routine inside the PageControl class?DrawElements. The PageControl's Paint event calls DrawElements, passing it a handle to the control's Graphics surface. This routine simply loops through the Elements collection and calls the Draw method of each Element instance (forwarding it the Graphics handle).

The Draw methods for each of the Element classes are all very similar. First create a GraphicsContainer instance with the call:

GraphicsContainer gc = g.BeginContainer();

This call to BeginContainer allows you to cache subsequent calls to the Graphics handle before rendering to the screen.

Next, create a System.Drawing.Rectangle instance and draw it to the Graphics surface, then fill accordingly. These calls are as follows:

Rectangle r =
new Rectangle(this.Position, this.Size);
g.DrawRectangle(
new Pen(new SolidBrush(this.PenColor),
this.PenThickness), r);
g.FillRectangle(
new SolidBrush(this.FillColor), r);

Finally, call the EndContainer method of the Graphics class to indicate that your application can now render the given element to the user's screen:

g.EndContainer(gc);

Selecting and Moving Elements

The next requirement to tackle is to allow a user to select and then drag a graphical element on the page. This will again involve our mouse events (MouseDown, MouseMove, and MouseUp). From an algorithm perspective, the code needs to handle the following sequence of events:

The first requirement is obviously the simplest, just an If statement inside of the MouseDown event (see Listing 1).

As for the second requirement, you need to write code to solve a common problem called “hit-testing.” Hit-testing involves capturing the point of the user's cursor at the time of their click and then determining if that captured point is within the bounds of any other element.

For simplicity sake, in this version, the code only supports single item select (no multi-select) and drag. In addition, elements are hit-tested in the reverse order that they were drawn to the screen (or added to the Elements collection class). This way, if two elements overlapped, the top-most Element would be selected every time. Let's look at the code that handles this.

First, add a routine to the Elements collection class called GetHitElement. This routine takes a Point (testPoint) type that represents the user's click (and our testing point). The routine returns the hit (or user selected) element (or a null in the case of no selection). You call this method from the MouseDown event on the control:

Element el =
m_doc.Elements.GetHitElement(m_sP);

Inside the GetHitElements method you simply loop the collection backwards and call each Element's internal HitTest method as follows:

//search the list backwards
for (int i=this.List.Count; i>0; i--)
{
Element e = (Element)this.List[i-1];
if (e.HitTest(testPoint))
return e;
}

Next you need to make each concrete Element class expose their own HitTest method. This is required because each element's shape can be different (rectangle vs. ellipse, for example). Thankfully, GDI+ makes hit-testing pretty simple. First create a System.Drawing.Drawing2D.GraphicsPath instance:

GraphicsPath gp = new GraphicsPath();

You'll use the GraphicsPath object to contain a version of the given Element. Therefore, you add the element to the graphics path:

gp.AddRectangle(
new Rectangle(this.Position, this.Size));

Next, you check the value of the IsVisible property of the GraphicsPath instance to determine if the user's click point is inside the element:

return gp.IsVisible(testPoint);

If the call to IsVisible returns true, you have a hit. Once you have determined the user's selected object, you need to indicate that visually to the user. To do this you'll add code to the PageControl's MouseDown event. Once again you'll leverage the object model. Now set the hit element to the Document's SelectedElement property, which allows you to add code to the control's OnPaint event that checked to make sure that a SelectedElement exists. If so, the Paint event tells the selected element to draw itself as follows:

if(m_doc.SelectedElement != null)
m_doc.SelectedElement.DrawSelected(g);

Of course, you have to create a DrawSelected method for each concrete Element class. In this case, to represent selection the code will just draw the object's outline in blue since the application does not allow the user to create objects with anything other than black pens.

Finally, to illustrate movement of the selected element about the page, you reset the selected element's Position property to that of the newly moved-to position. Before doing this, you need to set the moved point's offset values based on a difference calculated in the MouseDown event:

mp.Offset(m_xDiff, m_yDiff);
m_doc.SelectedElement.Position = mp;

Saving and Opening a Document

The last requirement of this control's library was that it be able to save a document to disk and re-open it at a later time. To give users this ability, you will add both a Save and Open method to the PageControl. These methods save and open instances of the PageControl's Document class to create and open binary versions of the Document class.

To manage this you will first mark the Document class (and its associated classes) as Serializeable. This allows the .NET Framework to serialize and de-serialize an object into one of multiple formats (in our case binary). You mark the class via an Attribute class as follows:

[Serializable()]
public class Document

Next, create the Save routine. This routine uses BinaryMessageFormatter to serialize the Document class out to a file as follows:

public void Save(string fileName)
{
Formatters.Binary.BinaryFormatter bf =
new Formatters.Binary.BinaryFormatter();

bf.Serialize(
new FileStream(fileName,
FileMode.Create), this.m_doc);
}

To open the serialized class you need to create another binary formatter. This time you call the Deserialize method and cast the results into a new Document instance that gets stored as the PageControl's associated Document instance. The code looks like the following:

Formatters.Binary.BinaryFormatter bf =
new Formatters.Binary.BinaryFormatter();

m_doc = new Document();

m_doc = (Document)bf.Deserialize(
new FileStream(fileName, FileMode.Open));

Surprisingly, you've done everything necessary for saving and opening instances of the custom Document class. Users can save their documents and open them at a later time. Of course, a developer that consumes the PageControl will still have to wire up the actual interaction with the file system.

The Application Container (Main)

You now have a fully functioning, version 1 PageControl. However, to see it in action (and to test it) you need to create an application that consumes the control. Keeping in mind the Visio paradigm, it makes sense to create a container for the application that could host multiple new documents. To handle this requirement you simply create a standard Windows Form and set its IsMdiContainer property to true. An MDI container also offers a very familiar paradigm to users.

You can add a number of controls to this main form to make it more useful. To handle basic navigation, add a menu bar and toolbar. Next, add the open and save common dialog controls. These controls will help you quickly and consistently write the code for interacting with the file system during opening and saving a Document. Finally, let's add the color dialog control to allow users to choose a fill color for drawing elements that they'll draw on a page. Figure 3 shows an example of this MDI container.

Figure 3: The MDI container form.
Figure 3: The MDI container form.

Wiring up these controls resulted in some pretty basic code (although showing the currently selected fill color on the toolbar required a little GDI+ work). This code is included in the sample download.

The Page Host

Now that you have a document hosting environment with an MDI interface, you must create a host for the actual PageControl user control. This host will be another Windows Form, and we'll call it DocumentContext. Its main job is to contain the newly created PageControl and act as a MDI child window to the MDI container. Figure 4 provides a screen shot of the DocumentContext MDI child form and our PageControl class in action.

Figure 4: The DocumentContext MDI child form with an instance of the PageControl user control.
Figure 4: The DocumentContext MDI child form with an instance of the PageControl user control.

This child window has one requirement: maintain the page in its center. Similar to Visio or Word, when a user re-sizes the form (DocumentContext), the page should remain in the center of the form with a standard border on both sides. This provides the application a more polished feel.

At first glance, the standard docking and anchoring properties available in Windows Forms applications appear to provide the necessary mechanism to make this feature happen. After some monkeying around, however, it soon becomes apparent that additional code was in order. Thankfully, the Layout event lends itself handily to the problem. This event gets fired when you need to move on the form, such as a user resizing the form.

The code added to this event represents some straightforward math: Capture the new width of the DocumentContext form and subtract the width of the page control (set based on the Orientation property) and divide by 2 to provide an equal border on both the left and the right. Use the resulting value to set the page control's Left property. You can find the complete code for this event in Listing 5.

Summary

In this article you've created the foundation for a drawing page control (see Figure 5 for a final look). In Part 2 you will learn how to tackle a number of other requirements for the control including adding text blocks, zooming in and out on the page, and scaling elements. Each of these requirements will present themselves with a whole new set of issues.

Figure 5: The application and control in action.
Figure 5: The application and control in action.

Download the complete source code for this article at: www.brilliantstorm.com/resources

Listing 1: MouseDown event

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

//purpose: capture the mouse down event

//indicate we are capturing
m_capture = true;

//get the down-point of the cursor (starting point)
m_sP = new Point(e.X, e.Y);

//release any selected elements
m_doc.SelectedElement = null;
this.Invalidate();

//verify it is time to draw
if (this.Draw == DrawType.Nothing)
{
//test for "hits" (selected elements)
Element el = m_doc.Elements.GetHitElement(m_sP);
if (el != null)
{
//set the selected item
m_doc.SelectedElement = el;

//set offset differences in case of moving the object
m_xDiff = m_doc.SelectedElement.Position.X - m_sP.X;
m_yDiff = m_doc.SelectedElement.Position.Y - m_sP.Y;

//change the cursor
this.Cursor = Cursors.SizeAll;

//force the form to re-draw
this.Invalidate();
}
}
}

Listing 2: MouseMove event

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

//purpose: capture the mouse move event

//determine if we are in capture mode
if (m_capture)
{

//create a point that is the mouse moved point
Point mp = new Point(e.X, e.Y);

//calculate the width of the move
int w = Math.Abs(mp.X - m_sP.X);

//calculate the height of the move
int h = Math.Abs(mp.Y - m_sP.Y);

//verify the start point of the rectanlge
// as the user can move in any direction relative
// to the starting point (mouse down)
if (mp.X > m_sP.X & mp.Y > m_sP.Y)
{
m_captureR = new Rectangle(m_sP.X, m_sP.Y, w, h);
}
else if (mp.X < m_sP.X & mp.Y < m_sP.Y)
{
m_captureR = new Rectangle(mp.X, mp.Y, w, h);
}
else if (mp.X > m_sP.X & mp.Y < m_sP.Y)
{
m_captureR = new Rectangle(m_sP.X, mp.Y, w, h);
}
else if (mp.X < m_sP.X & mp.Y > m_sP.Y)
{
m_captureR = new Rectangle(mp.X, m_sP.Y, w, h);
}

if(m_doc.SelectedElement != null)
{
//we are not drawing, and we have a selected an object
//  reset the position of the selected object
//  using offset values stored at the
//time of mouse-down
mp.Offset(m_xDiff, m_yDiff);
m_doc.SelectedElement.Position = mp;

}

//cause the page control to be repainted
this.Invalidate();

}
}

Listing 3: Paint event

protected override void OnPaint	System.Windows.Forms.PaintEventArgs e)
{

//get reference to graphics object
Graphics g = e.Graphics;

//draw page
this.DrawPageBounds(g);

//re-draw all elements
this.DrawElements(g);

//draw the rectangle if capturing in draw mode
if (m_capture & m_draw != DrawType.Nothing)
{
DrawCaptureR(g);
}
else
{
//draw the selected element if applicable
if(m_doc.SelectedElement != null)
m_doc.SelectedElement.DrawSelected(g);
}
}

Listing 4: MouseUp event

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

//purpose: capture the mouse up event

//indicate to stop capturing
m_capture = false;

if (m_draw != DrawType.Nothing)
{

//define an element var
Element el = null;

if (m_draw == DrawType.Rectangle)
{
//create a new rectangle element
el = new RectElement(m_captureR.Location,
m_captureR.Size,
new Pen(Brushes.Black, 2), m_fill);
}

if (m_draw == DrawType.Ellipse)
{
//create a new ellipse element
el = new EllipseElement(m_captureR.Location,
m_captureR.Size,
new Pen(Brushes.Black, 2), m_fill);
}

//add the element to the local collection
m_doc.Elements.Add(el);

}

if (m_doc.SelectedElement != null)
{
//change the cursor back
this.Cursor = Cursors.Default;
}

//allow the page to re-paint
this.Invalidate();

}

Listing 5: MDI child (DocumentContext) layout event

private void DocumentContext_Layout
(object sender, System.Windows.Forms.LayoutEventArgs e)
{

//purpose: respond to the layout event
//used to center the page control on the host

//calculate the left position of the page
int leftPos = (this.Width - pgControl.Width)/2;

//do not go below frame size
if (leftPos < pgControl.FrameSize)
{leftPos = pgControl.FrameSize;}

//position the control on the form
pgControl.Left = leftPos;
pgControl.Top = 0;

//force redraw of the control
pgControl.Invalidate();

}