Introduction
I must have OCD (Obsessive Control Disorder). The standard TrackBar
has annoyed me for a long time, but I just put up with it until now. I finally got tired of the boring appearance, having to code a TextBox
or Label
with it to display the value, and a Label
for what it is. So, here is the TrackBar
I needed, hope it helps you too. There are too many properties to just list them out like I normally would, so I decided to list the function of the property groups and a screenshot of the whole list.
- The
Control
group contains the control's Border
, Value
, and Orientation
properties.
FloatValue
is the value that appears as the slider is moved with the mouse.
Label
is the text that appears above the TrackBar
.
Slider
is the line, tick marks, and the slider button itself.
UpDownButtons
are the buttons at each end of the TrackBar
to increment the value by one.
ValueBox
is the box displaying the value.
Properties
The Building Region contains the routines to setup the layout, size, and positions for all parts of the control based on the properties set.
Painting
Override the Paint
event to custom draw each piece of the control in the right place with the correct orientation.
Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
MyBase.OnPaint(e)
Dim g As Graphics = e.Graphics
g.SmoothingMode = SmoothingMode.AntiAlias
g.TextRenderingHint = Drawing.Text.TextRenderingHint.AntiAlias
If _BorderShow Then
g.DrawRectangle(New Pen(_BorderColor), _
0, 0, Me.Width - 1, Me.Height - 1)
End If
If _UpDownShow Then DrawUpDnButtons(g)
DrawSliderLine(g)
If _LabelShow Then
DrawLabel(g)
End If
DrawSlider(g)
If _FloatValue AndAlso IsOverSlider AndAlso _
MouseState = eMouseState.Down Then
DrawFloatValue(g)
End If
If Not _ValueBox = eValueBox.None Then
DrawValueBox(g)
End If
If _ShowFocus AndAlso Me.Focused Then
ControlPaint.DrawFocusRectangle(g, New Rectangle( _
2 + CInt(Not _BorderShow), 2 + CInt(Not _BorderShow), _
Me.Width - ((2 + CInt(Not _BorderShow)) * 2), _
Me.Height - ((2 + CInt(Not _BorderShow)) * 2)), _
Color.Black, Me.BackColor)
End If
End Sub
I used basic GDI+ functions to draw each piece of the control. I will highlight some of the better parts.
In DrawUpDnButtons
, I basically have a button that needs to be drawn one of four ways depending on the Orientation
and which side of the control
it is on. The button itself is just a rectangle with a gradient color fill. The trick was the arrow. I could have calculated each arrow position and points to create
a separate GraphicsPath
for each, but I used a Matrix
instead to Rotate
and/or Translate
a simple GraphicsPath
to the right position.
First set three points and add a line between them to create the triangular arrow ^ shape:
Dim gp As New GraphicsPath
Dim pts() As Point
Dim mx As New Matrix
pts = New Point() { _
New Point(5, 0), _
New Point(0, 5), _
New Point(5, 10)}
gp.AddLines(pts)
For the left hand button, the GraphicsPath
is oriented correctly so it just needs to be translated (moved) to the right position inside the button's rectangle.
With rectDownButton
mx.Translate(5, CSng((rectDownButton.Y _
+ (rectDownButton.Height / 2)) - 6))
gp.Transform(mx)
g.DrawPath(pn, gp)
End With
For the right hand button, the GraphicsPath
needs to be flipped, but there isn't a built-in function for flipping. Use this to flip the GraphicsPath
horizontally instead: New Matrix(-1, 0, 0, 1, image width here, 0)
.
With rectUpButton
mx = New Matrix(-1, 0, 0, 1, 5, 0)
mx.Translate(.X + 9, 0, MatrixOrder.Append)
gp.Transform(mx)
g.DrawPath(pn, gp)
End With
For the upper button, the GraphicsPath
needs to be rotated 90 degrees. Find the center point and RotateAt
that point.
With rectDownButton
mx.RotateAt(90, New PointF(gp.GetBounds.Width / 2, _
gp.GetBounds.Height / 2))
mx.Translate(CSng((rectDownButton.X + _
(rectDownButton.Width / 2)) - 3), 4, MatrixOrder.Append)
gp.Transform(mx)
g.DrawPath(pn, gp)
End With
For the lower button, the GraphicsPath
needs to be flipped again. Use this to flip the image vertically:
New Matrix(1, 0, 0, -1, 0, image height here)
.
With rectUpButton
mx = New Matrix(1, 0, 0, -1, 0, 10)
mx.Translate(0, .Y + 6, MatrixOrder.Append)
gp.Transform(mx)
g.DrawPath(pn, gp)
End With
Custom Type Converters
The CenterPoint
and FocusScales
used in the slider brushes are of type PointF
. When you create a new property of type PointF
, you will notice that it is grayed out in the PropertyGrid
, but if a property is of type Point
, it will edit correctly in the PropertyGrid
. The problem is that there is no built-in TypeConverter
for PointF
. This is actually pretty easy to fix.
A PointF
type property without a custom TypeConverter
: it works in code, but you cannot edit it in the PropertyGrid
.
With the PointFConverter
, it performs just like the Point
property in the PropertyGrid
.
Create a property and add the TypeConverter
attribute referencing the PointFConverter
class we are about to add.
Private _SliderHighlightPt As PointF = New PointF(-5.0F, -2.5F)
<Category("Appearance Slider")> _
<Description("Point on the Slider for the Highlight Color")> _
<TypeConverter(GetType(PointFConverter))> _
Public Property SliderHighlightPt() As PointF
Get
Return _SliderHighlightPt
End Get
Set(ByVal value As PointF)
_SliderHighlightPt = value
Me.Invalidate()
End Set
End Property
The PointFConverter
class inherits ExpandableObjectConverter
, and overrides the CanConvertFrom
, ConvertFrom
, and ConvertTo
functions.
Friend Class PointFConverter : Inherits ExpandableObjectConverter
Public Overloads Overrides Function CanConvertFrom( _
ByVal context As System.ComponentModel.ITypeDescriptorContext, _
ByVal sourceType As System.Type) As Boolean
If (sourceType Is GetType(String)) Then
Return True
End If
Return MyBase.CanConvertFrom(context, sourceType)
End Function
Public Overloads Overrides Function ConvertFrom( _
ByVal context As System.ComponentModel.ITypeDescriptorContext, _
ByVal culture As System.Globalization.CultureInfo, _
ByVal value As Object) As Object
If TypeOf value Is String Then
Try
Dim s As String = CType(value, String)
Dim ConverterParts(2) As String
ConverterParts = Split(s, ",")
If Not IsNothing(ConverterParts) Then
If IsNothing(ConverterParts(0)) Then ConverterParts(0) = "-5"
If IsNothing(ConverterParts(1)) Then ConverterParts(1) = "-2.5"
Return New PointF(CSng(ConverterParts(0).Trim), _
CSng(ConverterParts(1).Trim))
End If
Catch ex As Exception
Throw New ArgumentException("Can not convert '" & _
CStr(value) & "' to type Corners")
End Try
Else
Return New PointF(-5.0F, -2.5F)
End If
Return MyBase.ConvertFrom(context, culture, value)
End Function
Public Overloads Overrides Function ConvertTo( _
ByVal context As System.ComponentModel.ITypeDescriptorContext, _
ByVal culture As System.Globalization.CultureInfo, _
ByVal value As Object, ByVal destinationType As System.Type) As Object
If (destinationType Is GetType(System.String) _
AndAlso TypeOf value Is PointF) Then
Dim ConverterProperty As PointF = CType(value, PointF)
Return String.Format("{0}, {1}", _
ConverterProperty.X, _
ConverterProperty.Y)
End If
Return MyBase.ConvertTo(context, culture, value, destinationType)
End Function
End Class
Custom Color Type Converters
Sometimes with custom controls the properties can get a bit unwieldy. There are a lot of color choices with this control and some of them have repeating patterns.
For example, the Slider button has three states and three properties (Face
,
Border
, and Highlight
). Instead of having nine colors listed in a long line of confusing properties,
they are grouped into three expandable properties with three sub properties each. The
ColorPack
class and TypeConverter
is used for the three slider state properties.
The ColorLinearGradient
class and TypeConveter
is used for the Linear gradient properties of the
slider lines. This makes the PropertyGrid look cleaner and easier to read.
Mouse Events
Here is where we check what part of the control the cursor is over and if the mouse button is pressed. Based on this information, the Value
is adjusted accordingly.
Because the MouseDown
, Click and so on are a one time deal, a Timer
is needed see if the mouse is still down and if so change the value again.
I didn't want it to run away as soon as it was clicked so there is a built in delay right after the mouse is clicked, then after the delay it will begin incrementing the value.
The other issue is if the Min/Max span was big it would crawl super slow and small spans would zip too fast, so the Timer
's interval is adjusted based on how big the span is.
Private Sub MouseTimer_Tick(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles MouseTimer.Tick
If MouseHoldDownTicker < 5 Then
MouseHoldDownTicker += 1
If MouseHoldDownTicker = 5 Then
MouseTimer.Interval = CInt(Math.Max _
(10, 100 - ((_MaxValue - _MinValue) / 10)))
End If
Else
Me.Value += MouseHoldDownChange
End If
End Sub
Key Events
I wanted to be able to adjust the Value
by pressing the arrow keys. That sounded simple. I figured I would just check the e.KeyValue
in the KeyUp
event and adjust the Value
accordingly. Well, not so simple. The problem is that the UserControl Inherits Button
, which automatically handles the arrow keys differently. After the KeyUp
event, the focus jumps to the next control in the Tab Order, even if you use e.Handled
and e.SuppressKeyPress
. I couldn't stop the focus change. Then, I thought I would use the KeyDown
event, but guess what, the arrows are automatically ignored there. To fix this behavior, I override the IsInputKey
function to allow the arrow keys. After this, the focus will not jump away anymore.
Protected Overrides Function IsInputKey( _
ByVal keyData As System.Windows.Forms.Keys) As Boolean
Select Case keyData And Keys.KeyCode
Case Keys.Up, Keys.Down, Keys.Right, Keys.Left
Return True
Case Else
Return MyBase.IsInputKey(keyData)
End Select
End Function
Private Sub gTrackBar_KeyUp(ByVal sender As Object, _
ByVal e As System.Windows.Forms.KeyEventArgs) Handles Me.KeyUp
Dim adjust As Integer = _ChangeSmall
If e.Shift Then
adjust = _ChangeLarge
End If
Select Case e.KeyValue
Case Keys.Up, Keys.Right
Me.Value += adjust
Case Keys.Down, Keys.Left
Me.Value -= adjust
End Select
End Sub
History
- Version 1.0 - March 2009
- Version 1.2 - April 2009
- Added Focus Rectangle
- Separated the label into its own rectangle area for better layout and sizing
- General Layout Fixes
- Version 1.3 - April 2009
- Version 1.4 - July 2010
- Added Image Slider
- Added
UpDownShow
to hide and turn off the Up/Down buttons
- Added
Timer
for changing the value (after a short delay) until the mouse is released
- Version 1.5 - July 2011
- Added JumpToMouse
- Fixed Tick alignment issue when Label shown
- Added
SnapToValue
property
- Version 1.6 - September 2011
- Added
ValueDivisor
and ValueAdjusted
to allow decimal values
- Fixed Vertical version when minimum is greater than 0
- Version 1.7 - February 2012
- Combined
Up
, Down
, and Hover
color properties to
separate ColorPack
class
- Added default values to properties
- Version 1.8 - April 2012
ColorLinearGradient
class for coloring the slider lines
- Fixed some layout bugs
- Added
TickThickness
and TickOffset
properties