|
|
Title | Make a drawing application in VB .NET |
Description | This example shows how to make a drawing application VB .NET. The user can draw lines, rectangles, ellipses, and stars. It also demonstrates ToolBar dropdowns that display images and complex XML serialization. |
Keywords | draw, drawing, Drawable, serialization, XmlSerializer |
Categories | Graphics, VB.NET, Software Engineering |
|
|
This is a very large application so only some key points are described here.
The Drawable class represents a drawing object (line, rectangle, etc.) that can be specified by a bounding rectangle. It cannot handle things such as curves, free-form drawing, and arbitrary polygons. This class defines the following MustOverride routines. Their names and comments should make them self-explanatory.
|
|
' Draw the object on this Graphics surface.
Public MustOverride Sub Draw(ByVal gr As Graphics)
' Return the object's bounding rectangle.
Public MustOverride Function GetBounds() As Rectangle
' Return True if this point is on the object.
Public MustOverride Function IsAt(ByVal x As Integer, ByVal _
y As Integer) As Boolean
' The user is moving one of the object's points.
Public MustOverride Sub NewPoint(ByVal x As Integer, ByVal _
y As Integer)
' Return True if the object is empty (e.g. a zero-length
' line).
Public MustOverride Function IsEmpty() As Boolean
|
|
The DrawableLine, DrawableRectangle, DrawableEllipse, and DrawableStar classes inherit from Drawable. They implement these routines appropriately for their type of drawing. For example, DrawableRectangle's Draw method draws a rectangle and its IsAt function returns True if a specified point lies within the rectangle.
The module Geometry.vb contains routines for working with the class's shapes. It has functions for calculating the distance between two points, or between a point and a line segment. It also has a function that can tell if a point lies inside an ellipse.
When the user presses the mouse down, the program creates a new object based on the currently selected tool.
The MouseMove event handler passes the new object information about the mouse's position by calling its NewPoint method. That method moves the second corner of the object's bounding rectangle.
When the user releases the mouse, the MouseUp event handler stops drawing the new object and, if the object is not empty, adds it to the picture.
|
|
' Perform an action depending on the currently pushed tool.
Private Sub picCanvas_MouseDown(ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs) Handles _
picCanvas.MouseDown
' See which button was pressed.
If e.Button = MouseButtons.Right Then
' Right button. See if we're drawing something.
If m_NewDrawable Is Nothing Then
' We are not drawing. Ignore this button.
Else
' We are drawing something. Cancel it.
m_Picture.Remove(m_NewDrawable)
m_NewDrawable = Nothing
RemoveHandler picCanvas.MouseMove, AddressOf _
NewDrawable_MouseMove
RemoveHandler picCanvas.MouseUp, AddressOf _
NewDrawable_MouseUp
' Redraw to erase the new object.
picCanvas.Invalidate()
End If
Else
' Left button. See which tool is pushed.
Select Case m_SelectedToolButton.ToolTipText
Case "Pointer"
' Select an object.
m_Picture.SelectObjectAt(e.X, e.Y)
Case "Line"
' Start drawing a line.
m_NewDrawable = New _
DrawableLine(m_CurrentForeColor, _
m_CurrentLineWidth, e.X, e.Y, e.X, e.Y)
m_Picture.Add(m_NewDrawable)
AddHandler picCanvas.MouseMove, AddressOf _
NewDrawable_MouseMove
AddHandler picCanvas.MouseUp, AddressOf _
NewDrawable_MouseUp
Case "Rectangle"
' Start drawing a rectangle.
m_NewDrawable = New _
DrawableRectangle(m_CurrentForeColor, _
m_CurrentFillColor, m_CurrentLineWidth, _
e.X, e.Y, e.X, e.Y)
m_Picture.Add(m_NewDrawable)
AddHandler picCanvas.MouseMove, AddressOf _
NewDrawable_MouseMove
AddHandler picCanvas.MouseUp, AddressOf _
NewDrawable_MouseUp
Case "Ellipse"
' Start drawing an ellipse.
m_NewDrawable = New _
DrawableEllipse(m_CurrentForeColor, _
m_CurrentFillColor, m_CurrentLineWidth, _
e.X, e.Y, e.X, e.Y)
m_Picture.Add(m_NewDrawable)
AddHandler picCanvas.MouseMove, AddressOf _
NewDrawable_MouseMove
AddHandler picCanvas.MouseUp, AddressOf _
NewDrawable_MouseUp
Case "Star"
' Start drawing a star.
m_NewDrawable = New _
DrawableStar(m_CurrentForeColor, _
m_CurrentFillColor, m_CurrentLineWidth, _
e.X, e.Y, e.X, e.Y)
m_Picture.Add(m_NewDrawable)
AddHandler picCanvas.MouseMove, AddressOf _
NewDrawable_MouseMove
AddHandler picCanvas.MouseUp, AddressOf _
NewDrawable_MouseUp
End Select
' Redraw.
picCanvas.Invalidate()
End If
End Sub
' On mouse move, continue drawing.
Private Sub NewDrawable_MouseMove(ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs)
' Update the new line's coordinates.
m_NewDrawable.NewPoint(e.X, e.Y)
' Redraw to show the new line.
picCanvas.Invalidate()
End Sub
' On mouse up, finish drawing the new object.
Private Sub NewDrawable_MouseUp(ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs)
' No longer watch for MouseMove or MouseUp.
RemoveHandler picCanvas.MouseMove, AddressOf _
NewDrawable_MouseMove
RemoveHandler picCanvas.MouseUp, AddressOf _
NewDrawable_MouseUp
' See if the new object is empty (e.g. a zero-length
' line).
If m_NewDrawable.IsEmpty() Then
' Discard this object.
m_Picture.Remove(m_NewDrawable)
End If
' We're no longer working with the new object.
m_NewDrawable = Nothing
' Redraw.
picCanvas.Invalidate()
End Sub
|
|
This program also shows how to make a ToolBar dropdown that displays images. The following code shows how this works for the line width dropdown. The form's Load event handler starts the process by calling subroutine MakeLineThicknessImages.
MakeLineThicknessImages saves the current number of images in the imlToolbarButtons ImageList control. If then draws a series of images showing different line thicknesses and adds them to the ImageList.
At design time, I gave the program a context menu containing menu items named mnuThick1, mnuThick2, and so forth. The context menu is associated with the thickness ComboBox-style ToolBar button.
The program calls subroutine PrepareThicknessMenu for each of these thickness menus. This routine adds Click, MeasureItem, and DrawItem event handlers for the menu item. It also sets the item's OwnerDraw property to True so it generates the MeasureItem and DrawItem events.
|
|
' Get ready.
Private Sub Form1_Load(ByVal sender As Object, ByVal e As _
System.EventArgs) Handles MyBase.Load
...
' Make line color images.
MakeLineThicknessImages()
' Set line thickness menu event handlers.
PrepareThicknessMenu(mnuThick1)
PrepareThicknessMenu(mnuThick2)
PrepareThicknessMenu(mnuThick3)
PrepareThicknessMenu(mnuThick4)
PrepareThicknessMenu(mnuThick5)
mnuThick1.PerformClick()
...
End Sub
' Make line thickness images.
Private Sub MakeLineThicknessImages()
Dim bm As New Bitmap(16, 16)
Dim gr As Graphics = Graphics.FromImage(bm)
m_FirstLineThicknessImage = _
imlToolbarButtons.Images.Count
For i As Integer = 1 To 5
gr.Clear(SystemColors.Control)
gr.DrawLine(New Pen(Color.Black, i), 0, 8, 16, 8)
imlToolbarButtons.Images.Add(bm)
Next i
End Sub
' Add Click, MeasureItem, and DrawItem event handlers
' to this MenuItem.
Private Sub PrepareThicknessMenu(ByVal menu_item As _
MenuItem)
AddHandler menu_item.Click, AddressOf mnuLineThick_Click
AddHandler menu_item.MeasureItem, AddressOf _
mnuLineThick_MeasureItem
AddHandler menu_item.DrawItem, AddressOf _
mnuLineThick_DrawItem
menu_item.OwnerDraw = True
End Sub
|
|
The mnuLineThick_Click event handler finds the menu item object that raised the Click event and checks its Text property to get the item's selected thickness. It sets the ComboBox ToolBar button's image to the one with the right thickness. It then sets the thickness for the currently selected object, if there is one.
The mnuLineThick_MeasureItem event handler tells VB thaht the menu item needs a 16x16 pixel area. VB will allocate a space at least this large. In practice, the area will be wider than this.
The mnuLineThick_DrawItem event handler determines whetrher the menu item is currently selected (the mouse is over it) and picks appropriate colors. It erases the menu item's background and then draws a line with the correct thickness.
|
|
' The user has selected a new line thickness.
Private Sub mnuLineThick_Click(ByVal sender As _
System.Object, ByVal e As System.EventArgs)
' Update the current pen.
Dim menu_item As MenuItem = DirectCast(sender, MenuItem)
m_CurrentLineWidth = Integer.Parse(menu_item.Text)
' Update the toolbar display.
tcboThickness.ImageIndex = m_CurrentLineWidth + _
m_FirstLineThicknessImage - 1
' Update the selected object if there is one.
If Not (m_Picture.SelectedDrawable Is Nothing) Then
m_Picture.SelectedDrawable.LineWidth = _
m_CurrentLineWidth
picCanvas.Invalidate()
End If
' Reselect the currently selected tool.
m_SelectedToolButton.Pushed = True
End Sub
' Allow room for the MenuItem.
Private Sub mnuLineThick_MeasureItem(ByVal sender As _
Object, ByVal e As _
System.Windows.Forms.MeasureItemEventArgs)
e.ItemWidth = 16
e.ItemHeight = 16
End Sub
' Draw the menu item.
Private Sub mnuLineThick_DrawItem(ByVal sender As Object, _
ByVal e As System.Windows.Forms.DrawItemEventArgs)
Dim menu_item As MenuItem = DirectCast(sender, MenuItem)
Dim thickness As Integer = Integer.Parse(menu_item.Text)
' See if we're selected.
Dim fg_pen As Pen
Dim bg_brush As Brush
If (e.State And DrawItemState.Selected) = 0 Then
' Not selected.
' Use a light background and dark foreground.
fg_pen = New Pen(SystemColors.MenuText, thickness)
bg_brush = New SolidBrush(SystemColors.Menu)
Else
' Selected.
' Use a dark background and light foreground.
fg_pen = New Pen(SystemColors.HighlightText, _
thickness)
bg_brush = New SolidBrush(SystemColors.Highlight)
End If
' Erase the background.
e.Graphics.FillRectangle(bg_brush, e.Bounds)
' Draw the line.
Dim y As Integer = e.Bounds.Y + e.Bounds.Height \ 2
e.Graphics.DrawLine(fg_pen, e.Bounds.X, y, _
e.Bounds.Right, y)
fg_pen.Dispose()
bg_brush.Dispose()
End Sub
|
|
This program also shows how to save and restore pictures. Each of the drawing classes, including the DrawablePicture class that holds the objects in a picture, has a Serializable attribute. That tells VB to include information that allows an XmlSerializer object to serialize the class.
The classes include other attributes to tell the serializer how to treat certain properties. For instance, the following code shows part of the Drawable class. Attributes indicate that the ForeColor, FillColor, and IsSelected properties should not be serialized. LineWidth, X1, Y1, X2, and Y2 should be serialized as attributes of the Drawable object, not as separate elements inside it in the XML result.
|
|
Imports System.Xml.Serialization
<Serializable()> _
Public MustInherit Class Drawable
' Drawing characteristics.
<XmlIgnore()> Public ForeColor As Color
<XmlIgnore()> Public FillColor As Color
<XmlAttributeAttribute()> Public LineWidth As Integer = _
0
<XmlAttributeAttribute()> Public X1 As Integer
<XmlAttributeAttribute()> Public Y1 As Integer
<XmlAttributeAttribute()> Public X2 As Integer
<XmlAttributeAttribute()> Public Y2 As Integer
' Indicates whether we should draw as selected.
<XmlIgnore()> Public IsSelected As Boolean = False
...
End Class
|
|
There are a couple of tricks here. Some properties are not serialized because they are not needed when you reload a picture. There's no need to remember whether an object was selected when you saved the file.
Some VB and system objects such as Color do not include information for serialization. If you try to serialize one, you get an empty element. To work around this, the Drawable class provides public properties that represent its colors as ARGB values. Those are integers so they are easy to serialize. The following code shows the property procedures for the ForeColor.
|
|
<XmlAttributeAttribute("ForeColor")> _
Public Property ForeColorArgb() As Integer
Get
Return ForeColor.ToArgb()
End Get
Set(ByVal Value As Integer)
ForeColor = Color.FromArgb(Value)
End Set
End Property
|
|
The DrawablePicture class stores the picture's objects in an ArrayList object. To serialize an ArrayList, you need to use the XmlElement attribute to tell VB what types of objects the ArrayList might contain.
|
|
' The list where we will store objects.
<XmlElement(GetType(Drawable)), _
XmlElement(GetType(DrawableLine)), _
XmlElement(GetType(DrawableRectangle)), _
XmlElement(GetType(DrawableEllipse)), _
XmlElement(GetType(DrawableStar))> _
Public Drawables As New ArrayList
|
|
The DrawablePicture class's SavePicture and LoadPicture methods save and load picture serializations. SavePicture makes an XmlSerializer initialized to work with DrawablePicture objects. It makes a StreamWriter attached to the output file and uses the XmlSerializer to serialize the picture into it.
LoadPicture makes an XmlSerializer initialized to work with DrawablePicture objects. It makes a StreamWriter attached to the input file and uses the XmlSerializer to read a DrawablePicture object out of it. It then returns this new object.
|
|
' Save the picture into the file.
Public Sub SavePicture(ByVal file_name As String)
Try
Dim xml_serializer As New _
XmlSerializer(GetType(DrawablePicture))
Dim stream_writer As New StreamWriter(file_name)
xml_serializer.Serialize(stream_writer, Me)
stream_writer.Close()
Catch ex As Exception
If MessageBox.Show(ex.Message & vbCrLf & _
"Show internal error?", "Save Error", _
MessageBoxButtons.YesNo, _
MessageBoxIcon.Question) = DialogResult.Yes _
Then
MessageBox.Show(ex.InnerException.ToString, _
"Internal Error", _
MessageBoxButtons.OK, _
MessageBoxIcon.Exclamation)
End If
End Try
End Sub
' Laod the picture from the file.
Public Shared Function LoadPicture(ByVal file_name As _
String) As DrawablePicture
Try
Dim xml_serializer As New _
XmlSerializer(GetType(DrawablePicture))
Dim file_stream As New FileStream(file_name, _
FileMode.Open)
Dim new_picture As DrawablePicture = _
DirectCast(xml_serializer.Deserialize(file_stream), _
DrawablePicture)
file_stream.Close()
Return new_picture
Catch ex As Exception
If MessageBox.Show(ex.Message & vbCrLf & _
"Show internal error?", "Save Error", _
MessageBoxButtons.YesNo, _
MessageBoxIcon.Question) = DialogResult.Yes _
Then
MessageBox.Show(ex.InnerException.ToString, _
"Internal Error", _
MessageBoxButtons.OK, _
MessageBoxIcon.Exclamation)
End If
Return Nothing
End Try
End Function
|
|
To let the user move objects, the program looks for MouseMove events. If the left button is down, the main form calls the picture's MoveSelectedDrawableToMouse method to move the currently selected Drawable. It then invalidates the picture to redraw it.
|
|
' If we have an object selected, move it.
Private Sub picCanvas_MouseMove(ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs) Handles _
picCanvas.MouseMove
' Only move if the left button is down.
If e.Button = Windows.Forms.MouseButtons.Left Then
' Move it.
m_Picture.MoveSelectedDrawableToMouse(e.X, e.Y)
' Redraw to show the new position.
picCanvas.Invalidate()
End If
End Sub
|
|
The DrawablePicture's MoveSelectedDrawableToMouse method exits if no Drawable is selected.
If an object is selected, the program calculates the distance the mouse has been moved and calls the selected Drawable's MoveRelative method to move it by a corresponding amount. It then saves the new mouse position for future moves.
(Note: The mouse's position is saved in (m_SelectedMouseX, m_SelectedMouseY) when a Drawable is selected so it is ready to move.)
|
|
' Move the selected drawable. The mouse has moved from
' (m_SelectedMouseX, m_SelectedMouseY) to (x, y).
Public Sub MoveSelectedDrawableToMouse(ByVal x As Integer, _
ByVal y As Integer)
' Do nothing if nothing is selected.
If SelectedDrawable Is Nothing Then Exit Sub
' See how far we want it moved.
Dim new_dx As Integer = x - m_SelectedMouseX
Dim new_dy As Integer = y - m_SelectedMouseY
' Move it.
SelectedDrawable.MoveRelative(new_dx, new_dy)
' Save the new mouse position.
m_SelectedMouseX = x
m_SelectedMouseY = y
End Sub
|
|
The program uses a PrintDocument and PrintPreviewDialog to print the drawing and to display a print preview. The most interesting part of this code is the PrintPage event handler that the is called to generate the printout.
The actual printing is done by a simple call to:
m_Picture.Draw(e.Graphics)
The rest of the code scales the picture to fill the printable area and translates it to center the result.
|
|
Private Sub pdPrint_PrintPage(ByVal sender As _
System.Object, ByVal e As _
System.Drawing.Printing.PrintPageEventArgs) Handles _
pdPrint.PrintPage
#Const PRINT_CENTERED = True
#Const PRINT_ENLARGED = True
'#Const PRINT_MARGIN = True
#If PRINT_CENTERED Then ' Center the picture.
' Get the picture's bounds.
Dim bounds As Rectangle = m_Picture.GetBounds()
' Translate the drawing to the origin.
e.Graphics.TranslateTransform(-bounds.X, -bounds.Y)
Dim scale As Single = 1
#If PRINT_ENLARGED Then ' Scale to fit.
Dim xscale As Double = 1
If bounds.Width > 0 Then xscale = _
e.MarginBounds.Width / bounds.Width
Dim yscale As Double = 1
If bounds.Height > 0 Then yscale = _
e.MarginBounds.Height / bounds.Height
If xscale > yscale Then
scale = CSng(yscale)
Else
scale = CSng(xscale)
End If
e.Graphics.ScaleTransform(scale, scale, _
Drawing2D.MatrixOrder.Append)
#End If
' Translate to center the drawing.
Dim cx As Integer = CInt((e.MarginBounds.Width - _
bounds.Width * scale) / 2)
Dim cy As Integer = CInt((e.MarginBounds.Height - _
bounds.Height * scale) / 2)
e.Graphics.TranslateTransform( _
e.MarginBounds.X + cx, _
e.MarginBounds.Y + cy, _
Drawing2D.MatrixOrder.Append)
#End If
' Draw the picture.
m_Picture.Draw(e.Graphics)
#If PRINT_MARGIN Then
' Draw the margin.
e.Graphics.ResetTransform()
Using margin_pen As New Pen(Color.Red)
margin_pen.DashPattern = New Single() {5, 5}
e.Graphics.DrawRectangle(margin_pen, _
e.MarginBounds)
End Using
#End If
End Sub
|
|
This example also shows how to change the stacking order of the objects it draws, and how to delete objects. See the code for additional details.
|
|
|
|
|
|