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:
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:
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.
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.
Public Class Form1
Private WithEvents _ruler As MouseRuler
Public Sub New()
InitializeComponent()
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
End Sub
End Class
History
Changes made to date: