devxlogo

GDI+ Drawing Page, Part I

GDI+ Drawing Page, Part I

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

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.

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:

  1. Determine when the user clicks the mouse and they are not intending to draw.
  2. Check to see if the user’s click intersects with any of their previously drawn elements.
  3. Visually indicate the selection to the user.
  4. Re-draw the element as the user moves it about the page.
  5. Re-lock the element on the page.

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.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist

©2024 Copyright DevX - All Rights Reserved. Registration or use of this site constitutes acceptance of our Terms of Service and Privacy Policy.