Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB

gTimePicker- Control to Pick a Time Value (VB.NET)

4.88/5 (38 votes)
8 Feb 2012CPOL7 min read 143.3K   4K  
Stop using the DateTimePicker for time values. This control makes it easy to pick a time, and if you act now, get the matching Nullable gDateTimePicker at no extra cost.
Image 1

Introduction

I couldn't put up with the standard time part of the DateTimePicker any longer. I wanted an easy to use date dropdown and a non-existent time dropdown. First, I made the gTimePickerCntrl to pick the time in a couple of clicks using a clock-like interface. Second, I needed a dropdown control to contain it. Third, the developer in me needed the extra design-time support.

After making the gTimePicker, I realized I needed a nullable DateTimePicker to go with it. Now, I have added the gDateTimePicker and added a nullable feature to the gTimePicker.

How to Use the gTimePickerCtrl

Very simple -
Click a number in the inner ring for the hour.
Click a number from the outer ring for minutes.

Control Properties

Here is a list of the primary properties:

  • Public Property Time() As String

    Get or set the time value

  • Public Property TimeAMPM() As eTimeAMPM

    Get or set the AM PM value

  • Public Property Hr24() As Boolean

    Get or set the time as 12 or 24 hour

  • Public Property TrueHour() As Boolean

    Get or set if the hour hand shows true clock position or stays pointing at the chosen hour regardless of the minute

  • Public Property TimeColors() As TimeColors

    Get or set the color scheme for the control

Paint

At first, I had a background image of the clock face and drew the hands on top of it, but then I got stuck with that image's color. To make it more dynamic, I drew the frames using a simple PathGradientBrush to create the illusion of a 3D frame.

VB.NET
Sub DrawClockFace(ByRef g As Graphics, ByVal rect As Rectangle)

    'Simple Breakdown of creating a ColorBlend from scratch
    g.SmoothingMode = SmoothingMode.AntiAlias

    Dim blend As ColorBlend = New ColorBlend()

    'Add the Array of Color
    Dim bColors As Color() = New Color() { _
        TimeColors.FrameOuter, _
        TimeColors.FrameInner, _
        TimeColors.FrameOuter, _
        TimeColors.FaceOuter, _
        TimeColors.FaceInner}
    blend.Colors = bColors

    'Add the Array Single (0-1) colorpoints to place each Color
    Dim bPts As Single() = New Single() { _
        0, _
        0.0408, _
        0.082, _
        0.109, _
        1}
    blend.Positions = bPts

    ' Create a PathGradientBrush
    Dim gp As New GraphicsPath
    gp.AddEllipse(rect)
    Using br As New PathGradientBrush(gp)

        'Blend the colors into the Brush
        br.InterpolationColors = blend

        'Fill the rect with the blend
        g.FillEllipse(br, rect)

    End Using
    gp.Dispose()
End Sub

To draw the hands on the clock, you need to calculate the point around the clock.

VB.NET
...
Dim HourAngle As Single = 90 - (CSng(30 * (Val(_Time.Substring(0, 2))) + _
    CSng(IIf(TrueHour, Val(_Time.Substring(3, 2)) / 2, 0))))
Dim MinAngle As Single = 90 - CSng(6 * Val(_Time.Substring(3, 2)))

e.Graphics.DrawLine(HrPen, Center, GetPoint(Center, 35, HourAngle))
e.Graphics.DrawLine(MinPen, Center, GetPoint(Center, 60, MinAngle))

...

Public Function GetPoint(ByVal ptCenter As Point, ByVal nRadius As Integer, _
       ByVal fAngle As Single) As Point

    Dim x As Single = CSng(Math.Cos(2 * Math.PI * fAngle / 360)) * nRadius + ptCenter.X
      Dim y As Single = -CSng(Math.Sin(2 * Math.PI * fAngle / 360)) * nRadius + ptCenter.Y

    Return New Point(CInt(Fix(x)), CInt(Fix(y)))

End Function

In the Time property, the validation of the value occurs, including some Regex matching.

Values like 07:00, 800, 4, 0900p, 4:00 A... are acceptable.

VB.NET
Public Property Time() As String
     Get
         Return _Time
     End Get
     Set(ByVal value As String)
         Dim tTime As String = _Time

         If Not IsNothing(value) And value <> String.Empty Then

             'Check if value is just the hour
             If Regex.IsMatch(value, _
                "^[0-9]{1}$|^[0-1]{1}[0-9]{1}$|^[2]{1}[0-3]{1}$") Then
                 value = value & ":00"
             End If

             Dim ap As eTimeAMPM

             If Hr24 Then
                 If Val(value.Replace(":", String.Empty)) >= 1200 Then
                     ap = eTimeAMPM.PM
                 Else
                     ap = eTimeAMPM.AM
                 End If
                 value = Format(Val(value.Replace(":", String.Empty)), "0000")
             Else
                 ap = _TimeAMPM

                 'Check if a P, PM, A or AM is on the End
                 'Update TimeAMPM Prop and remove from value
                 If value.ToUpper.EndsWith("P") Or value.ToUpper.EndsWith("PM") Then
                     value = value.ToUpper.Trim(CChar("M")).Trim(CChar("P")).Trim
                     ap = eTimeAMPM.PM
                 ElseIf value.ToUpper.EndsWith("A") _
         Or value.ToUpper.EndsWith("AM") Then
                     value = value.ToUpper.Trim(CChar("M")).Trim(CChar("A")).Trim
                     ap = eTimeAMPM.AM
                 End If
             End If

             'Check if value is a valid time with or without a colon
             If Regex.IsMatch_
     (value, "^(([0-9])|([0-1][0-9])|([2][0-3])):?([0-5][0-9])$") Then
                 'Check and add leading '0'
                 If Regex.IsMatch_
         (value, "^(([0-9])):?([0-5][0-9])$") Then value = "0" & value
                 'Add a Colon if missing
                 If Regex.IsMatch_
         (value, "^(([0-1][0-9])|([2][0-3]))([0-5][0-9])$") Then
                     _Time = String.Format("{0}:{1}", value.Substring(0, 2), _
                         value.Substring(2, 2))
                 Else
                     _Time = value
                 End If

                 If Not IsNothing(ap) Then TimeAMPM = ap

                 'Adjust for 12 or 24 hour time
                 If Hr24 Then
                     If Hour() >= 12 Then
                         TimeAMPM = eTimeAMPM.PM
                     Else
                         TimeAMPM = eTimeAMPM.AM
                     End If
                 Else
                     If Hour() > 12 Then
                         _Time = String.Format("{0:0#}:{1:0#}", _
                             Hour() - 12, Minute)
                         TimeAMPM = eTimeAMPM.PM
                     ElseIf Hour() = 0 Then
                         _Time = String.Format("12:{0:0#}", Minute)

                     End If

                 End If
             End If

         Else
             _Time = String.Empty

         End If
         If tTime <> _Time Then RaiseEvent TimePicked(Me)

         Invalidate()
     End Set
 End Property

Get Time from Mouse Position

The MouseDown event determines if the mouse is in the Hour or the Minute ring by calculating the distance the mouse is from the center.

VB.NET
Private Sub gTimePickerCntrl_MouseDown(ByVal sender As Object, _
  ByVal e As MouseEventArgs) Handles Me.MouseDown

    'Determine how far from center
    Dim radius As Integer = CInt( _
        Math.Round( _
        Math.Sqrt( _
            Math.Pow(CDbl(Center.X - e.Location.X), 2) + _
            Math.Pow(CDbl(Center.Y - e.Location.Y), 2)) _
            , 2))
    If radius <= 55 Then
        IsHourRadius = True
    Else
        IsHourRadius = False
    End If
    UpdateTime(e)

End Sub

Then the GetAngle function is used in the Sub UpdateTime to Calculate which number it is over.

VB.NET
Private Shared Function GetAngle(ByVal Origin As PointF, _
                            ByVal XYPoint As PointF) As Integer

    Dim angleRadians As Double = Math.Atan2( _
                                    (-(XYPoint.Y - Origin.Y)), _
                                    ((XYPoint.X - Origin.X)))
    Dim translatedAngle As Integer
    Dim angle As Integer = CInt(Math.Round(angleRadians * (180 / Math.PI)))

    'Translate to orient o degrees to the North
    If angle <= 90 Then
        translatedAngle = 90 - angle
    Else
        translatedAngle = 450 - angle
    End If

    Return translatedAngle

End Function

gTimePicker DropDown Control

Image 2

The gTimePicker is a simple dropdown User Control to contain the gTimePickerCntrl. I use an extended TextBox and manually draw the buttons. The gTimePickerCntrl is hosted in a ToolstripDropDown.

Events

  • Public Event TimePicked(ByVal sender As Object)

    This event will fire when the time is changed.

Initialize

To setup a ToolStripDropDown, first put the gTimePickerCntrl into a ToolStripControlHost.

VB.NET
host = New ToolStripControlHost(gTime)

Then, add the ToolStripControlHost to the ToolStripDropDown.

VB.NET
popup.Items.Add(host)

Here is the complete Sub, including the added event handlers:

VB.NET
Private gTime As New gTimePickerCntrl

Private Sub gTimePicker_Load(ByVal sender As Object, _
            ByVal e As System.EventArgs) Handles Me.Load

    host = New ToolStripControlHost(gTime)

    host.Margin = Padding.Empty
    host.Padding = Padding.Empty
    host.AutoSize = False
    host.Size = gTime.Size

    popup.Size = gTime.Size
    popup.Items.Add(host)

    AddHandler popup.Closed, AddressOf popup_Closed
    AddHandler popup.Closing, AddressOf popup_Closing
    txbTime.Text = _Time
    Clear.Items.Add("Clear Time")
    Me.ContextMenuStrip = Clear

End Sub

Control Properties

The matching gTimePickerCntrl properties get or set the corresponding TimePickerCntrl values. Here is a list of the additional properties:

  • Public Property TimeAMPM() As eTimeAMPM

    Get or set the AM PM value

  • Public Property Hr24() As Boolean

    Get or set the time as 12 or 24 hour

  • Public Property TrueHour() As Boolean

    Get or set if the hour hand shows true clock position or stays pointing at the chosen hour regardless of the minute

  • Public Property TimeColors() As TimeColors

    Get or set the color scheme for the control

  • Public Property TextBackColor() As Color

    Get or set the backcolor for the text

  • Public Property TextForeColor() As Color

    Get or set the forecolor for the text

  • Public Property TextAlign() As HorizontalAlignment

    Get or set the horizontal alignment for the text

  • Public Property TextFont() As Font

    Get or set the textbox font

  • Public Property ButtonForeColor() As Color

    Get or set the color of the arrow on the dropdown button

  • Public Property ButtonBackColor() As Color

    Get or set the base color of the dropdown button

  • Public Property ButtonHighlight() As Color

    Get or set the highlight color of the dropdown button

  • Public Property ButtonBorder() As Color

    Get or set the border color of the dropdown button

  • Public Property NullText() As String

    Text to display when NULL

  • Public Property NullTextInFront() As Boolean

    Should the NULL text appear in front of the hatch fill

  • Public Property NullTextColor() As Color

    Color for the NULL text

  • Public Property NullHatchStyle() As HatchStyle

    Chooses the HatchStyle

  • Public Property NullColorA() As Color

    Color A for the HatchStyle

  • Public Property NullColorB() As Color

    Color B for the HatchStyle

  • Public Property NullAlpha() As Integer

    Alpha value for HatchStyle so you can see the NULL text through it

Methods

  • Public Function ToStringAMPM() As String

    Returns _Time & " " & _TimeAMPM.ToString

  • Public Function ToDate() As DateTime

    Returns CDate(_Time & " " & _TimeAMPM.ToString)

  • Public Function Hour() As Integer

    Returns the hour

  • Public Function Minutes() As Integer

    Returns the minutes

  • Public Sub TimeInMinutes(minutes as Integer)

    Sets the time using minutes (example: 1100 minutes equals 06:20 PM)

Mouse Events

In the MouseDown event, check if the popup is open or closed, and Show or Hide it accordingly. One problem is when clicking the button to close the ToolStripDropDown, it loses focus, causing it to close automatically, so when the click gets through, it reopens. To get around this, check if the pointer is over the button and set IsPopupOpen = False in the popup_Closing event.

VB.NET
Private Sub popup_Closing(ByVal sender As Object, _
        ByVal e As ToolStripDropDownClosingEventArgs)
    'Workaround Focus loss
    Try
        If (Not rectDropDownButton.Contains(PointToClient(Control.MousePosition)) _
            Or (e.CloseReason = ToolStripDropDownCloseReason.Keyboard)) Then
            IsPopupOpen = False
        End If

    Catch ex As Exception

    End Try
End Sub

KeyPress Event

For a quick adjustment, press the Up or Down arrow keys to adjust the minutes and Shift-Up or Shift-Down to adjust the hours.

Nullable TextBox

I wanted the gTimePicker to match the gDataTimePicker visually when nulled, so I extended the standard TextBox, added the null properties, and overrode the WndProc Sub to paint the null properties when Text.Length = 0.

VB.NET
Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
    MyBase.WndProc(m)
    Const WM_PAINT As Integer = &HF
    If m.Msg = WM_PAINT Then

        If Me.Text.Length <> 0 Then
            Return
        End If
        Using g As Graphics = Me.CreateGraphics
            g.Clear(Me.BackColor)
            If Not _NullTextInFront Then _
                g.DrawString(_NullText, New Font(Me.Font.Name, Me.Font.Size, _
                FontStyle.Bold), New SolidBrush(_NullTextColor), 0, 0)
            g.FillRectangle(New HatchBrush(_NullHatchStyle, _
                Color.FromArgb(_NullAlpha, _NullColorA), _
                Color.FromArgb(_NullAlpha, _NullColorB)), ClientRectangle)
            If _NullTextInFront Then _
                g.DrawString(_NullText, New Font(Me.Font.Name, Me.Font.Size, _
                FontStyle.Bold), New SolidBrush(_NullTextColor), 0, 0)
        End Using
    End If
End Sub

Private Sub gTextBox_TextChanged(ByVal sender As Object, _
            ByVal e As System.EventArgs) Handles Me.TextChanged
    If Me.Text = "" Or Me.Text.Length = 1 Then Me.Invalidate()
End Sub

Design Time Extras

Since I already have an article on UITypeEditors, go here for a more detailed explanation of any design time features: UITypeEditorsDemo[^].

I created a separate class for the color scheme so I could manipulate it easier in the editors. Class TimeColorConverter: Inherits ExpandableObjectConverter allows editing of individual colors directly in the property grid. Class TimeUIEditor : Inherits UITypeEditor gives a dropdown for the Time property in the property grid.

Image 3

Class TimeColorsUIEditor : Inherits UITypeEditor opens a dialog to edit all the colors with a preview by pressing the button in the property grid.

Image 4

Image 5

gDateTimePicker

Image 6

It looks like a DateTimePicker because it Inherits System.Windows.Forms.DateTimePicker. There are quite a few nullable DateTimePickers out there, but I wanted something a little different (as usual), so here it is. Everything functions normally, except you can set the value to Nothing. Setting DateTimePicker.Value = Nothing will cause an error normally. I started by shadowing the Value property of the DateTimePicker control and using the DateTime.MinValue switcheroo method I had seen in some other controls, but that just led to a lot of synchronization problems, especially when I tried to bind the control.

Time to start over. The DateTimePicker has three properties I need to manipulate: Value, Format, and CustomFormat. The trick to simulating the Null is to set the Format to Custom and CustomFormat = " " so it displays a space no matter what the value is. This visually looks like a NULL, but the value isn't truly a NULL, especially if you want to bind it. First, I made these properties hidden:

VB.NET
<Browsable(False)> _
<DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)> _
<EditorBrowsable(EditorBrowsableState.Never)> _
Public Shadows Property Value() As DateTime
    Get
        Return MyBase.Value
    End Get
    Set(ByVal value As DateTime)
        MyBase.Value = value
    End Set
End Property

<Browsable(False)> _
<DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)> _
<EditorBrowsable(EditorBrowsableState.Never)> _
Public Shadows Property Format() As DateTimePickerFormat
    Get
        Return MyBase.Format
    End Get
    Set(ByVal value As DateTimePickerFormat)
        MyBase.Format = value
    End Set
End Property

<Browsable(False)> _
<DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)> _
<EditorBrowsable(EditorBrowsableState.Never)> _
Public Shadows Property CustomFormat() As String
    Get
        Return MyBase.CustomFormat
    End Get
    Set(ByVal value As String)
        MyBase.CustomFormat = value
    End Set
End Property

These are replaced by gValue, gFormat, and gCustomFormat. The gValue is defined as Type Nullable(Of DateTime) so the gValue can actually be a Date or NULL without any manipulation.

VB.NET
Private _gValue As Nullable(Of DateTime) = Today
<Editor(GetType(NullableDateTimeTypeEditor), GetType(UITypeEditor))> _
<Bindable(True)> _
<Category("Appearance")> _
Public Property gValue() As Nullable(Of DateTime)
    Get
        Return _gValue
    End Get

    Set(ByVal value As Nullable(Of DateTime))
        CheckFormat(value)
        Dim changed As Boolean = Not _gValue.Equals(value)
        _gValue = value
        If _gValue.HasValue Then
            MyBase.Value = CDate(_gValue)
        End If
        If changed Then RaiseEvent ValueOrNullChanged(Me)
    End Set
End Property

The only additional extra I wanted was to still be able to have the date dropdown in the propertygrid and still be able to NULL it. The Type Nullable(Of DateTime) doesn't have its own editor, so I made the NullableDateTimeTypeEditor and NullableDateTimeDropDown for it.

Image 7

Altering the null properties allows you to change the appearance of the control when it is Null. Change the Fill and/or add a text message to appear.

VB.NET
Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
    MyBase.WndProc(m)
    Const WM_ERASEBKGND As Integer = &H14
    If m.Msg = WM_ERASEBKGND Then
        Using g As Graphics = Me.CreateGraphics
            If Not _gValue.HasValue Then
                'Reduce the ClientRectangle so the dropdown button won't get
                'erased when something else covers part of the control
                Dim meRect As Rectangle = New Rectangle(ClientRectangle.X, _
                      ClientRectangle.Y, ClientRectangle.Width - 18,
                      ClientRectangle.Height)

                g.FillRectangle(New SolidBrush(_BackFillColor), meRect)
                If Not _NullTextInFront Then g.DrawString(_NullText, _
                    New Font(Me.Font.Name, Me.Font.Size, FontStyle.Bold), _
                    New SolidBrush(_NullTextColor), 0, 0)
                g.FillRectangle(New HatchBrush(_NullHatchStyle, _
                    Color.FromArgb(_NullAlpha, _NullColorA), _
                    Color.FromArgb(_NullAlpha, _NullColorB)), meRect)
                If _NullTextInFront Then g.DrawString(_NullText, _
                    New Font(Me.Font.Name, Me.Font.Size, FontStyle.Bold), _
                    New SolidBrush(_NullTextColor), 0, 0)
            End If
        End Using
        Return
    End If
End Sub

To clear the gTimePicker or the gDateTimePicker, use the Delete key, or right-click the control to get a ContextMenu.

Image 8

History

  • Version 1.0: August 2009
  • Version 1.1: August 2009: Fixed the 24 hour format problem
  • Version 1.2: August 2009: Added the AM PM button
  • Version 1.3: August 2009: Threw the Time, TimeAMPM, and HR24 properties out and started over. It was becoming too patch-worky.
  • Version 1.4: September 2009
    • Added the nullable feature binding
    • Added the nullable DateTimePicker
  • Version 1.5: July 2010
    • Added Dropdown and ContextMenu open events
    • Renamed gTextBox, gTimeBox because of a naming conflict
    • Fixed some bugs in the DateTimePicker Nullable feature.
  • Version 1.6: February 2012
    • Removed Redundant property code
    • Right click hour to 00 minutes
    • Added Null button and fixed nullable behavior
    • Replaced Link Numbers with numbers drawn directly on Graphics surface
    • Removed bottom mid-minutes box and added direct minute selection with the mouse
  • Version 1.7: February 2012
    • AM PM Bug-Fix

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)