Click here to Skip to main content
15,888,610 members
Articles / Desktop Programming / Windows Forms
Article

Rubberband Lines

Rate me:
Please Sign up or sign in to vote.
4.74/5 (17 votes)
2 Nov 20053 min read 95.2K   1.3K   41   24
Using the rubberband line as a ruler.

Sample Image - Rubberbands.jpg

Introduction

This article explains the process I went through to create a small class used to create a "Rubberband" style ruler that can be used in any project. It uses a technique in GDI+ that replaces the ControlPaint.DrawReversibleLine which has many issues which I won't go into here. Please note that this code was written using VS2005 but there is no reason it wouldn't work in earlier versions, just make sure you change the Using commands.

Background

I was trying to create an application that required I draw a floor plan, and this lead to an issue of how to display to the user the length of the wall to be drawn. I initially started with the well known and documented .NET method DrawReversibleLine in the ControlPaint class. But this lead to many issues, some of which were world coordinate correlation, painting issues (XOR drawing is flawed in many ways), color choices (there are none), and others, but they were the main ones. So I decided to write my own class.

Using the code

I started by keeping the reversible line code in MouseDown, MouseMove and MouseUp events, this can be found in many articles about drawing reversible lines, so I won't go into the specifics. In brief though, MouseDown stores the start point, MouseMove does the hard work in setting the current point and invalidating the control surface so that it will repaint, and then finally the MouseUp event fires the event to inform listeners that the two points have been defined.

There are a couple of tricks in this implementation of rubberband lines that step outside of the norm, and they are what I'd like to share.

The first is the addition of a text label in the middle of the line (rotating with the line as it moves) which displays the line's length. The second is the way in which the class only draws the sections of the control that it has changed.

Text Label (rotating text)

This was the hardest thing to get working but a few features in the GDI+ library made the task easier. The first thing to overcome was the length itself, but basic Trigonometry answered that:

VB
' length^2 = width^2 + height^2

length = Math.Sqrt(rect.Width ^ 2 + rect.Height ^ 2)

Now all I had to do was to put that value onto the line rotated to follow the line, and to do that I needed the angle to rotate the text by. Again, Trig came to the rescue, giving me the following answer:

VB
angle = Math.Atan((p1.Y - p2.Y) / (p1.X - p2.X)) * (180 / Math.PI)

Now having both the LineLength and the Angle functions, I put together the following section which I added to the Paint event (if the mouse was still pressed). The matrix is used to perform the rotation around the midpoint of the line, so that when the ellipse and text are drawn, they are actually drawn on a rotated graphics device. The string format is just used to ensure the text is centered on the line, this worked much better than trying to draw the text within the rectangle used to draw the ellipse. The purpose of the ellipse is to ensure that the text can always be easily read regardless of the background.

VB
Using mx As New System.Drawing.Drawing2D.Matrix
    mx.Translate(midPoint.X, midPoint.Y)
    mx.Rotate(Angle(_origin, _last))
    e.Graphics.Transform = mx
    Using sf As New StringFormat()
        Dim ls As String = CInt(LineLength(_origin, _last))
        Dim l As SizeF = e.Graphics.MeasureString(ls, _
                         _parent.Font, _parent.ClientSize, sf)
        sf.LineAlignment = StringAlignment.Center
        sf.Alignment = StringAlignment.Center

        Dim rt As New Rectangle(0, 0, l.Width, l.Height)
        rt.Inflate(3, 3)
        rt.Offset(-(l.Width / 2), -(l.Height / 2))
        Using backBrush As New SolidBrush(_backColor)
            e.Graphics.FillEllipse(backBrush, rt)
        End Using
        Using foreBrush As New SolidBrush(_foreColor)
            e.Graphics.DrawString(ls, _parent.Font, foreBrush, 0, 0, sf)
        End Using
    End Using
End Using

Using the Class

To actually use the class is as easy as adding it to your project, then instantiating the class as needed. The following code uses the class in a form. (In the Dispose method, make sure to dispose off the class.) Note the styles that are set on the form to prevent flickering.

VB
Public Class Form1
    Private WithEvents _ruler As MouseRuler

    Public Sub New()

        ' This call is required by the Windows Form Designer.
        InitializeComponent()

        ' Add any initialization after the InitializeComponent() call.
        Me.SetStyle(ControlStyles.OptimizedDoubleBuffer Or _
                    ControlStyles.AllPaintingInWmPaint, True)

        _ruler = New MouseRuler(Form1)
    End Sub

    Private Sub _ruler_CaptureFinished(ByVal sender As Object, _
                ByVal e As CaptureEventArgs) Handles _ruler.CaptureFinished
        'todo:  Add wall between points in CaptureEventArgs
    End Sub
End Class

History

Changes made to date:

  • None.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Software Developer (Senior)
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionexcellent! Pin
Southmountain25-May-22 15:30
Southmountain25-May-22 15:30 
QuestionPicturebox control Pin
Member 1327605623-Jun-17 9:53
Member 1327605623-Jun-17 9:53 
GeneralMy vote of 5 Pin
Manoj Kumar Choubey26-Feb-12 21:51
professionalManoj Kumar Choubey26-Feb-12 21:51 
Generalfloorplan Pin
Ashar Shah9-Nov-09 4:00
Ashar Shah9-Nov-09 4:00 
GeneralArgumentOutOfRangeException Pin
Darchangel11-Aug-09 4:50
Darchangel11-Aug-09 4:50 
GeneralLines Label Pin
cds toecutter4-Jun-09 15:57
cds toecutter4-Jun-09 15:57 
GeneralC# Rubberband Lines Pin
carlmalden3-Nov-08 7:35
carlmalden3-Nov-08 7:35 
GeneralRe: C# Rubberband Lines Pin
carlmalden3-Nov-08 8:07
carlmalden3-Nov-08 8:07 
GeneralRe: C# Rubberband Lines Pin
Southmountain30-May-22 7:17
Southmountain30-May-22 7:17 
GeneralThank you!!! Pin
BobLeavell6-Aug-08 16:40
BobLeavell6-Aug-08 16:40 
QuestionSave the Lines Pin
cds toecutter2-Aug-08 0:58
cds toecutter2-Aug-08 0:58 
AnswerRe: Save the Lines Pin
BobLeavell6-Aug-08 16:43
BobLeavell6-Aug-08 16:43 
QuestionEquation Pin
cutenush20-Nov-07 20:01
cutenush20-Nov-07 20:01 
QuestionHow does it do that? Pin
RobinLetMeLogin19-Jan-07 2:21
RobinLetMeLogin19-Jan-07 2:21 
AnswerRe: How does it do that? Pin
Ilíon15-Jun-08 7:20
Ilíon15-Jun-08 7:20 
GeneralRe: How does it do that? Pin
AllanNielsen15-Jun-08 20:48
AllanNielsen15-Jun-08 20:48 
GeneralRe: How does it do that? Pin
Ilíon15-Jun-08 23:52
Ilíon15-Jun-08 23:52 
GeneralRe: How does it do that? Pin
miliu30-May-09 16:40
miliu30-May-09 16:40 
Generalgetting started Pin
cgriff1-Feb-06 20:01
cgriff1-Feb-06 20:01 
GeneralRe: getting started Pin
AllanNielsen1-Feb-06 23:10
AllanNielsen1-Feb-06 23:10 
AnswerRe: getting started Pin
Michael Thornberry2-May-06 6:25
Michael Thornberry2-May-06 6:25 
Here's a VB 2003 Version. One thing to note, VB2003 has no ControlStyles.OptimizedDoubleBuffer so you'll need to use ControlStyles.DoubleBuffer.

-------------------------------------------
Imports System.ComponentModel

Public Delegate Sub CaptureFishedEventHandler(ByVal sender As Object, ByVal e As CaptureEventArgs)

Public Class CaptureEventArgs
Inherits EventArgs

Private ReadOnly _startPoint As Point
Private ReadOnly _endPoint As Point

Public Sub New(ByVal startPoint As Point, ByVal endPoint As Point)
_startPoint = startPoint
_endPoint = endPoint
End Sub

Public ReadOnly Property StartPoint() As Point
Get
Return _startPoint
End Get
End Property

Public ReadOnly Property EndPoint() As Point
Get
Return _endPoint
End Get
End Property
End Class

''' <summary>
''' A utility class which can be used as a ruler.
''' </summary>
Public Class MouseRuler
Implements IDisposable

Private _mouseCaptured As Boolean
Private _angle As Single
Private _length As Single
Private _origin As Point
Private _last As Point

Private _parent As Control
Private _disposed As Boolean = False
Private _foreColor As Color
Private _backColor As Color
Private _lineWidth As Integer = 5
Private _compArray As Single() = New Single() {0.0, 0.16, 0.33, 0.66, 0.83, 1.0}

Public Event CaptureFinished As CaptureFishedEventHandler

''' <summary>
''' Constructor for the MouseRuler
''' </summary>
''' <param name="parent">A control that contains the mouse capture.</param>
''' <exception cref="ArgumentNullException">Thrown if parent is Null.</exception>
Public Sub New(ByVal parent As Control)
If parent Is Nothing Then
Throw New ArgumentNullException("parent", "MouseCapture must be associated with a control.")
End If

_parent = parent
_foreColor = _parent.ForeColor
_backColor = _parent.BackColor

AddHandler _parent.Paint, AddressOf Me.Painting
AddHandler _parent.MouseDown, AddressOf Me.MouseDown
AddHandler _parent.MouseMove, AddressOf Me.MouseMove
AddHandler _parent.MouseUp, AddressOf Me.MouseUp
End Sub

''' <summary>
''' The backcolor used when drawing the line.
''' </summary>
''' <value>A Color value.</value>
''' <remarks>Defaults to the parent control's backcolor.</remarks>
Public Property Backcolor() As Color
Get
Return _backColor
End Get
Set(ByVal value As Color)
_backColor = value
End Set
End Property

''' <summary>
''' The forecolor used when drawing the line.
''' </summary>
''' <value>A <see cref="System.Drawing.Color"/> value</value>
''' <remarks>Defaults to the parent control's backcolor.</remarks>
Public Property ForeColor() As Color
Get
Return _foreColor
End Get
Set(ByVal value As Color)
_foreColor = value
End Set
End Property

''' <summary>
''' The width of the pen used to draw the line.
''' </summary>
''' <value>An integer value.</value>
''' <remarks>Defaults to 1.</remarks>
Public Property LineWidth() As Integer
Get
Return _lineWidth
End Get
Set(ByVal value As Integer)
If _lineWidth < 1 Then
Throw New ArgumentOutOfRangeException("LineWidth", value, "Line width must greater than or equal to one.")
End If
_lineWidth = value
End Set
End Property

''' <summary>
''' Gets or sets an array values that specify a compound pen. A compound pen draws a compound line made up of parallel lines and spaces.
''' </summary>
''' <value>An array of single values.</value>
''' <remarks>A compound line is made up of alternating parallel lines and spaces of varying widths.
''' The values in the array specify the starting points of each component of the compound line
''' relative to the pen's width. The first value in the array specifies where the first component
''' (a line) begins as a fraction of the distance across the width of the pen. The second value in the
''' array specifies the beginning of the next component (a space) as a fraction of the distance across
''' the width of the pen. The final value in the array specifies where the last component ends.
''' Suppose you want a pen to draw two parallel lines where the width of the first line is 20 percent of
''' the pen's width, the width of the space that separates the two lines is 50 percent of the pen' s width,
''' and the width of the second line is 30 percent of the pen's width. Start by creating a Pen object and
''' an array of real numbers.
''' Set the compound array by passing the array with the values 0.0, 0.2, 0.7, and 1.0 to this property.
''' </remarks>
Public Property LineCompoundArray() As Single()
Get
Return _compArray
End Get
Set(ByVal value As Single())
For Each i As Single In value
If i < 0 OrElse i > 1 Then
Throw New ArgumentOutOfRangeException("LineCompoundArray", i, "All elements in the compound array must be >=0 or <=1.")
End If
Next
_compArray = value
End Set
End Property

Private Sub Painting(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs)
If _mouseCaptured Then
e.Graphics.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias
Dim wallpen As New Pen(_foreColor, _lineWidth)
If Not (_compArray Is Nothing) AndAlso _compArray.Length > 0 Then
wallpen.CompoundArray = _compArray
End If
wallpen.SetLineCap(Drawing2D.LineCap.RoundAnchor, Drawing2D.LineCap.ArrowAnchor, Drawing2D.DashCap.Flat)
e.Graphics.DrawLine(wallpen, _origin, _last)

Dim midPoint As New Point
midPoint.X = CInt(Math.Min(_origin.X, _last.X) + ((Math.Max(_origin.X, _last.X) - Math.Min(_origin.X, _last.X)) / 2))
midPoint.Y = CInt(Math.Min(_origin.Y, _last.Y) + ((Math.Max(_origin.Y, _last.Y) - Math.Min(_origin.Y, _last.Y)) / 2))

Dim mx As New System.Drawing.Drawing2D.Matrix
mx.Translate(midPoint.X, midPoint.Y)
mx.Rotate(Angle(_origin, _last))
e.Graphics.Transform = mx
Dim sf As New StringFormat
Dim ls As String = LineLength(_origin, _last).ToString

Dim l As SizeF = e.Graphics.MeasureString(ls, _parent.Font, New PointF(_parent.ClientSize.Width, _parent.ClientSize.Height), sf)
sf.LineAlignment = StringAlignment.Center
sf.Alignment = StringAlignment.Center

Dim rt As New Rectangle(0, 0, CInt(l.Width), CInt(l.Height))
rt.Inflate(3, 3)
rt.Offset(CInt(-(l.Width / 2)), CInt(-(l.Height / 2)))
Dim backBrush As New SolidBrush(_backColor)
e.Graphics.FillEllipse(backBrush, rt)
backBrush.Dispose()
Dim foreBrush As New SolidBrush(_foreColor)
e.Graphics.DrawString(ls, _parent.Font, foreBrush, 0, 0, sf)
foreBrush.Dispose()
sf.Dispose()
mx.Dispose()
wallpen.Dispose()
End If
End Sub

Private Sub MouseDown(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs)
_mouseCaptured = True
_origin = New Point(e.X, e.Y)
_last = New Point(-1, -1)
End Sub

Private Sub MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs)
If _mouseCaptured Then
Dim r As Rectangle = NormalizeRect(_origin, _last)
r.Inflate(_parent.Font.Height, _parent.Font.Height)
_parent.Invalidate(r)
_last = New Point(e.X, e.Y)
End If
End Sub

Private Sub MouseUp(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs)
_mouseCaptured = False
_parent.Invalidate()
RaiseEvent CaptureFinished(Me, New CaptureEventArgs(_origin, New Point(e.X, e.Y)))
End Sub

Private Function NormalizeRect(ByVal p1 As Point, ByVal p2 As Point) As Rectangle
Dim r As New Rectangle
If p1.X < p2.X Then
r.X = p1.X
r.Width = p2.X - p1.X
Else
r.X = p2.X
r.Width = p1.X - p2.X
End If
If p1.Y < p2.Y Then
r.Y = p1.Y
r.Height = p2.Y - p1.Y
Else
r.Y = p2.Y
r.Height = p1.Y - p2.Y
End If
Return r
End Function

Private Function LineLength(ByVal p1 As Point, ByVal p2 As Point) As Single
Dim r As Rectangle = NormalizeRect(p1, p2)
_length = CSng(Math.Sqrt(r.Width ^ 2 + r.Height ^ 2))
Return _length
End Function

Private Function Angle(ByVal p1 As Point, ByVal p2 As Point) As Single
_angle = CSng(Math.Atan((p1.Y - p2.Y) / (p1.X - p2.X)) * (180 / Math.PI))
Return _angle
End Function

' IDisposable
Private Overloads Sub Dispose(ByVal disposing As Boolean)
If Not Me._disposed Then
If disposing Then
' TODO: put code to dispose managed resources
RemoveHandler _parent.Paint, AddressOf Me.Painting
RemoveHandler _parent.MouseDown, AddressOf Me.MouseDown
RemoveHandler _parent.MouseMove, AddressOf Me.MouseMove
RemoveHandler _parent.MouseUp, AddressOf Me.MouseUp

_parent.Dispose()
End If

' TODO: put code to free unmanaged resources here
End If
Me._disposed = True
End Sub

#Region " IDisposable Support "
' This code added by Visual Basic to correctly implement the disposable pattern.
Public Overloads Sub Dispose() Implements IDisposable.Dispose
' Do not change this code. Put cleanup code in Dispose(ByVal disposing As Boolean) above.
Dispose(True)
GC.SuppressFinalize(Me)
End Sub

Protected Overrides Sub Finalize()
' Do not change this code. Put cleanup code in Dispose(ByVal disposing As Boolean) above.
Dispose(False)
MyBase.Finalize()
End Sub
#End Region

End Class

GeneralRe: getting started Pin
AllanNielsen2-May-06 22:41
AllanNielsen2-May-06 22:41 
Generalfiles missing from .zip file Pin
dfellman2-Nov-05 6:42
dfellman2-Nov-05 6:42 
AnswerRe: files missing from .zip file Pin
AllanNielsen2-Nov-05 18:12
AllanNielsen2-Nov-05 18:12 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.