Share via


VB.NET: International Time & Currency converter

 


Overview

This is an old one written a few years ago, but still a favourite. It's an International Times and major currencies converter. Internally it uses two datatables, one which is currency rates downloaded from the European Central Bank, and the other contains TimeZone information and the dominant currency for each geographical area. These two datatables are used extensively throughout the application.

The application consists of three international timezone clocks, each of which has a ComboBox linked to it allowing changing Timezone (which also changes currency displayed).The clocks can display time for any timezone and also have a Daylight Saving Time indicator.These clocks are analog and show hour, minute, and second, and have an optional alarm which is also displayed in an analog fashion as a red alarm hand.There is a calculator for converting specific amounts from one currency unit to another.

Animating these clocks is achieved through three timers, each with a 250 millisecond interval, which draw the background of the clock (i.e. the legend, timezone region, the date, DST indicator, and a digital representation of the time in 24 hour format), then call the Shared procedures in the clockWorks class - checkAlarm, drawAlarmHand, drawHands. These methods sound the alarm at the appropriate time, and complete the drawing of the clock face.

 


The clockWorks Class

This contains three Shared methods for checking and sounding alarms, and drawing hands on the analog clocks:




      Public Class  clockWorks
                    '' global alarms list      
                    Public Shared  alarms As  New List(Of alarm)  
   
                    Public Shared  clock1AlarmForm As  Form  
                    Public Shared  clock2AlarmForm As  Form  
                    Public Shared  clock3AlarmForm As  Form  
   
                    Public Shared  minutePic As  Bitmap = New  Bitmap(My.Resources.MINUTE1)  
                    Public Shared  secondPic As  Bitmap = New  Bitmap(My.Resources.SECOND1)  
                    Public Shared  hourPic As  Bitmap = New  Bitmap(My.Resources.HOUR1)  
                    Public Shared  centerPic As  Bitmap = New  Bitmap(My.Resources.CENTRE)  
   
                    Public Shared  Sub checkAlarm(ByVal placeName As String, ByVal  code As  String, ByVal alarmTime As TimeSpan, ByVal timeOffset As DateTimeOffset, ByRef clock As Form)  
                        '"hh:mm tt"      
   
                        Dim alarm As alarm = alarms.Where(Function(a) a.placeName = placeName AndAlso  a.code = code).FirstOrDefault  
                        If alarmTime.Hours = timeOffset.Hour AndAlso alarmTime.Minutes = timeOffset.Minute Then  
                            alarms(alarms.FindIndex(      Function      (a) a       Is  alarm)).alarmSounded = True  
                            clock =       New  aFrm1(placeName.Substring(5), timeOffset.ToString("hh:mm tt"))  
                            clock.ShowDialog()      
                        End If  
   
                    End Sub  
   
                    Public Shared  Sub drawAlarmHand(ByVal g As Graphics, ByVal alarmTime As TimeSpan)  
   
                        g.SmoothingMode = Drawing2D.SmoothingMode.HighQuality      
                        g.InterpolationMode = Drawing2D.InterpolationMode.HighQualityBicubic      
   
                        Dim redPen1 As New  Pen(Color.Red, 1)  
                        Dim redPen3 As New  Pen(Color.Red, 3)  
                        Dim redPen5 As New  Pen(Color.Red, 5)  
   
                        Dim angleDegHour As Single  
                        Dim angleRadHour As Single  
                        Dim lineX2 As Single  
                        Dim lineY2 As Single  
   
                        '' the alarm hand is 3 different thickness lines      
                        '' rather than a picture like the other hands      
   
                        angleDegHour =       CSng      ((alarmTime.Hours * 30) + (alarmTime.Minutes * 0.5))      
   
                        'Turn degrees to radians (because of the sin and cos operations)      
                        angleRadHour =       CSng      (Math.PI * angleDegHour / 180)      
                        'Calculate X2 and Y2      
                        lineX2 =       CSng      (96 + Math.Sin(angleRadHour) * 60)      
                        lineY2 =       CSng      (96 - Math.Cos(angleRadHour) * 60)      
   
                        'draw the line (5 pixel thickness, 60 pixels long)      
                        g.DrawLine(redPen5, 96, 96, lineX2, lineY2)      
   
                        'Calculate X2 and Y2      
                        lineX2 =       CSng      (96 + Math.Sin(angleRadHour) * 63)      
                        lineY2 =       CSng      (96 - Math.Cos(angleRadHour) * 63)      
   
                        'draw the line (3 pixel thickness, 63 pixels long)      
                        g.DrawLine(redPen3, 96, 96, lineX2, lineY2)      
   
                        'Calculate X2 and Y2      
                        lineX2 =       CSng      (96 + Math.Sin(angleRadHour) * 65)      
                        lineY2 =       CSng      (96 - Math.Cos(angleRadHour) * 65)      
   
                        'draw the line (1 pixel thickness, 65 pixels long)      
                        g.DrawLine(redPen1, 96, 96, lineX2, lineY2)      
   
                    End Sub  
   
                    Public Shared  Sub drawHands(ByVal g As Graphics, ByVal timeOffset As DateTimeOffset)  
   
                        g.SmoothingMode = Drawing2D.SmoothingMode.HighQuality      
                        g.InterpolationMode = Drawing2D.InterpolationMode.HighQualityBicubic      
   
                        '' draw the hour hand      
                        g.RotateTransform(      CSng      (-90 + ((timeOffset.Hour * 30) + (timeOffset.Minute * 0.5))))      
   
                        g.DrawImage(hourPic, -2, -2)      
                        g.ResetTransform()      
   
   
                        '' draw the minute hand      
                        g.TranslateTransform(96, 96)      
   
                        g.RotateTransform(      CSng      (-90 + (((timeOffset.Minute * 60) + timeOffset.Second) * 0.1)))      
   
                        g.DrawImage(minutePic, -2, -2)      
                        g.ResetTransform()      
   
                        '' draw the second hand      
                        g.TranslateTransform(96, 96)      
                        g.RotateTransform(-90 + (timeOffset.Second * 6))      
                        g.DrawImage(secondPic, -2, -2)      
                        g.ResetTransform()      
   
                    End Sub  
   
   
      End Class

The getCurrencyRates Class

This contains just one Shared function that downloads currency rates and creates and returns that as a Datatable. These currency rates are published daily by the European Central Bank in an online xml file:

Imports <xmlns:alias="http://www.ecb.int/vocabulary/2002-08-01/eurofxref">
 
Public Class  getCurrencyRates
 
    Public Shared  Function getRatesAsDatatable() As DataTable
 
        Dim xml As XDocument = XDocument.Load("http://www.ecb.int/stats/eurofxref/eurofxref-daily.xml")
 
        Dim dt As New  DataTable
 
        Dim items = From node In xml...<alias:Cube>...<alias:Cube>...<alias:Cube> _
                    Select New  With { _
                    .code = node.@currency, _
                    .rate = node.@rate}
 
        dt.Columns.Add("code")
        dt.Columns.Add("EUR")
 
        For Each  i In  items
            dt.Columns.Add(i.code)
        Next
 
        Dim dr As DataRow
 
        Dim firstRow As Boolean  = True
        For Each  i In  items
            Dim columns() As String
            If firstRow Then
                dr = dt.NewRow
                ReDim columns(dt.Columns.Count - 1)
                columns(0) = "EUR"
                columns(1) = (1).ToString
                dr.ItemArray = columns
                dt.Rows.Add(dr)
                firstRow = False
            End If
            ReDim columns(dt.Columns.Count - 1)
            columns(0) = i.code
            columns(1) = i.rate
            dr = dt.NewRow
            dr.ItemArray = columns
            dt.Rows.Add(dr)
        Next
 
        firstRow = True
        For Each  row As  DataRow In  dt.Rows
            If firstRow Then
                dt.Rows(0).Item(dt.Rows(0).Item(0).ToString) = 1
                firstRow = False
            Else
                dt.Rows(0).Item(row.Item(0).ToString) = 1 / CDbl(row.Item(1).ToString)
                row.Item(row.Item(0).ToString) = 1
            End If
        Next
 
        For c As Integer  = 2 To  dt.Columns.Count - 1
            For r As Integer  = 1 To  dt.Rows.Count - 1
                dt.Rows(r).Item(dt.Columns(c).ColumnName) = CDbl(dt.Rows(0).Item(dt.Columns(c).ColumnName)) / CDbl(dt.Rows(0).Item(dt.Rows(r).Item("code").ToString))
            Next
        Next
 
        Return dt
 
    End Function
 
End Class

The timeZones Class

This creates and returns a Datatable containing all international TimeZones:




      Public Class  timeZones
                    Public Shared  Function GetSystemTimeZones() As DataTable  
                        Dim linesByCountry() As String  = My.Resources._2DigitCountryCodes.Split(New String() {Environment.NewLine}, StringSplitOptions.None)  
   
                        Dim dt As New  DataTable  
                        dt.Columns.Add(      "region"      )      
                        dt.Columns.Add(      "tzID"      )      
                        dt.Columns.Add(      "cc"      )      
   
                        Dim dr As DataRow  
   
                        Dim lines() As String  = My.Resources.CCbyCity.Split(New String() {Environment.NewLine}, StringSplitOptions.None)  
   
                        For Each  tz As  TimeZoneInfo In  TimeZoneInfo.GetSystemTimeZones  
                            If Not  tz.ToString.Contains("Coordinated Universal Time") AndAlso  Not tz.ToString.Contains("International Date Line West") Then  
                                Dim parts() As String  = tz.ToString.Split(New String() {") "}, StringSplitOptions.None)  
                                Dim regions() As String  = parts(1).Split(New  String() {", "}, StringSplitOptions.None)  
                                For Each  r As  String In  regions  
                                    If r = "Mid-Atlantic" Then  Continue For  
                                    Dim cc() As String  
                                    Dim line As String  = lines.Where(Function(s) s.StartsWith(r) OrElse  s.Contains(r)).FirstOrDefault  
                                    If line = Nothing Then  
                                        Continue       For      
                                    Else      
                                        cc = line.Split(      ","      c)      
                                    End If  
   
                                    r = linesByCountry.First(      Function      (s) s.Substring(5) = r).Trim      
   
                                    dr = dt.NewRow      
                                    dr.ItemArray =       New  String() {r, tz.Id, If(cc.Count > 0, cc(1), "")}  
                                    dt.Rows.Add(dr)      
                                    If r.Contains("(US & Canada)") Then  
                                        dr = dt.NewRow      
                                        dr.ItemArray =       New  String() {"CA - " & r.Substring(5).Trim, tz.Id, "CAD"}  
                                        dt.Rows.Add(dr)      
                                    End If  
   
                                Next      
                            End If  
                        Next      
   
                        Return dt  
   
                    End Function  
      End Class

Extended ComboBox

The extended ComboBoxes allow country names to be displayed in the ToolStripStatusLabels below the ComboBox as you move the mouse over the dropped down list.
The ComboBox needs to be subclassed to get a Handle to the dropdown window which is used to create a NativeWindow class where the WM_MOUSEMOVE message is used to identify the item below the mousepointer and display the related country name in the ToolStripStatusLabel below the ComboBox.

Imports System.Runtime.InteropServices
 
Public Class  comboboxEx
    Inherits ComboBox
 
    Private Const  WM_CTLCOLORLISTBOX As Integer  = &H134
 
    Dim label As ToolStripStatusLabel
 
    Public Sub  setUpLabels(ByVal  lbl As  ToolStripStatusLabel)
        label = lbl
    End Sub
 
    Protected Overrides  Sub WndProc(ByRef m As System.Windows.Forms.Message)
        MyBase.WndProc(m)
        If m.Msg = WM_CTLCOLORLISTBOX Then
            Dim n As New  nWindow(Me, label)
            n.AssignHandle(m.LParam)
        End If
    End Sub
 
    Protected Overrides  Sub OnDropDownClosed(ByVal e As System.EventArgs)
        label.Text = ""
        MyBase.OnDropDownClosed(e)
    End Sub
 
End Class
 
Public Class  nWindow
    Inherits NativeWindow
 
    Private combo As comboboxEx
    Private countries() As String
    Dim label As ToolStripStatusLabel
 
    Public Sub  New(ByVal cb As comboboxEx, ByVal lbl As ToolStripStatusLabel)
        combo = cb
        countries = My.Resources.countries.Split(New String() {Environment.NewLine}, StringSplitOptions.None)
        label = lbl
    End Sub
 
    Public Declare  Function GetScrollInfo Lib "user32"  Alias "GetScrollInfo"  (ByVal  hWnd As  IntPtr, _
                    ByVal n As Integer, <MarshalAs(UnmanagedType.Struct)> ByRef  lpScrollInfo As  SCROLLINFO) As  Integer
 
    <StructLayout(LayoutKind.Sequential)> _
    Public Structure  SCROLLINFO
        Public cbSize As Integer
        Public fMask As Integer
        Public nMin As Integer
        Public nMax As Integer
        Public nPage As Integer
        Public nPos As Integer
        Public nTrackPos As Integer
    End Structure
 
    Private Const  SB_ENDSCROLL As  Integer = 8
 
    Const SBS_VERT As Integer  = 1
    Const SIF_RANGE As Integer  = 1
    Const SIF_PAGE As Integer  = 2
    Const SIF_POS As Integer  = 4
    Const SIF_TRACKPOS As Integer  = 10
    Const SIF_ALL As Integer  = (SIF_RANGE Or  SIF_PAGE Or  SIF_POS Or  SIF_TRACKPOS)
 
    Private Const  WM_MOUSEMOVE As  Integer = &H200
 
    Private lastIndex As Integer  = -1
 
    Protected Overrides  Sub WndProc(ByRef m As System.Windows.Forms.Message)
        If m.Msg = WM_MOUSEMOVE Then
            Dim itemHeight As Integer  = combo.GetItemHeight(0)
            Dim si As New  SCROLLINFO
            si.fMask = SIF_ALL
            si.cbSize = Marshal.SizeOf(si)
            GetScrollInfo(Me.Handle, SBS_VERT, si)
 
            Dim newIndex As Integer  = si.nPos + (New  Point(m.LParam.ToInt32).Y \ itemHeight)
            If lastIndex <> newIndex And newIndex <= combo.Items.Count - 1 And newIndex >= 0 Then
                lastIndex = newIndex
                Dim newItem As DataRowView = DirectCast(combo.Items(lastIndex), DataRowView)
                label.Text = countries.First(Function(s) s.Substring(0, 2) = newItem.Item(0).ToString.Substring(0, 2)).Substring(5)
            End If
        End If
        MyBase.WndProc(m)
    End Sub
 
End Class

  


Other Resources

 
Full article and download here...



Update Info

Add to resource text files:

City: Pyongyang
Country: North Korea
ICC: KP
Currency code: KPW

18/04/16

Add to My.Resources._2DigitCountryCodes

RU - Kaliningrad
RU - Volgograd
RU - Ekaterinburg
RU - Novosibirsk
RU - Krasnoyarsk
RU - Irkutsk
RU - Yakutsk
RU - Petropavlovsk-Kamchatsky

31/07/16

Add to resource text files:

City: Havana
Country: Cuba
ICC: CU
Currency code: CUP