Cool TrackBar for Windows Forms - CodeProject
The Windows Forms
TrackBar is okay, but I thought it could be improved. So I stopped it from scrolling, made its current value more prominent, made the value move with the mouse smoothly when it changes value, and gave it the ability to manage a Time value.
A problem: Bad scrolling behaviour:
TrackBar control happens to have the focus on a window (a form), and the user scrolls their mousewheel, they probably want to scroll the window, but the trackbar gets scrolled, changing its value. This can be very bad - it might be holding a value that is crucial to your business logic - and then the user closes the window without noticing they changed the
Better to simply disable the
TrackBar's scroll event, which has been documented in lots of places - you just substitute a class that inherits the
TrackBar and forwards the scroll event to the parent, or since mine is in a usercontrol, to the
Parent.Parent. So the new class appears in your toolbox, and you don't actually use the
TrackBar, but rather the new item in the toolbox. The code for the class is farther down, in "Using the code".
Due to its nice compact format, the
TrackBar is good for setting all sorts of things like an integer value, or the size of the text in a window, or some values like
Time - hey, it can't do that! So here's how to get it to show a time value, and the user can easily and quickly set a time.
TrackBar has been around forever, but it has limitations. This tip will show you how to prevent scrolling it with the mousewheel, and adds some extra functionality, namely time value selection.
Using the Code
Here's the class I found that replaces the
TrackBar with one that won't scroll. It passes focus to the parent with
Parent.Focus, but might need to be
Parent.Parent.Focus since the parent is the
UserControl, not the
Form. Or I guess you could handle the
UserControl.Scroll event and do something similar to forward the scroll event to the
Public Class No_ScrollWheel_TrackBar Inherits TrackBar Protected Overrides Sub OnMouseWheel(e As MouseEventArgs) Dim H As HandledMouseEventArgs = DirectCast(e, HandledMouseEventArgs) H.Handled = True Parent.Focus() End Sub End Class
The value of the control is just a label, but with a difference - it is round. So it's a custom label class, which I found somewhere, lots of people have got this documented, so it's just an example of how to do that:
Public Class OvalLabel Inherits Label Protected Overrides Sub OnResize(e As EventArgs) MyBase.OnResize(e) Using GPath As New GraphicsPath() GPath.AddEllipse(New Rectangle(0, 0, Me.Width - 1, Me.Height - 1)) Me.Region = New Region(GPath) End Using End Sub End Class
Oh, here's what makes it cool - the value label follows the mouse smoothly, which is accomplished by handling
MouseMove on the
To do that, all you have to do is set a boolean
mMouseDown True in the
MouseDown handler, set it
False in the
MouseUp handler, and then move the main value label in the
MouseMove handler, but only if
Private mMouseDown As Boolean = False Private Sub TB_MouseDown(sender As Object, e As MouseEventArgs) Handles TB.MouseDown mMouseDown = True End Sub Private Sub TB_MouseUp(sender As Object, e As MouseEventArgs) Handles TB.MouseUp mMouseDown = False End Sub Private Sub TB_MouseMove(sender As Object, e As MouseEventArgs) Handles TB.MouseMove If mMouseDown Then MoveLabel(e.X) End If End Sub
Notice how I don't do the work in the
MouseMove sub? I prefer putting all that kind of code into a "helper sub", especially because I usually find I inevitably need to call it from other
I guess a person could use that same concept to put all things like that in a class instead of in the form, so your forms end up having almost no code at all, then it's easier to move all of this to a webform of some kind. Just a thought.
So here's the
MoveLabel sub then:
Private Sub MoveLabel(MouseX As Integer) Dim L As Integer = 0 'Set its horizontal center to the mouse's X value: L = CInt(MouseX - oLBLMain.Width / 2) 'Label Left is never negative: If L < 0 Then L = 0 'Label won't go any further right than the right edge of the UserControl: If L > Me.Width - oLBLMain.Width Then L = Me.Width - oLBLMain.Width 'move the label: oLBLMain.Left = L 'keep it in front of other stuff: oLBLMain.BringToFront() End Sub
My typical users want am/pm time, so I have the
PMTime sub for subtracting 12 and adding "
Private Function PMTime(MilitaryHr As Integer, Optional NewMins As Integer = -1) As String 'note the optional value, ' for use with the max and min labels, ' where I don't want the minutes showing Dim ReturnValue As String = "" Dim NewHour As Integer Dim AMPMText As String Select Case MilitaryHr Case Is < 12 NewHour = MilitaryHr AMPMText = "am" Case Is = 12 NewHour = MilitaryHr AMPMText = "pm" Case Else AMPMText = "pm" NewHour = MilitaryHr - 12 End Select ReturnValue = CStr(NewHour) If NewMins = -1 Then 'ignore mins Else ReturnValue += ":" + Format(NewMins, "0#") End If ReturnValue += AMPMText Return ReturnValue End Function
Multi-purpose as well
Well, you could use the
TrackBar for all sorts of things where you want a tidy small control that can change a value. I use them for many things. For example, in my custom
messagebox window, I have it at the bottom. If the user moves it, the text in the
messagebox gets bigger or smaller.
One thing that's cool is managing a time value with it.
To do that, you have to have 2 or more "modes" that you can set. In this case, I call them
DataTypes, and one is "
Integers" and the other is "
Times". I usually include "
enums. Since the first value is zero, that makes all the others one-based. Rant: One-based! What's that? That's where you count your beans starting at one and ending with the number of beans. What an idea!
Public Enum DataTypes NotSetYet Integers Times End Enum
When you wish to set the mode, some defaults are set for the minimum, maximum, and value:
Private mDataType As DataTypes = DataTypes.Integers Public Property DataType As DataTypes Get Return mDataType End Get Set(value As DataTypes) If mDataType = value Then 'don't do anything Else 'it is changing mDataType = value 'now set max, min, value according to the DataType: Select Case value Case DataTypes.Integers 'narrower circle to display integer oLBLMain.Width = mIntCircleWidth 'throw in some defaults Min_IntValue = 1 Max_IntValue = 10 Current_IntValue = 5 'whatever Case DataTypes.Times 'wider circle to display e.g. 11:45pm oLBLMain.Width = mTimeCircleWidth 'throw in some defaults Min_Hour = 6 Max_Hour = 22 Current_TimeValue = 14 'whatever End Select End If End Set End Property
Also, when you change the "
DataType", you have to change pretty well everything about the control. Instead of managing sequential integers, 1, 2, 3, etc., it now will manage quarters of an hour: 3:00pm, 3:15pm, 3:30pm, etc.
To do that, you have to have 4x the number of ticks as you have hours displayed, and then convert all of that whenever the max, min, or value changes. Here's the helper
sub for when the
TrackBar changes value:
Private Sub DoValueChanged() Select Case mDataType Case DataTypes.Integers 'simple: oLBLMain.Text = TB.Value.ToString RaiseEvent IntegerValue_Changed(TB.Value) Case DataTypes.Times 'the TrackBar is moving by increments of 15 minutes, ' so we can get the time displayed by multiplying the ' value of the trackbar by .25, or 1/4 hour ' and then adding that to the min value Dim NewTime As Single = mMin_Hour + (0.25 * TB.Value) 'Need the Hour value, but not rounded. Int does not round, CInt does. Dim NewHour As Integer = Int(NewTime) 'extract the minutes portion: Dim NewMins As Integer = CInt((NewTime - NewHour) * 60) 'get the formatted string oLBLMain.Text = PMTime(NewHour, NewMins) RaiseEvent TimeValue_Changed(NewTime) End Select If mMouseDown = False Then 'have to move circle based on tb.value. Dim RelativePosn As Integer = CInt((TB.Value - TB.Minimum) / (TB.Maximum - TB.Minimum) * Me.Width) MoveLabel(RelativePosn) End If End Sub
I might also use this to set "tenths" e.g. 2.1, 2.2, 2.3 - so I will give it a third mode maybe later.
Points of Interest
I thought the value label following the mouse was pretty good.