Introduction
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:
When the 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 TrackBar
's value.
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".
Positive Note
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.
Background
The 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 Form
.
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 TrackBar
.
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 mMouseDown
is true
.
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 sub
s.
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
L = CInt(MouseX - oLBLMain.Width / 2)
If L < 0 Then L = 0
If L > Me.Width - oLBLMain.Width Then L = Me.Width - oLBLMain.Width
oLBLMain.Left = L
oLBLMain.BringToFront()
End Sub
My typical users want am/pm time, so I have the PMTime sub
for subtracting 12 and adding "pm
".
Private Function PMTime(MilitaryHr As Integer, Optional NewMins As Integer = -1) As String
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
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 "NotSetYet
" in enum
s. 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
Else
mDataType = value
Select Case value
Case DataTypes.Integers
oLBLMain.Width = mIntCircleWidth
Min_IntValue = 1
Max_IntValue = 10
Current_IntValue = 5
Case DataTypes.Times
oLBLMain.Width = mTimeCircleWidth
Min_Hour = 6
Max_Hour = 22
Current_TimeValue = 14
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
oLBLMain.Text = TB.Value.ToString
RaiseEvent IntegerValue_Changed(TB.Value)
Case DataTypes.Times
Dim NewTime As Single = mMin_Hour + (0.25 * TB.Value)
Dim NewHour As Integer = Int(NewTime)
Dim NewMins As Integer = CInt((NewTime - NewHour) * 60)
oLBLMain.Text = PMTime(NewHour, NewMins)
RaiseEvent TimeValue_Changed(NewTime)
End Select
If mMouseDown = False Then
Dim RelativePosn As Integer = CInt((TB.Value - TB.Minimum) / (TB.Maximum - TB.Minimum) * Me.Width)
MoveLabel(RelativePosn)
End If
End Sub
Future Idea
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.
History