Cool TrackBar for Windows Forms - CodeProject

:

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 subs.

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 "pm".

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 "NotSetYet" in 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

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