Creating a Design Surface Using Windows Forms and GDI+ in Microsoft .NET
Duncan Mackenzie
Microsoft Developer Network
June 2002
Summary: Techniques used to create a GDI+-based design surface for the user to create and position various graphic elements within a Windows Forms control in Microsoft .NET. Includes downloadable sample code. (20 printed pages)
Applies to:
Microsoft® .NET Framework
Microsoft® Visual Studio® .NET
Windows® XP
Windows® Server 2003
Download DesignSurface.exe.
Contents
In Search of a Sample
Requirements for a Useful Design Surface
Storing the Graphic Elements
The Design Surface
The Test Application
Conclusion
In Search of a Sample
Tasked with creating an article on printing in Windows Forms applications, I followed my usual writing path and started with a sample application to demonstrate a few of the more important printing concepts. I decided early on that I wanted to do a highly graphical sample instead of a standard database-driven reporting scenario, so I started designing an application to make posters and greeting cards. At this point, I began to realize what I was creating was more complex than required for a printing demo, but I pressed on regardless. Soon, my application design was finished and I had identified a key point: The single most critical part of my application was a design surface where the user could create and position various graphic elements. It wasn't long after that point when I realized just creating the design surface would be much more complicated than printing out the finished results. The end result is that this article will be about building the design surface; if you were hoping for an application that could produce greeting cards, you will not find it here. (You could check out the Microsoft Greetings 2002 Web site, though.)
Requirements for a Useful Design Surface
Many applications, such as Microsoft® Visio®, include a design surface, so I felt pretty comfortable with the set of functionality I would need to provide if this design surface was eventually going to be used inside a graphics program. The basic list of features included:
- Support multiple graphical elements such as text, pictures, and shapes.
- Allow movement and rotation of those elements.
- Support drawing to the printer as well as to the screen.
- Allow finished compositions to be saved to disk and loaded back into the design surface.
- Visually represent printer page boundaries on the screen to make it easier to position elements relative to the printed page.
Figure 1. The finished design surface in use
Storing the Graphic Elements
The first step in creating the design surface was to provide an underlying data structure for all the graphic elements—essentially an object model designed to hold images, text, shapes, and other types of objects. Having built many little samples using shapes to illustrate object-oriented (OO) concepts, it didn't take too long to come up with a good object model. I wanted the design surface to be able to treat all its graphic elements the same, regardless of which specific type of element it was, so I designed the object model so that all graphic elements were derived from the same base class.
Figure 2. Graphic object library
The shared base class, GraphicObject, contains all of the properties that are common to all of the possible graphic elements, such as position and size. Each individual subclass is built on that base by adding properties such as Text, Font, and FontColor for the TextGraphic subclass and ImagePath for the LinkedImageGraphic subclass. This underlying object library also includes a strongly-typed GraphicObjectCollection class, which will be used to hold a set of GraphicObject instances. Each individual class in this object model overrides the Draw method of the base class so that it knows how to handle its own drawing; this will allow the final design surface code to just call each element's Draw method, without any knowledge of the specific element type. There is a great deal of code in this little object library, even though it only includes a small number of objects, but the most interesting code is found in each class's implementation of the Draw method. In each case, this is the code that will be used to display the object onto the final design surface; it has to be able to handle drawing the element at a specified location and possibly with a certain degree of rotation.
Rotation
Using GDI+, you do not actually rotate a shape, text or image; instead you rotate the entire coordinate space and then draw the element onto that rotated surface. Take a look at the code from the RectangleGraphic class for an example of this rotation procedure:
Public Overrides Sub Draw(ByVal g As Graphics)
Dim gContainer As Drawing2D.GraphicsContainer
Dim myMatrix As Drawing2D.Matrix
gContainer = g.BeginContainer()
myMatrix = g.Transform()
If m_Rotation <> 0 Then
myMatrix.RotateAt(m_rotation, New PointF(X, Y), _
Drawing2D.MatrixOrder.Append)
g.Transform = myMatrix
End If
Dim rect As New Rectangle(X, Y, Width, Height)
If Me.Fill Then
g.FillRectangle(New SolidBrush(Me.FillColor), rect)
End If
Dim myPen As New Pen(Me.LineColor, Me.LineWidth)
g.DrawRectangle(myPen, rect)
g.EndContainer(gContainer)
End Sub
The RotateAt method allows you to transform a matrix by rotating it around a specific point: the upper-left corner of the graphic element in this system. There is also a Rotate method, but it will rotate your world around the origin, which doesn't work when you just want to affect a single object, as the farther away that object is from the origin, the larger the rotation effect would be.
Graphic Containers
The code in this routine uses the GraphicsContainer class, through the BeginContainer and EndContainer method calls, to isolate the rest of my graphic work from any changes that are made within this code. Without this isolation, the rotation I perform within this code would affect any elements I attempt to draw after this one, which would get fairly confusing.
Using MustInherit and MustOverride to Build an Object Model
When I was developing this set of graphic objects, I used inheritance, overriding, and overloading to make the most useful object model I could. All elements inherit from GraphicObject, which I then marked as MustInherit; intended to be used only as a base class, MustInherit was prevented from being created directly.
Note Specifying MustInherit is equivalent to creating an abstract base class in C++, C#, or Java, if you are more familiar with one of these languages. In these languages, the abstract term is also used on methods, and corresponds to the Microsoft® Visual Basic® .NET MustOverride modifier in this case.
Inside this base class, I also mark a method as MustOverride, which means that my base class will provide no implementation for that method—the implementation must be provided by the derived (child) classes. I use this technique for the Draw method, but I don't mark the HitTest function (discussed later in this article) with the same modifier, because it will be overridden by most, but not all, of my child classes. I want to provide a default implementation of HitTest, but a default implementation of Draw wouldn't be very useful, so I mark Draw as MustOverride and HitTest as Overridable. Continuing down my object model, I created two additional MustInherit classes, ShapeGraphic and ImageGraphic, which allowed me to add a few properties specific to those types of elements without having to duplicate those properties in all three types of ShapeGraphic and the two types of ImageGraphic. By marking ShapeGraphic and ImageGraphic as MustInherit, I gain two benefits: I clearly indicate the purpose of these two classes and I avoid developers accidentally creating an instance of a class that is not intended to be used directly.
Extending The Object Model
Depending on the type of application you are creating, this set of graphic elements might not be enough for you, but you can expand on these classes by creating new elements. Possibilities that occur to me include creating subclasses of the TextGraphic class to handle paragraphs of text for a reporting tool, or creating application-specific elements, such as a Map class or a set of office furniture pieces. There are really no limits; just create a new class that extends GraphicObject and you will have the basic properties of location, size, and rotation provided, at which point you can add any additional properties you wish and implement the Draw method to produce your desired visual representation. Whatever you implement, as long as you base it on GraphicObject, it will function within the design surface just like the elements I have provided.
The Design Surface
Once the underlying object model was completed, and code had been written for the Draw routines in each control, it was time to move onto the actual design surface. I started inheriting from UserControl, which worked fine for this example, but the ScrollableControl or Panel classes would be suitable base classes as well. There was no immediate need to set any existing properties or otherwise adjust the basic behavior of this new control, but I did need to start adding a set of new properties, internal variables, and eventually internal procedures.
Determining the Bounds and Margins
To define the size of my drawing surface, I added two public properties: one called SurfaceBounds to hold a rectangle that indicates the size and position of my drawing surface, and another, called SurfaceMargins, to represent the printing area information. Both of these rectangles are in 1/100ths of an inch, and I default them to the dimensions of a Letter size piece of paper. In my sample application that I discuss later, I set these values to the default settings of my printer at startup. Once I have these bounds and margins set up, I don't actually use them for much. I set up a background graphic and grid (see the Creating the Background Graphics section) to indicate these bounds, but I allow elements to be freely dragged outside of this area (onto the non-printing area, essentially) and I even go ahead and print those objects. Anything outside the indicated bounds shouldn't print, but I don't concern myself with that detail, leaving it up to the printer to ignore anything I print outside its page dimensions. My personal experience with drawing and layout packages has led me to believe that this is a common way of handling the non-printing area, and that being able to have non-printing elements in your project is a useful feature.
Creating the Background Graphics
As just discussed, the design surface has two properties—SurfaceBounds and SurfaceMargins—that define the printing dimensions of your current printer. Using these two values along with a few grid-related settings, I draw a background onto the design surface before any graphic elements, and then overlay the margins on top of the surface after I have drawn all the elements. This is not complicated code (the complete DrawGrid routine is shown below) but I try hard to make it flexible by ensuring that all of the variables (size of the grid, color, line width, etc.) are available as properties (and the entire DrawGrid procedure is Overridable):
Protected Overridable Sub DrawGrid(ByVal g As Graphics)
Dim bounds As Rectangle
Dim horizGridSize As Integer = _
ConvertToHPixels(GridSize / 100) * Me.Zoom
Dim vertGridSize As Integer = _
ConvertToVPixels(GridSize / 100) * Me.Zoom
bounds = ConvertToPixels(SurfaceBounds)
bounds = ZoomRectangle(bounds)
If Me.AutoScrollMinSize.Height <> bounds.Height _
AndAlso Me.AutoScrollMinSize.Width <> bounds.Width Then
Me.AutoScrollMinSize = New Size(bounds.Width, bounds.Height)
End If
g.Clear(m_NonPrintingAreaColor)
g.FillRectangle(New SolidBrush(Me.BackColor), bounds)
Dim gridPen As New Pen(GridColor, m_GridLineWidth)
Dim i As Integer
For i = vertGridSize To bounds.Height Step vertGridSize
g.DrawLine(gridPen, 0, i, bounds.Width, i)
Next
For i = horizGridSize To bounds.Width Step horizGridSize
g.DrawLine(gridPen, i, 0, i, bounds.Height)
Next
End Sub
I handle the margins in a separate procedure so that I can draw the background before the rest of the graphic elements, and then draw the margins as a final step:
Protected Overridable Sub DrawMargins(ByVal g As Graphics)
Dim margins As Rectangle = _
ZoomRectangle(ConvertToPixels(Me.m_SurfaceMargins))
Dim marginPen As New Pen(m_MarginColor)
marginPen.DashStyle = Drawing2D.DashStyle.Dash
marginPen.Width = m_MarginLineWidth
g.DrawRectangle(marginPen, margins)
End Sub
Drawing All the Elements
Earlier I described the underlying object model that stores all my graphic elements, each of which is based on GraphicObject. Included in that underlying set of objects is a strongly-typed collection object, GraphicObjectCollection, and my design surface uses an instance of this type of collection to manage its graphic elements. I have added a DrawObjects method to that collection class that accepts a Graphics object and draws all of the objects it contains:
Public Sub DrawObjects(ByVal g As Graphics, ByVal Scale As Single)
Dim drawObj As GraphicObject
Dim i As Integer
Dim gCon As Drawing2D.GraphicsContainer
Dim myOriginalMatrix As Drawing2D.Matrix
myOriginalMatrix = g.Transform()
gCon = g.BeginContainer
g.PageUnit = GraphicsUnit.Pixel
g.ScaleTransform(Scale, Scale)
If Not Me.InnerList Is Nothing AndAlso Me.InnerList.Count > 0 Then
For i = 0 To Me.InnerList.Count - 1
drawObj = CType(Me.InnerList(i), GraphicObject)
drawObj.Draw(g)
Next
End If
g.EndContainer(gCon)
g.Transform = myOriginalMatrix
End Sub
The controls are drawn based on their positions within the collection, which translates into their z-order on the drawing surface; items at higher indexes of the internal list are drawn on top of items at lower indexes. Each object is cast to an instance of GraphicObject, and then its Draw method is called to execute the drawing code of the appropriate element type. The Scale parameter is used to pass the current zoom level of the design surface, as discussed in the next section.
Zooming
To support zooming in and out of my design surface, I created a Zoom property for my control that takes a Single value, where 1 is equal to 100 percent zoom level, and then I added additional code into the DrawObjects method of my GraphicObjectsCollection. Before I draw any of the graphic elements onto my design surface, I perform a transform on the GDI+ world, similar to what I did for rotating individual objects, but using the Scale method this time:
g.ScaleTransform(Scale, Scale)
Once I've done this transform, I can perform all my other drawing without any knowledge of the current zoom level, and everything will be produced correctly. I wanted to take advantage of my mouse wheel, so I overrode the OnMouseWheel method of the Control class, increasing or decreasing the zoom level accordingly:
Protected Overrides Sub OnMouseWheel(ByVal e As MouseEventArgs)
If e.Delta <> 0 Then 'has wheel been moved?
Dim detents As Integer = Math.Abs(e.Delta \ 120)
Dim i As Integer
Dim zoomIn As Boolean = (e.Delta < 0)
Dim zoomPercentage As Integer = Me.Zoom * 100
For i = 1 To detents
If zoomPercentage <= 100 Then
If zoomIn Then
zoomPercentage = zoomPercentage \ 2
Else
zoomPercentage = zoomPercentage * 2
End If
Else
If zoomIn Then
zoomPercentage = zoomPercentage - 50
Else
zoomPercentage = zoomPercentage + 50
End If
End If
Next
Me.Zoom = zoomPercentage / 100
Me.Invalidate()
End If
End Sub
Later when I discuss using the mouse to select, drag, and rotate objects on the design surface, I will show how the zoom level is taken into account when converting from mouse coordinates to page coordinates.
Scrolling
Learning a new development platform is so much fun; you get to re-do your work on a regular basis. When I first wanted to add scrolling to my design surface, I started by adding a pair of scroll bars to the control, and then I wrote a whole bunch of code. I had to write code to adjust the min/max values of the scroll bars on every change in zoom level or form size, and then more code to make sure I was drawing the design surface correctly based on the position of the scroll bars. A little while after I finally got all that code working, I was showing someone the cool AutoScroll feature of Windows Forms, where I can have more controls on my form than will fit in the visible area and it automatically provides scrolling functionality. You could probably hear the gears turning in my head. It was only a few minutes later that I had checked and confirmed that the UserControl class inherits from the ScrollableControl class just like the Form class does, and therefore has the exact same AutoScroll features! I went back to my code and ripped out all the scrolling code, and the two scroll bars, set AutoScroll to True and added the one line of code I needed to replace all my hard work:
Me.AutoScrollMinSize = New Size(bounds.Width, bounds.Height)
Once the code was added, scrolling worked perfectly, but I still have to take it into account when I draw out my images. When using the AutoScroll feature, I can adjust for the current scrolled position by offsetting my drawing by the X and Y values in Me.AutoScrollPosition:
'handle the possibility that the viewport is scrolled,
'adjust my origin coordintates to compensate
Dim pt As Point = Me.AutoScrollPosition
g.TranslateTransform(pt.X, pt.Y)
Another transform! Transforms, which I have so far used for rotation, scaling, and offsets, are a key concept in GDI+ and are used in most graphics work. If you want to learn more about Transforms and GDI+ in general, I would suggest reading Charles Petzold's book, Programming Microsoft Windows with C#. If you are looking for something a bit quicker and accessible online, you could start with the Introduction to GDI+ or the Coordinate System and Transformations primer, both from the .NET Framework Developer's Guide.
Selecting and Manipulating a Graphic Element
Using only code, I could add elements to this design surface and then edit their properties to change their position or rotation, but what I want is to move or rotate these objects using the mouse. Since adding this type of interaction involves trapping mouse clicks and movement, all the required code will be added into the design surface itself, and then this code will manipulate the properties of the underlying object model.
Selecting Objects
Before you can start manipulating an object, you have to select it. I chose to support two different ways to select an object—either by directly clicking it, or by clicking and dragging a selection rectangle on the design surface and completely covering the desired object. In either case, only a single object can be selected, simply because that was all I decided to support. Adding multiple select would not be complex, but it would add a lot of code changes to support dragging a group of objects, rotating a group of objects, and changing the SelectedObject property into a collection instead of a single GraphicObject variable. Both types of selection (single-click or click-and-drag) require a calculation commonly referred to as a "hit test" to determine which object was hit based on the coordinates clicked, and that calculation is not as easy as it might seem. Since the design surface supports a variable scale (zooming in and out) and scrolling, the coordinates of the design surface control are not the same as the coordinates of the underlying graphic elements. For example, if a rectangle has been added to the design surface at X, Y coordinates of (100, 100), and then the user zooms out to 50 percent, the rectangle will actually start at (50, 50) on the visible form. If they now scroll down a bit, the rectangle might appear to start at (50, 10), but it has never changed from its (100, 100) position internally. To compensate for this problem, I created a procedure with the cryptic name "gscTogoc," which stands for graphic surface coordinates to graphic object coordinates. This procedure converts a specific point from the coordinate space of the design surface to the correct values for the underlying objects:
Private Overloads Function gscTogoc ( _
ByVal gsPT As Point) As Point
Dim myNewPoint As Point
myNewPoint.X = CInt((gsPT.X - Me.AutoScrollPosition.X) / Me.Zoom)
myNewPoint.Y = CInt((gsPT.Y - Me.AutoScrollPosition.Y) / Me.Zoom)
Return myNewPoint
End Function
Using this function, I can convert mouse clicks into the coordinates of the graphic elements and then move on to determine which object corresponds to that location:
From GraphicSurface.vb
Private Sub GraphicsSurface_MouseDown(ByVal sender As Object, _
ByVal e As MouseEventArgs) _
Handles MyBase.MouseDown
Dim mousePT As Point = gscTogoc(e.X, e.Y)
Me.SelectedObject = Me.drawingObjects.FindObjectAtPoint(mousePT)
From GraphicObjects.vb
Public Function FindObjectAtPoint(ByVal pt As Point) _
As GraphicObject
Dim drawObj As GraphicObject
Dim i As Integer
If Not Me.InnerList Is Nothing AndAlso _
Me.InnerList.Count > 0 Then
For i = Me.InnerList.Count - 1 To 0 Step -1
drawObj = CType(Me.InnerList(i), GraphicObject)
If drawObj.HitTest(pt) Then
Return drawObj
Exit For
End If
Next
End If
Return Nothing
End Function
For the direct-click test, which I perform in the OnMouseDown event of the design surface, once I have the coordinates in the correct space, I loop through all of the GraphicObjects in my GraphicObjectsCollection collection (using the FindObjectAtPoint function of that collection class) and call each object's HitTest function. This function, which essentially just checks if a certain point is within the bounds of the object, can be overridden within each type of graphic element to handle more complex shapes:
Public Overridable Overloads _
Function HitTest(ByVal pt As Point) As Boolean
Dim gp As New Drawing2D.GraphicsPath()
Dim myMatrix As New Drawing2D.Matrix()
gp.AddRectangle(New Rectangle(X, Y, Width, Height))
If Me.Rotation <> 0 Then
myMatrix.RotateAt(Me.Rotation, New PointF(Me.X, Me.Y), _
Drawing2D.MatrixOrder.Append)
End If
gp.Transform(myMatrix)
Return gp.IsVisible(pt)
End Function
Even the default implementation of the function, in GraphicObject, is a bit more complex than you might expect, as it has to take into account the rotation of the object before checking for a hit. Overall, this design surface is as complex as it is because every action has to take into account scroll bars, scaling, and rotation.
The second type of object selection, using a selection rectangle, works in a similar fashion. For this type of selection, though, an overload of HitTest is used that takes a rectangle instead of a point:
Public Overridable Overloads _
Function HitTest(ByVal rect As Rectangle) As Boolean
'is this object contained within the supplied rectangle
Dim gp As New Drawing2D.GraphicsPath()
Dim myMatrix As New Drawing2D.Matrix()
gp.AddRectangle(New Rectangle(X, Y, Width, Height))
If Me.Rotation <> 0 Then
myMatrix.RotateAt(Me.Rotation, New PointF(Me.X, Me.Y), _
Drawing.Drawing2D.MatrixOrder.Append)
End If
gp.Transform(myMatrix)
Dim gpRect As Rectangle = Rectangle.Round(gp.GetBounds)
Return rect.Contains(gpRect)
End Function
This version of HitTest is only slightly different, since it is checking for containment of the element within the passed rectangle instead of checking for containment of a point within the element's bounds.
Moving Objects
Dragging an object around on the design surface is implemented through a combination of three event handlers: MouseDown, MouseMove, and MouseUp. In MouseDown, when the user presses the mouse button down on the object, I determine the offset between the mouse location and the upper-left corner of the element:
Private dragOffset As Point
Private Sub GraphicsSurface_MouseDown(ByVal sender As Object, _
ByVal e As MouseEventArgs) _
Handles MyBase.MouseDown
Dim mousePT As Point = gscTogoc(e.X, e.Y)
Me.SelectedObject = Me.drawingObjects.FindObjectAtPoint(mousePT)
Me.Invalidate()
If Not m_SelectedObject Is Nothing Then
dragging = True
dragOffset.X = m_SelectedObject.X - mousePT.X
dragOffset.Y = m_SelectedObject.Y - mousePT.Y
End If
End Sub
Then, in the MouseMove, I change the location of the element as the user moves the mouse, always including that original offset between the position of the element and the mouse pointer's location:
Private Sub GraphicsSurface_MouseMove(ByVal sender As Object, _
ByVal e As MouseEventArgs) Handles MyBase.MouseMove
Dim dragPoint As Point = gscTogoc(e.X, e.Y)
If Not m_SelectedObject Is Nothing Then
If dragging Then
dragPoint.Offset(dragOffset.X, dragOffset.Y)
m_SelectedObject.SetPosition(dragPoint)
Me.Invalidate()
End If
End If
End Sub
Finally, in the MouseUp, I set dragging = false to stop the drag now that the mouse button has been released:
Private Sub GraphicsSurface_MouseUp(ByVal sender As Object, _
ByVal e As MouseEventArgs) Handles MyBase.MouseUp
dragging = False
End Sub
Rotating Objects
To rotate an object, I use the same three event handlers as for dragging an object—MouseDown, MouseMove, and MouseUp—and I use the right mouse button as the indicator that the user wishes to rotate instead of dragging the object. As the mouse is moved, I calculate the angle between the mouse coordinates and the upper-left corner of my object, subtract the angle at which the mouse down occurred, and set the rotation of the selected object accordingly (see Figure 3).
Figure 3. Finding the angle between two points is a common operation in graphics work.
To come up with the angle between two points, the mouse position and the upper-left corner of the selected object, I had to revisit my high school math and try to remember the formula for figuring out an angle given the dimensions of the sides. Well, luckily for me, I still remembered it, and when combined with the Atan function in the .NET Framework, I was able to produce a small function that calculates the angle between any two points on a surface:
Private Function AngleToPoint(ByVal Origin As Point, _
ByVal Target As Point) As Single
'a cool little utility function,
'given two points finds the angle between them....
'forced me to recall my highschool math,
'but the task is made easier by a special overload to
'Atan that takes X,Y co-ordinates.
Dim Angle As Single
Target.X = Target.X - Origin.X
Target.Y = Target.Y - Origin.Y
Angle = Math.Atan2(Target.Y, Target.X) / (Math.PI / 180)
Return Angle
End Function
Using this function, the code for the MouseDown, MouseMove, and MouseUp events was relatively easy to write:
Private Sub GraphicsSurface_MouseDown(ByVal sender As Object, _
ByVal e As MouseEventArgs) _
Handles MyBase.MouseDown
Dim mousePT As Point = gscTogoc(e.X, e.Y)
Me.SelectedObject = _
Me.drawingObjects.FindObjectAtPoint(mousePT)
Me.Invalidate()
If Not m_SelectedObject Is Nothing Then
If e.Button And MouseButtons.Right Then
rotating = True
startingRotation = _
AngleToPoint(m_SelectedObject.GetPosition, mousePT)
originalRotation = m_SelectedObject.Rotation
Else
'dragging
End If
End If 'no selected object
End Sub
Private Sub GraphicsSurface_MouseMove(ByVal sender As Object, _
ByVal e As MouseEventArgs) Handles MyBase.MouseMove
Dim dragPoint As Point = gscTogoc(e.X, e.Y)
If Not m_SelectedObject Is Nothing Then
If rotating Then
Dim currentRotation As Single
currentRotation = _
AngleToPoint(m_SelectedObject.GetPosition, dragPoint)
currentRotation = _
CInt((currentRotation - _
startingRotation + _
originalRotation) Mod 360)
m_SelectedObject.Rotation = currentRotation
Me.Invalidate()
End If
End If
End Sub
Private Sub GraphicsSurface_MouseUp(ByVal sender As Object, _
ByVal e As MouseEventArgs) Handles MyBase.MouseUp
rotating = False
End Sub
I limited my rotation to whole degrees, by placing CInt() around my angle calculation in MouseMove, but if you removed the CInt() you could rotate to any value with the precision of a single data type. Personally I found that without restricting the rotation to integers, it is very difficult to rotate an object to a specific degree value, such as 45 or 90.
Printing
Windows Forms Printing is based on GDI+, so all of the code that is being used to draw graphic elements onto my design surface will work just fine to print out those same elements. However, when printing, I don't want any of my grid lines, margins, or other background graphics at all, so I created a special method on my GraphicObjectCollection class: PrintObjects. This method is similar to DrawObjects, which is called from the OnPaint code of the design surface, but it doesn't have to worry about scaling (zoom level), scroll bar position, the currently selected object, or drawing a grid:
Public Sub PrintObjects(ByVal g As Graphics)
Dim drawObj As GraphicObject
Dim i As Integer
g.PageUnit = GraphicsUnit.Pixel
If Not Me.InnerList Is Nothing _
AndAlso Me.InnerList.Count > 0 Then
For i = 0 To Me.InnerList.Count - 1
drawObj = CType(Me.InnerList(i), _
GraphicObject)
drawObj.Draw(g)
Next
End If
End Sub
Saving and Loading All Your Elements
If users are going to set up a bunch of graphic elements, positioning and rotating them until getting it just right, users will need a way to save their work. To support saving and loading all the current elements, I marked all the element classes as serializable and added code to the design surface to save the entire object collection to a file:
Public Overridable Sub SaveToFile(ByVal fileName As String)
'save objects to a binary file
Try
Dim settingsPath As String = fileName
Dim myBinarySerializer As New Formatters.Binary.BinaryFormatter()
myBinarySerializer.Serialize( _
New IO.FileStream(settingsPath, IO.FileMode.Create), _
Me.drawingObjects)
Catch e As Exception
Throw New ApplicationException("Save Failed.", e)
End Try
End Sub
Loading a file back into the design surface isn't any more complex:
Public Overridable Sub LoadFromFile(ByVal fileName As String)
'load objects from a binary file
Try
Dim settingsPath As String = fileName
Dim myBinarySerializer _
As New Formatters.Binary.BinaryFormatter()
m_drawingObjects = _
myBinarySerializer.Deserialize( _
New IO.FileStream(settingsPath, IO.FileMode.Open))
Me.SelectedObject = Nothing
Me.Invalidate()
Catch e As Exception
Throw New ApplicationException("File failed to load.", e)
End Try
End Sub
The Test Application
Included with the download for this article is a sample application that allows you to test the design surface by adding various graphical elements, saving and loading sets of elements, and even printing out your creations. This application is just for testing the design surface and has no real functionality of its own. Instructions for using the test application are provided through a Readme.htm file that has been included in the application's project.
From a developer's point of view, the code in this test application demonstrates a few key items, including adding new images, lines, and other graphics to the element collection of the design surface.
Conclusion
The combination of Windows Forms with its time-saver features such as AutoScroll, and GDI+, allows you to create feature-rich graphics applications in a short amount of time. The design surface detailed in this article is not a complete solution if you need to build a drawing or layout solution, but it provides a set of basic functionality and has been designed to be usable for a variety of applications.