|

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:
| You must Sign In to use this message board. |
|
| | Msgs 1 to 14 of 14 (Total in Forum: 14) (Refresh) | FirstPrevNext |
|
|
 |
|
|
Although I didn't use your code, your example showed me why my code was not working. I attempted this by creating my own graphics in the MouseDown event. I moved my line drawing code to the form paint event handler, and voila!! No more flicker, and it works like a charm!! Thanks again!
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
This is excellent....
Is there a way of saving the lines so they do not disappear and putting the lengths into a list box?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
I did it by creating an array of points.
Point[] linePoints = new Point[100];
You can then store each new point into the array.
When you want to redraw the lines, you just call the DrawLines function, which takes as an input an array of points.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Hello, I need to write the codes in vb.net for the eqation of line (y=mx+c) and equation of circle (x2+y2=r2) where 2 is square.Can anyone help me out plz. thank you
cutenush
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Hi, nice code, thanks for sharing this.
I am a beginner to .Net drawing and so unfortunately cant quite see how this is working. I am referring to the way that the line/text gets drawn over whatever is on the background graphics object, yet it 'dissapears' and gets redrawn as you mouse drag it around. Basically what I dont understand here is how come once a line has been drawn it is no longer part of that graphics object. I understand that the line is redrawn, but I dont see anything in the code that 'removes' the line from its old location. The graphics that made up the background is not getting redrawn...so I dont get how that works.
Does my question make sense? Please try explain this to me, its killing me!!
|
| Sign In·View Thread·PermaLink | 4.00/5 (1 vote) |
|
|
|
 |
|
|
The secret will be the ControlPaint.DrawReversibleLine() method.
I haven't looked at this code, but based on having used the ControlPaint.DrawReversibleFrame() method in my "Marching Ants" project[^], I expect that the existing line is "undrawn" before the new one is drawn -- that is, that the program invokes the method a second time with the same arguments previously used, which action causes the line to disappear.
Think of it this way: multiply a number by -1 and you get its negative; multiply that number by -1 and you get the original number back.
Also -- and I could be misremebering here, but -- I don't think that the line (or rectangle, as in my project) is actually incorporated into the background graphics object, but rather is drawn onto the screen.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Sorry, I have not been getting emails to tell me the article has new comments.
Ilion, this routine does NOT use the reversible lines code as it suffers many problems. Instead it uses standard drawing techniqules made available to .Net. There is no need to redraw the reverse line because the parent.Invalidate() calls ensure the canvas will blank the next time it draws, and therefore only needs to draw once. The standard Invalidate and repaint when possible is what makes this a superior solution. And being a seperate control ensures it does not affect anything else on the screen.
Hope this helps. Allan
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
chimeric69 wrote: Sorry, I have not been getting emails to tell me the article has new comments.
That would be a real nuisance.
chimeric69 wrote: Ilion, this routine does NOT use the reversible lines code as it suffers many problems.
And you do clearly state that in the Introduction ... but I just skimmed it, rather than *reading* it, and so I missed the word "replaces."
chimeric69 wrote: Hope this helps.
Sure, it helps me. But, I suppose it really depends upon whether poor (I'm referring to the 1 1/2 years between his post and ours) Robin gets an email to tell him that you've corrected my incorrect answer to his question.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Hi could you point me in the right direction is there a lot of coding to get this running I have vb.net 2003 ? will this work
-- modified at 3:11 Thursday 2nd February, 2006
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
I don't know... I only have 2005. I'de recommend tring it. Let me know if you have any errors during compile and I'll see if I can help.
Create a new 2003 VB project and add the class files to the project, is probably the easiest way to start.
Sorry I couldn't be of more help. Maybe someone out there has already done it and can help.
-- modified at 5:11 Thursday 2nd February, 2006
You really only need MouseRuler.vb for this to work... Form1 is only a demo of how to use it.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
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
|
| Sign In·View Thread·PermaLink | 5.00/5 (1 vote) |
|
|
|
 |
|
|
 |
|
|
 |
|
|
 |
|
|
General News Question Answer Joke Rant Admin
|