|
You will start the project by defining the project specification. In plain English, it is a Windows application that can do the following:
Draw two types of rectangular structures and four types of lines. A line always connects two rectangular structures and cannot stand alone.
Write labels on the rectangular structure and attach labels on the line.
Resize and move each rectangular structure around the draw area.
Automatically adjust the line position when either of its rectangular structure is resized or moved.
Allow the user to manually move a line around its rectangular structure.
The two rectangular structures available are the class structure and the interface structure. Figure 4-9 shows the class structure, and Figure 4-10 shows the interface structure.
Figure 4-9: The class structure
Figure 4-10: The interface structure
As shown in Figure 4-9, a class structure has three partitions. The top-most partition is for the class name. The one in the middle is for the class's list of attributes, and the bottom one is for the list of operations. Figure 4-11 illustrates a class with a name (Rectangle), four attributes (left, top, height, and width) and two operations (Draw() and Move()).
Figure 4-11: A class with a name, attributes, and operations
An interface structure is similar to a class structure, but it only has two partitions. The top partition is for the interface name, which always follows the word <Interface>. The bottom partition is for the interface's list of operations. Figure 4-12 shows an interface called IButtonControl with two operations: NotifyDefault and PerformClick.
Figure 4-12: An interface with a name and operations
Two structures can have a relationship between them. A line represents a relationship. Four types of relationships are provided using one of the lines in Figure 4-13.
Figure 4-13: Types of relationships
As an example, Figure 4-14 shows a generalization relationship between the class Rectangle and the class Shape. In the figure, Rectangle is a child class of Shape.
Figure 4-14: A generalization relationship
Note | A line always has two structures at its ends. It cannot stand alone. If one of the structures it connects is deleted, the line will be automatically removed, too. |
By default, a line connecting two structures will move automatically when one of the structures is moved. The position of the line will depend on the relative position between the two structures it connects. Figure 4-15 shows the line positions in four relative positions of the two structures.
Figure 4-15: Automatic adjustments of the line positions
Automatic adjustment of a line's position that is caused by moving one of its structures always places the line at the center of one of the structure's edges. The user is allowed to adjust the line manually by dragging the line. Figure 4-16 shows a line that has been moved manually.
Figure 4-16: Manually positioning a line
A line can have zero to three labels. You can place each label on the left, center, and the right of the line. Figure 4-17 shows a line with labels.
Figure 4-17: Line with labels
Finally, you can select a structure and a line to manipulate individually. For example, to add a name to a class structure, you must first select that class. A selected structure or line will be the active object and have handles. Figure 4-18 shows a selected class, and Figure 4-19 shows a selected line.
Figure 4-18: A selected class structure
Figure 4-19: A selected line
The UML class diagram editor project is a Windows application. Figure 4-20 shows the class diagram for this application.
Figure 4-20: The class diagram
The main form has a number of menu items, a toolbar, a status bar, and a draw area. The draw area is represented by the DrawArea class, which derives from the System.Windows.Forms.Panel class. The draw area fills the entire client area of the main form.
The class structure is represented by the ClassRect class, and the interface structure is represented by the InterfaceRect class. Both ClassRect and InterfaceRect are direct child classes of the Rect class. The Line class represents a line.
The Rect class derives from the System.Windows.Forms.UserControl class and can draw itself. Every rectangular shape is added as a child control of the DrawArea object.
The Line class, on the other hand, does not inherit from any Windows control class. Therefore, it does not have a drawing surface to draw itself. As a result, it must use the Graphics object of the DrawArea control to draw itself. Every line drawn is added to an ArrayList of the DrawArea object.
Both rectangular shapes and lines have an internal state called selected that indicates whether the shape is being selected. In addition, each shape has the index field that stores a number that is unique to that shape. This unique number is passed to the constructor of both the Rect class and the Line class.
You can persist the shapes drawn into a file and later retrieve them from the same file.
The types used in this application are as follows:
The ShapeType enumeration
The RectHandle enumeration
The LineHandle enumeration
The RectPart enumeration
The States class
MathUtil
Rect
ClassRect
InterfaceRect
ClassRectMemento
InterfaceRectMemento
Line
LineMemento
InputBox
PropertyForm
DrawArea
Form1
The following sections discuss each type, from the simplest classes to the more complex.
The ShapeType enumeration provides a way to enumerate the shapes that can be drawn plus None. Listing 4-13 gives the ShapeType enumeration, which you can find in the Misc.vb file in the project directory.
Listing 4-13: The ShapeType Enumeration
Imports System.Windows.Forms Public Enum ShapeType As Integer [Class] [Interface] Generalization Dependency Association Aggregation None End Enum
When selected, a class structure or an interface structure has four handles. Each handle is denoted by one of the members of the RectHandle enumeration. Listing 4-14 gives the RectHandle enumeration, which you can find in the Misc.vb file in the project directory.
Listing 4-14: The RectHandle Enumeration
Public Enum RectHandle As Integer TopLeft TopRight BottomLeft BottomRight None End Enum
Figure 4-21 shows a selected class structure.
Figure 4-21: A selected class structure
When selected, a line has two handles. LineHandle.FromHandle denotes the handle on the start point. LineHandle.ToHandle denotes the handler on the end point. Listing 4-15 shows the LineHandle enumeration, which you can find in the Misc.vb file in the project directory.
Listing 4-15: The LineHandle Enumeration
Public Enum LineHandle As Integer FromHandle ToHandle None End Enum
When manipulating the parts of a class or interface, you need to refer to that part by using the RectPart enumeration. Listing 4-16 shows RectPart, which you can find in the Misc.vb file in the project directory.
Listing 4-16: The RectPart Enumeration
Public Enum RectPart As Integer Name Attributes Operations End Enum
A class has three parts: name, attributes, and operations. An interface has two parts: name and operations.
The States class stores two values that are shared by other classes in the application. Listing 4-17 shows this class, which you can find in the Misc.vb file in the project directory.
Listing 4-17: The States Class
Public Class States Public Shared ShapeDrawn As ShapeType = ShapeType.None Public Shared RectPart As RectPart End Class
The first value is ShapeDrawn of type ShapeType. Its value can be one of the members of the ShapeType enumeration. It tells the application what shape the user is drawing. The user can choose which shape to be drawn by clicking one of the toolbar buttons representing those shapes. For example, if a user clicks the toolbar button representing the class structure, the event handler of that click event changes the value of States.ShapeDrawn. The DrawArea object then uses this value to respond to the user mouse click and drag.
The second value is RectPart of type RectPart. It tells which part of a class structure or an interface structure is being manipulated. The user can edit the label in those parts.
The MathUtil class contains three methods, all of which are static. The first two methods are public static methods: GetRectangleFromPoints and GetSquareDistance. The third method is a private overload of GetSquareDistance. The private static method GetSquareDistance is used by the public GetSquareDistance method. Listing 4-18 shows the MathUtil class. Each method is explained after the code.
Listing 4-18: The MathUtil Class
Option Explicit On Option Strict On Imports System Imports System.Drawing Public Class MathUtil Public Shared Function GetSquareDistance(ByVal p1 As Point, _ ByVal p2 As Point, ByVal p3 As Point) As Double ' calculate the distance between P3 and the line passing P1 and P2 Dim aSquare, bSquare, cSquare, d, eSquare As Double aSquare = GetSquareDistance(p1, p3) bSquare = GetSquareDistance(p2, p3) cSquare = GetSquareDistance(p1, p2) ' c should not be zero, otherwise it means p1 = p2 ' if it is so, return the distance of p3 and p1 If cSquare = 0 Then Return Math.Sqrt(aSquare) End If d = (bSquare - aSquare + cSquare) / (2 * Math.Sqrt(cSquare)) eSquare = bSquare - d ^ 2 Return Math.Abs(eSquare) End Function Private Shared Function GetSquareDistance(ByVal p1 As Point, _ ByVal p2 As Point) As Double Return ((p2.X - p1.X) ^ 2 + (p2.Y - p1.Y) ^ 2) End Function Public Shared Function GetRectangleFromPoints(ByVal p1 As Point, _ ByVal p2 As Point) As Rectangle Dim x1, x2, y1, y2 As Integer If p1.X < p2.X Then x1 = p1.X x2 = p2.X Else x1 = p2.X x2 = p1.X End If If p1.Y < p2.Y Then y1 = p1.Y y2 = p2.Y Else y1 = p2.Y y2 = p1.Y End If ' x2 > x1 and y2 > y1 Return New Rectangle(x1, y1, x2 - x1, y2 - y1) End Function End Class
The GetRectangleFromPoints method is the same method explained in the section "Creating a Simple Drawing Application". It returns a System.Drawing.Rectangle object given two Point objects.
The public GetSquareDistance method obtains the square distance between a point (p1) and a line passing two points (p2 and p3). The private GetSquareDistance returns the square distance between two points. Before delving into these two methods, however, let's have a look at some math. Those who are allergic to mathematics should not worry because there is only one simple mathematical formula: Pythagoras's Theorem. It says that in a right-angled triangle, the area of the square on the hypotenuse is the sum of the areas of the squares on the other two sides (see Figure 4-22).
Figure 4-22: Pythagoras's Theorem
Note | Pythagoras's Theorem is c2 = a2 + b2. |
The following shows how you can use Pythagoras's Theorem to find the distance between a point and a line if you know the coordinate of the point and the two points passed by the line.
The distance between point P3 and the line is the closest distance to the line at the tangent to the line that passes through P3. In Figure 4-23, the distance between P3 and the line passing P1 and P2 is e.
Figure 4-23: The distance between a point and a line
Applying Pythagoras's Theorem, you get the illustration in Figure 4-24.
Figure 4-24: Using Pythagoras's Theorem to find the distance between a point and a line
Using Pythagoras's Theorem on the two triangles in Figure 4-24, you get the following:
Equating (i) and (ii), you get the following:
From (ii) you know the following:
e2 = b2 - d2
Therefore, the distance is as follows:
where d is known from (iii).
Because you know the coordinates of the start point and the end point of a line in the class diagram, you know the distance between the two points P1 (x1, y1) and P2 (x2, y2) in Figure 4-24 is as follows:
Sqrt((x1 - x2) 2 + (y1 - y2) 2)
Therefore, you know c in (iii) previously. You also know a and b by using the same formulae to obtain the distance between P1 and P3 and the distance between P2 and P3. Therefore, you know a and b in (iii).
You use the private GetSquareDistance method to obtain the square distance between two points:
Private Shared Function GetSquareDistance(ByVal p1 As Point, _ ByVal p2 As Point) As Double Return ((p2.X - p1.X) ^ 2 + (p2.Y - p1.Y) ^ 2) End Function
Using (iii) and (iv), you can get the square distance of point P3 and the line passing P1 and P2 and calculate it using the public GetSquareDistance method in the MathUtil class. Both methods return the square of the distance to avoid rounding. In fact, knowing the square distance is as useful as knowing the distance itself.
The shared public GetSquareDistance method determines which line is closest to a click point on the draw area. It is called by the GetNearestLine method in the DrawArea class to determine which line is closest to a click point.
The Rect class is an abstract class that represents a rectangular area and derives from the System.Windows.Forms.UserControl class. It has two child classes: ClassRect and InterfaceRect. ClassRect represents the class structure, and the InterfaceRect represents the interface structure.
Listing 4.19 shows the Rect class.
Listing 4-19: The Rect Class
Option Explicit On Option Strict On Imports System Imports System.Windows.Forms Imports System.Drawing Imports System.Collections Imports Microsoft.VisualBasic Public MustInherit Class Rect : Inherits UserControl Public StartPoint As Point Public EndPoint As Point Public selected As Boolean = True Private handleColor As Color = Color.Red Public Const handleWidth As Integer = 6 Public minimumWidth As Integer = 50 Public minimumHeight As Integer = 100 Protected foregroundPen As New Pen(Color.Black) Private handleBrush As New SolidBrush(handleColor) Protected textBrush As New SolidBrush(Color.Black) Public Shared Shadows fontHeight As Integer = 12 Public Shared textTopMargin As Integer = 5 Public index As Integer Protected yPos As Integer Protected xPos As Integer = 5 Public operations As New ArrayList() Public Sub New(ByVal startPoint As Point, ByVal endPoint As Point, _ ByVal index As Integer) Me.StartPoint = startPoint Me.EndPoint = endPoint Me.index = index Dim r As Rectangle = MathUtil.GetRectangleFromPoints(startPoint, endPoint) Dim currentWidth As Integer = _ CInt(IIf(r.Width > minimumWidth, r.Width, minimumWidth)) Dim currentHeight As Integer = _ CInt(IIf(r.Height > minimumHeight, r.Height, minimumHeight)) Me.SetBounds(r.X, r.Y, currentWidth, currentHeight) Font = New Font("Times New Roman", 10) End Sub Public Sub Delete() ' call Dispose to remove me from parent's Controls collection Me.Dispose() Me.DestroyHandle() 'trigger the HandleDestroyed event End Sub Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) Dim g As Graphics = e.Graphics If selected Then g.DrawRectangle(foregroundPen, _ CInt(handleWidth / 2), CInt(handleWidth / 2), Me.Width - handleWidth, _ Me.Height - handleWidth) DrawHandles(g) Else g.DrawRectangle(foregroundPen, _ 0, 0, Me.Width - 1, Me.Height - 1) End If DrawName(g) 'draw the line that partition the name part and the next part yPos += fontHeight If selected Then Dim x1 As Integer = CInt(handleWidth / 2) Dim x2 As Integer = x1 + Me.Width - handleWidth g.DrawLine(foregroundPen, x1, yPos, x2, yPos) Else Dim x1 As Integer = 0 Dim x2 As Integer = x1 + Me.Width g.DrawLine(foregroundPen, x1, yPos, x2, yPos) End If DrawMembers(g) End Sub Private Sub DrawHandles(ByRef g As Graphics) 'draw handles g.FillRectangle(handleBrush, _ 0, 0, handleWidth, handleWidth) g.FillRectangle(handleBrush, _ Me.Width - handleWidth, 0, handleWidth, handleWidth) g.FillRectangle(handleBrush, _ 0, Me.Height - handleWidth, handleWidth, handleWidth) g.FillRectangle(handleBrush, _ Me.Width - handleWidth, Me.Height - handleWidth, _ handleWidth, handleWidth) End Sub Protected MustOverride Sub DrawName(ByRef g As Graphics) Protected MustOverride Sub DrawMembers(ByRef g As Graphics) Public Function OnWhichHandle(ByVal p As Point) As RectHandle If Not selected Then Return RectHandle.None End If Dim x As Integer = p.X Dim y As Integer = p.Y If x <= handleWidth And y <= handleWidth Then Return RectHandle.TopLeft ElseIf x >= Me.Width - handleWidth And y <= handleWidth Then Return RectHandle.TopRight ElseIf x <= handleWidth And y >= Me.Height - handleWidth Then Return RectHandle.BottomLeft ElseIf x >= Me.Width - handleWidth And y >= Me.Height - handleWidth Then Return RectHandle.BottomRight Else Return RectHandle.None End If End Function Public Sub SetTop(ByVal top As Integer) If top >= 0 Then Me.Top = top End If StartPoint.Y = Me.Top EndPoint.Y = StartPoint.Y + Me.Height End Sub Public Sub SetLeft(ByVal left As Integer) If left >= 0 Then Me.Left = left End If StartPoint.X = Me.Left EndPoint.X = StartPoint.X + Me.Width End Sub Public Sub SetWidth(ByVal width As Integer) If width > minimumWidth Then Me.Width = width Else Me.Width = minimumWidth End If EndPoint.X = StartPoint.X + Me.Width End Sub Public Sub SetHeight(ByVal height As Integer) If height > minimumHeight Then Me.Height = height Else Me.Height = minimumHeight End If EndPoint.Y = StartPoint.Y + Me.Height End Sub End Class
The user draws a class structure or an interface structure by clicking the mouse on the draw area and dragging the mouse and then releasing the mouse. Therefore, there are two important Point objects when drawing: the Point object representing the click point on the draw area and the Point object representing the point on which the mouse click is released. The first point is called the start point, and the second point is called the end point.
The Rect class's child class constructor will invoke the Rect class's constructor, passing the start point and the end point as well as an integer that will become a unique identifier for the constructed Rect object. The index is generated in the DrawArea object and is a sequential number.
The startPoint, endPoint, and index arguments are assigned to the class-level variables:
Me.StartPoint = startPoint Me.EndPoint = endPoint Me.index = index
Again, because you cannot predict how users drag the mouse when drawing, you use the GetRectangleFromPoints method of the MathUtil class to obtain a Rectangle object from the start point and the end point:
Dim r As Rectangle = MathUtil.GetRectangleFromPoints(startPoint, endPoint)
You impose a minimum width and a minimum height for a Rect object. If the Rectangle object's width is narrower than the minimum width, the minimum width is used. If the Rectangle object's height is shorter than the minimum height, the minimum height is used. You get two values: currentWidth and currentHeight. For example:
Dim currentWidth As Integer = _ CInt(IIf(r.Width > minimumWidth, r.Width, minimumWidth)) Dim currentHeight As Integer = _ CInt(IIf(r.Height > minimumHeight, r.Height, minimumHeight))
Next, set the bounds for the Rect object using the Rectangle object and currentWidth and currentHeight:
Me.SetBounds(r.X, r.Y, currentWidth, currentHeight)
Next, construct a Font object used for drawing the labels in the Rect object:
Font = New Font("Times New Roman", 10)
You draw the rectangular area by overriding the OnPaint method. First you draw the rectangle around the Rect object and then around the name (by calling the DrawName method) and the members (by calling the DrawMembers method). To draw, you need to first obtain the Graphics object from the PaintEventArgs argument:
Dim g As Graphics = e.Graphics
The rectangle will depend on whether the Rect object is selected—in other words, it depends on the value of the selected field. If it is not selected, it draws the rectangle that has the same dimension as the Rect object:
g.DrawRectangle(foregroundPen, _ 0, 0, Me.Width - 1, Me.Height - 1)
If it is selected, the rectangle is slightly smaller because you need to draw the four handles:
g.DrawRectangle(foregroundPen, _ CInt(handleWidth / 2), CInt(handleWidth / 2), Me.Width - handleWidth, _ Me.Height - handleWidth) DrawHandles(g)
Afterward, you need to draw the name by calling the DrawName method:
DrawName(g)
Then, you need to draw the partition line that separates the name and the next part (the attributes if the Rect object is a class structure, and the operation if it is an interface structure). Note that you keep the y position in the yPos variable:
yPos += fontHeight
Again, the line drawn will depend on whether the object is being selected:
If selected Then Dim x1 As Integer = CInt(handleWidth / 2) Dim x2 As Integer = x1 + Me.Width - handleWidth g.DrawLine(foregroundPen, x1, yPos, x2, yPos) Else Dim x1 As Integer = 0 Dim x2 As Integer = x1 + Me.Width g.DrawLine(foregroundPen, x1, yPos, x2, yPos) End If
At the end, you call the DrawMembers method to draw the members:
DrawMembers(g)
Both the DrawName and the DrawMembers methods are abstract methods and must be overridden by the child class.
When a Rect object is moved, its Left and Top property values must also change. When the Rect object is resized, its Width and Height property values must also update. Rather than accessing these properties directly, you use the SetTop, SetLeft, SetWidth, and SetHeight methods so that you can restrict as well as modify some other values.
For instance, when the user tries to move the Rect object up further than the top edge of the DrawArea, the value of the top argument will be negative and the SetTop method will not allow it to happen because it checks the top argument:
Public Sub SetTop(ByVal top As Integer) If top >= 0 Then Me.Top = top End If StartPoint.Y = Me.Top EndPoint.Y = StartPoint.Y + Me.Height End Sub
Also, when the user tries to move the Rect object past the left edge of the DrawArea, the left argument value of the SetLeft method will be negative and it will not allow it to happen:
Public Sub SetLeft(ByVal left As Integer) If left >= 0 Then Me.Left = left End If StartPoint.X = Me.Left EndPoint.X = StartPoint.X + Me.Width End Sub
The SetWidth and SetHeight method restrict the smallest width and height a Rect object can get:
Public Sub SetWidth(ByVal width As Integer) If width > minimumWidth Then Me.Width = width Else Me.Width = minimumWidth End If EndPoint.X = StartPoint.X + Me.Width End Sub Public Sub SetHeight(ByVal height As Integer) If height > minimumHeight Then Me.Height = height Else Me.Height = minimumHeight End If EndPoint.Y = StartPoint.Y + Me.Height End Sub
When the Rect object is selected, the user can resize it by dragging one of the four handles. The first thing to check is whether the mouse is over any handle and, if it is, which handle. The OnWhichHandle method returns a member of the RectHandle enumeration. If the mouse is over the Rect object but not over one of the handles, or if the Rect object is not selected, the method returns RectHandle.None.
The Delete method is called when the DrawArea object receives a Delete input key when the Rect object is selected. The Delete method calls two methods:
Me.Dispose() Me.DestroyHandle() 'trigger the HandleDestroyed event
The Dispose method removes it from the Controls collection of the DrawArea method. The DestroyHandle method is called so that the HandleDestroyed event triggers. The HandleDestroyed event will be caught by any Line object connecting this Rect object to another Rect object. When the Line object receives this event, the Line object knows that one of its structures is deleted and therefore will remove itself.
The ClassRect class represents a class structure. It derives from the Rect class. Listing 4-20 shows the ClassRect class.
Listing 4-20: The ClassRect Class
Option Explicit On Option Strict On Imports System Imports System.Windows.Forms Imports System.Drawing Imports System.Collections Imports Microsoft.VisualBasic Public Class ClassRect : Inherits Rect Public attributes As New ArrayList() Public Sub New(ByVal startPoint As Point, ByVal endPoint As Point, _ ByVal index As Integer) MyBase.new(startPoint, endPoint, index) End Sub Public Sub New(ByVal memento As ClassRectMemento) MyBase.New(memento.StartPoint, memento.EndPoint, memento.index) Me.Name = memento.name Me.attributes = memento.attributes Me.operations = memento.operations Me.selected = memento.selected End Sub Public ReadOnly Property AttributeCount() As Integer Get Return attributes.Count End Get End Property Protected Overrides Sub DrawName(ByRef g As Graphics) yPos = textTopMargin ' center text Dim x As Integer = _ CInt((Me.Width - g.MeasureString(Me.Name, Font).Width) / 2) If x > xpos Then g.DrawString(Name, Font, textBrush, x, yPos) Else g.DrawString(Name, Font, textBrush, xPos, yPos) End If yPos += fontHeight End Sub Protected Overrides Sub DrawMembers(ByRef g As Graphics) 'draw attributes here Dim attrEnum As IEnumerator = attributes.GetEnumerator While attrEnum.MoveNext Dim attribute As String = CType(attrEnum.Current, String) g.DrawString(attribute, Font, textBrush, xpos, ypos) yPos += fontHeight End While 'draw the line that partition the attributes and operations yPos += fontHeight If selected Then Dim x1 As Integer = CInt(handleWidth / 2) Dim x2 As Integer = x1 + Me.Width - handleWidth g.DrawLine(foregroundPen, x1, yPos, x2, yPos) Else Dim x1 As Integer = 0 Dim x2 As Integer = x1 + Me.Width g.DrawLine(foregroundPen, x1, yPos, x2, yPos) End If 'draw operations Dim opsEnum As IEnumerator = operations.GetEnumerator While opsEnum.MoveNext Dim operation As String = CType(opsEnum.Current, String) g.DrawString(operation, Font, textBrush, xpos, ypos) yPos += fontHeight End While End Sub Public Function GetMemento() As ClassRectMemento Dim memento As New ClassRectMemento() memento.index = Me.index memento.name = Me.Name memento.attributes = Me.attributes memento.operations = Me.operations memento.StartPoint = Me.StartPoint memento.EndPoint = Me.EndPoint memento.selected = Me.selected Return memento End Function End Class
The ClassRect class has two constructors. The first constructor is as follows:
Public Sub New(ByVal startPoint As Point, ByVal endPoint As Point, _ ByVal index As Integer) MyBase.new(startPoint, endPoint, index) End Sub
This constructor is called when the user has just drawn a class structure. This constructor simply calls the constructor in the base class (the Rect class).
The second constructor is called when the ClassRect object needs to be restored from the ClassRectMemento object. It accepts a ClassRectMemento object. This constructor calls the base class's constructor and restores any needed states from the RectMemento object:
MyBase.New(memento.StartPoint, memento.EndPoint, memento.index) Me.Name = memento.name Me.attributes = memento.attributes Me.operations = memento.operations Me.selected = memento.selected
The GetMemento method returns a ClassRectMemento object for object serialization.
The DrawName method draws the name for this class structure.
The DrawMembers method draws the list of attributes and operations in this class structure.
The ClassRectMemento class is the memento for the ClassRect object (see Listing 4-21). Note that it is marked with the <Serializable> attribute so that it can be serialized.
Listing 4-21: The ClassRectMemento Class
Imports System Imports System.Drawing Imports System.Collections <Serializable()> Public Class ClassRectMemento Public index As Integer Public name As String Public StartPoint As Point Public EndPoint As Point Public selected As Boolean = True Public operations As ArrayList Public attributes As ArrayList End Class
The InterfaceRect class represents a UML interface structure. It derives from the Rect class (see Listing 4-22).
Listing 4-22: The InterfaceRect Class
Option Explicit On Option Strict On Imports System Imports System.Windows.Forms Imports System.Drawing Imports System.Collections Imports Microsoft.VisualBasic Public Class InterfaceRect : Inherits Rect Public Sub New(ByVal startPoint As Point, ByVal endPoint As Point, _ ByVal index As Integer) MyBase.New(startPoint, endPoint, index) End Sub Public Sub New(ByVal memento As InterfaceRectMemento) MyBase.New(memento.StartPoint, memento.EndPoint, memento.index) Me.Name = memento.name Me.operations = memento.operations Me.selected = memento.selected End Sub Protected Overrides Sub DrawName(ByRef g As Graphics) yPos = textTopMargin 'draw prefix here Dim s As String = "<<Interface>>" Dim x As Integer = _ CInt((Me.Width - g.MeasureString(s, Font).Width) / 2) If x > xpos Then g.DrawString(s, Font, textBrush, x, yPos) Else g.DrawString(s, Font, textBrush, xPos, yPos) End If yPos += fontHeight 'draw name ' center text x = CInt((Me.Width - g.MeasureString(Me.Name, Font).Width) / 2) If x > xpos Then g.DrawString(Name, Font, textBrush, x, yPos) Else g.DrawString(Name, Font, textBrush, xPos, yPos) End If yPos += fontHeight End Sub Protected Overrides Sub DrawMembers(ByRef g As Graphics) 'draw operations here 'draw operations Dim opsEnum As IEnumerator = operations.GetEnumerator While opsEnum.MoveNext Dim operation As String = CType(opsEnum.Current, String) g.DrawString(operation, Font, textBrush, xpos, ypos) yPos += fontHeight End While End Sub Public Function GetMemento() As InterfaceRectMemento Dim memento As New InterfaceRectMemento() memento.index = Me.index memento.name = Me.Name memento.operations = Me.operations memento.StartPoint = Me.StartPoint memento.EndPoint = Me.EndPoint memento.selected = Me.selected Return memento End Function End Class
The InterfaceRect class has two constructors. The first constructor is as follows:
Public Sub New(ByVal startPoint As Point, ByVal endPoint As Point, _ ByVal index As Integer) MyBase.new(startPoint, endPoint, index) End Sub
This constructor is called when the user has just drawn an interface structure. This constructor just calls the constructor in the base class (the Rect class).
The second constructor is called when the InterfaceRect object needs to be restored from the InterfaceRectMemento object. It accepts an InterfaceRectMemento object. This constructor calls the base class's constructor and restores any needed states from the RectMemento object:
MyBase.New(memento.StartPoint, memento.EndPoint, memento.index) Me.Name = memento.name Me.operations = memento.operations Me.selected = memento.selected
The GetMemento method returns an InterfaceRectMemento object for object serialization.
The DrawName method draws the name for this interface structure.
The DrawMembers method draws the list of operations in this interface structure.
The InterfaceRectMemento class represents the memento for the InterfaceRect object (see Listing 4-23). Note that this class is marked with the <Serializable> attribute to make it serializable.
Listing 4-23: The InterfaceRectMemento Class
Imports System Imports System.Drawing Imports System.Collections <Serializable()> Public Class InterfaceRectMemento Public index As Integer Public name As String Public StartPoint As Point Public EndPoint As Point Public selected As Boolean = True Public operations As ArrayList End Class
An InputBox object edits a class structure's name, list of attributes, and list of operations and edits a UML interface structure's name and list of operations. The InputBox object is represented by the InputBox class. The InputBox class derives from the System.Windows.Forms.TextBox class and is instantiated when the DrawArea control is constructed. The InputBox object is then added to the Controls collection of the DrawArea object. It is the first child control of the DrawArea object.
By default, the InputBox object is not visible. It is visible when the user rightclicks a class structure or an interface structure. When visible, the InputBox object is positioned on top of the clicked structure to give the impression that the InputBox is actually part of the structure. The InputBox object will become invisible again when it loses focus.
When it is shown, the InputBox object will receive a Rect object so that it will have access to the Rect object whose part is being edited. Listing 4-24 shows the InputBox class.
Listing 4-24: The InputBox Class
Option Explicit On Option Strict On Imports System Imports System.Windows.Forms Imports System.Collections Public Class InputBox : Inherits TextBox 'the interface or class that is being updated Public rect As Rect Public Property LineArrayList() As ArrayList Get Dim str() As String = Me.Lines Dim al As New ArrayList() Dim count As Integer = str.Length Dim i As Integer For i = 0 To count - 1 al.Add(str(i)) Next Return al End Get Set(ByVal al As ArrayList) Dim itemCount As Integer = al.Count If itemCount = 0 Then Me.Lines = Nothing Else Dim str(itemCount - 1) As String Dim i As Integer For i = 0 To itemCount - 1 str(i) = CType(al.Item(i), String) Next Me.Lines = str End If End Set End Property End Class
The InputBox class adds the LineArrayList property to the TextBox class. This property enables an ArrayList to populate the Lines property. Assigning an ArrayList object to the InputBox object will iterate the ArrayList and create an array of string that will then be fed to the Lines property:
Dim itemCount As Integer = al.Count If itemCount = 0 Then Me.Lines = Nothing Else Dim str(itemCount - 1) As String Dim i As Integer For i = 0 To itemCount - 1 str(i) = CType(al.Item(i), String) Next Me.Lines = str End If
This property returns an ArrayList object containing each element in the Lines array of strings:
Dim str() As String = Me.Lines Dim al As New ArrayList() Dim count As Integer = str.Length Dim i As Integer For i = 0 To count - 1 al.Add(str(i)) Next Return al
The Line class represents a relationship line. There are four types of lines in this application, and they differ only in the graphical appearance—in other words, the end cap and the line style.
There are two possible ways of implementing it. The first is to create a base Line class with four child classes, each representing a line type. The base Line class will have a virtual method for drawing itself and will depend on polymorphism to draw the correct type of line. This solution is elegant, but the number of extra classes adds to the maintenance burden.
The second way is to have a variable that holds the type of the line and then use a Select Case block to draw the correct type of line. This project uses the latter because the only difference among the four types of lines is the type of Pen object to draw those lines.
A line always has two structures at its ends. When one of its structures is moved, the line will follow. In other words, the line will adjust its own position based on the positions of its structures. When first created, the adjustment is automatic, meaning that the line can connect to a structure at any of the structure's sides.
The user can drag a line manually, causing the mode of adjustment to change to manual. In this case, the line will only connect to the structure at that particular side.
The autoMove variable in the Line class indicates a line's adjustment mode.
You can see the Line class in the Line.vb file in the project's directory. To save space, the Line class source code is not printed here.
The Line class has two constructors. The first constructor is used when the user draws a Line object. Its signature is as follows:
Public Sub New(ByVal fromRect As Rect, ByVal toRect As Rect, _ ByVal index As Integer, ByVal lineType As ShapeType)
Note that it accepts two Rect objects, fromRect and toRect. fromRect is the Rect object from which the line is drawn. toRect is the Rect object to which the line is drawn. It also accepts the index number and the line type. The line type is of type ShapeType enumeration.
This constructor first assigns the arguments to the class variables:
Me.lineType = lineType Me.fromRect = fromRect Me.toRect = toRect Me.index = index
It then wires the LocationChanged, HandleDestroyed, and Resize events of both Rect objects.
AddHandler fromRect.LocationChanged, AddressOf rect_LocationChanged AddHandler toRect.LocationChanged, AddressOf rect_LocationChanged AddHandler fromRect.HandleDestroyed, AddressOf rect_HandleDestroyed AddHandler toRect.HandleDestroyed, AddressOf rect_HandleDestroyed AddHandler fromRect.Resize, AddressOf rect_Resize AddHandler toRect.Resize, AddressOf rect_Resize
When one of its Rect objects is moved or resized, the Line object needs to readjust its position. Therefore, both the rect_LocationChanged and the rect_Resize event handlers call the ReadjustPosition method.
Next, the constructor constructs a Pen object used for drawing itself and readjusts its location by calling the ConstructPen and ReadjustLocation methods:
ConstructPen() ReadjustLocation()
The second constructor reconstructs the Line object from a LineMemento object. Its signature is as follows:
Public Sub New(ByVal fromRect As Rect, ByVal toRect As Rect, _ ByVal memento As LineMemento)
It calls the first constructor and then populates the Line object with the states from the LineMemento object.
This method constructs a Pen object based on the value of the lineType variable. The ConstructPen method is only called once—in other words, when the Line object is instantiated. The method contains a Select Case block such as the following:
Select Case Me.lineType Case ShapeType.Generalization ... Case ShapeType.Dependency ... Case ShapeType.Association ... Case ShapeType.Aggregation ... End Select
The ReadjustLocation method changes the value of the Line object's startPoint and endPoint. There are two types of adjustments: automatic and manual, which are determined by the value of autoMove. When adjustment is automatic, the ReadjustLocation method takes into account the relative positions of fromRect and toRect.
When adjustment is manual, the Line object "remembers" which side of fromRect and toRect it connects to and at which points on those sides. The Line class uses a private enumeration whichSide that defines the four sides of a Rect object:
Private Enum whichSide As Integer Top = 0 Bottom = 1 Left = 2 Right = 3 End Enum
As in the automatic adjustment, the manual adjustment aims at updating the value of the Line object's start point and end point of both fromRect and toRect. This is the skeleton of the part that deals with the manual adjustment:
'adjust the StartPoint Select Case fromRectWhichSide Case whichSide.Bottom ... Case whichSide.Top ... Case whichSide.Left ... Case whichSide.Right ... End Select 'adjust the EndPoint Select Case toRectWhichSide Case whichSide.Bottom ... Case whichSide.Top ... Case whichSide.Left ... Case whichSide.Right ... End Select
The Draw method draws the following.
The line connecting the start point and the end point
The three labels at the left, center, and right side of the line
The handles when the Line object is selected
Note that when the line type is ShapeType.Aggregation or ShapeType.Generalization, the Draw method needs to do the following extra work:
If lineType = ShapeType.Aggregation Or _ lineType = ShapeType.Generalization Then 'create a new GraphicsPath so the following transform won't affect the 'arrow head Dim gp As GraphicsPath = CType(arrowHeadGraphicPath.Clone, GraphicsPath) Dim rotAngle As Single = GetRotationAngle() g.TranslateTransform(EndPoint.X, EndPoint.Y) g.RotateTransform(rotAngle + 270) g.FillPath(Brushes.White, gp) 'FillPath will erase some of the graphics path 'redraw the arrow head g.DrawPath(Pens.Black, gp) g.ResetTransform() End If
To understand why it needs to do the extra work, look at the part near the end points of an aggregation line and a generalization line in Figure 4-25 if the Draw method does not do this.
Figure 4-25: Imperfect aggregation line and generalization line
See how the lines go through the arrow heads? The "extra work" patches the arrow heads with a GraphicsPath object filled with the background color (in this case, white):
g.FillPath(Brushes.White, gp)
Because the patching can override some points of the arrow head, the arrow head needs to be redrawn with the foreground color (in this case, black):
g.DrawPath(Pens.Black, gp)
The TranslateTransform method of the Graphics class is called to move the origin to the end point:
g.TranslateTransform(EndPoint.X, EndPoint.Y)
The RotateTransform method rotates the GraphicsPath object to the correct angle. The correct rotation angle is returned by the GetRotationAngle method.
Finally, you reset the transformation by calling the ResetTransform method:
g.ResetTransform()
The GetMemento method returns a LineMemento object containing the states of the Line that need to be persisted.
The Line class raises the Removed event when one of its structures is removed. The DrawArea control captures this event.
The LineMemento class represents a memento for a Line object (see Listing 4-25). Note that this class is marked with the <Serializable> attribute to make the LineMemento object serializable.
Listing 4-25: The LineMemento Class
Imports System Imports System.Drawing <Serializable()> Public Class LineMemento Public index As Integer Public lineType As ShapeType Public fromRectIndex As Integer Public toRectIndex As Integer Public StartPoint As Point Public EndPoint As Point Public selected As Boolean Public leftText As String Public centerText As String Public rightText As String Public autoMove As Boolean Public fromRectXRelPos As Integer Public fromRectYRelPos As Integer Public toRectXRelPos As Integer Public toRectYRelPos As Integer Public fromRectWhichSide As Integer Public toRectWhichSide As Integer End Class
A form edits the labels attached to a line. This form is called the property form and is represented by the PropertyForm class. You can find the PropertyForm class in the PropertyForm.vb file in the project's directory; it is not printed here to save space.
Note | The user can display the property form by pressing the F4 key or by clicking Property Form from the View menu. |
Figure 4-26 shows the property form.
Figure 4-26: The property form
The PropertyForm class includes the PropertyChanged event, which is of type PropertyEventHandler. The PropertyEventHandler delegate is defined as follows:
Public Delegate Sub PropertyEventHandler(ByVal sender As Object, _ ByVal e As EventArgs)
The PropertyChanged event triggers every time a property is updated.
The DrawArea class represents the draw area of the application. It derives from the System.Windows.Forms.Panel class. You can find the DrawArea class in the DrawArea.vb file in the project's directory. It is not printed here to save space.
The form is represented by the Form1 class, which is derived from the System.Windows.Forms.Form class. You can find it in the Form1.vb file in the project's directory. It is not printed here to save space.
You can find all the classes used in the application in the UMLClassDiagram.vb file. To compile this application, follow these steps:
Create a working directory.
Copy all the .vb files to the working directory.
Run the build.bat file to build the project.
The content of the build.bat file is as follows:
vbc /t:library /r:System.dll Misc.vb vbc /t:library /r:System.dll,System.Drawing.dll,mscorlib.dll MathUtil.vb vbc /t:library /r:System.dll,System.Drawing.dll ClassRectMemento.vb vbc /t:library /r:System.dll,System.Drawing.dll InterfaceRectMemento.vb vbc /t:library /r:System.dll,System.Drawing.dll, System.Windows.Forms.dll,Misc.dll,MathUtil.dll Rect.vb vbc /t:library /r:System.dll,System.Drawing.dll,System.Windows.Forms.dll, Misc.dll,MathUtil.dll,Rect.dll,ClassRectMemento.dll ClassRect.vb vbc /t:library /r:System.dll,System.Drawing.dll,System.Windows.Forms.dll, Misc.dll,MathUtil.dll,Rect.dll,InterfaceRectMemento.dll InterfaceRect.vb vbc /t:library /r:System.dll,System.Drawing.dll,Misc.dll LineMemento.vb vbc /t:library /r:System.dll,System.Drawing.dll,System.Windows.Forms.dll, Misc.dll,MathUtil.dll,Rect.dll,LineMemento.dll Line.vb vbc /t:library /r:System.dll,System.Drawing.dll,System.Windows.Forms.dll, Line.dll PropertyForm.vb vbc /t:library /r:System.dll,System.Drawing.dll,System.Windows.Forms.dll, Rect.dll InputBox.vb vbc /t:library /r:System.dll,System.Drawing.dll,System.Windows.Forms.dll, Misc.dll,MathUtil.dll,Rect.dll,ClassRect.dll,InterfaceRect.dll, ClassRectMemento.dll,InterfaceRectMemento.dll,Line.dll, LineMemento.dll,InputBox.dll DrawArea.vb vbc /t:winexe /r:System.dll,System.Drawing.dll,System.Windows.Forms.dll, Misc.dll,MathUtil.dll,Rect.dll,ClassRect.dll,InterfaceRect.dll, ClassRectMemento.dll,InterfaceRectMemento.dll,Line.dll, LineMemento.dll,InputBox.dll,DrawArea.dll,PropertyForm.dll Form1.vb
To run the application, type form1.exe in the command prompt. Note that the images directory must be in the directory where the form1.exe file resides. You should see the screenshot as displayed in Figure 4-27.
Figure 4-27: The UML class diagram editor
|