49

Problem Statement

In VBA, three main kinds of date time controls can be used provided certain ocxs have been registered using administrator rights. These are VB6 controls and are not native to VBA environment. To install the Montview Control and Datetime Picker, we need to set a reference to Microsoft MonthView Control 6.0 (SP4) which can only be accessed by elevated registration of mscomct2.ocx. Similarly for mscal.ocx and mscomctl.ocx. Having said that, the deprecated mscal.ocx may or may not work on Windows 10.

Depending on your Windows and Office versions (32 bit or 64 bit), it can be really painful to register these ocxs.

The Monthview Control, Datetime Picker and the deprecated Calendar control look like below.

enter image description here

So what problem can I face if I include these in my applicaiton?

If you include them in your project and distribute them to your friends, neighbours, clients etc the application may or may not work depending on whether they have those ocx installed.

And hence it is highly advisable NOT to use them in your project

What alternative(s) do I have?

This calendar, using Userform and Worksheet, was suggested earlier and is incredibly basic.

When I saw the Windows 10 calendar which popped up when I clicked on the date and time from the system tray, I could not help but wonder if we can replicate that in VBA.

This post is about how to create a calendar widget which is not dependant on any ocx or 32bit/64bit and can be freely distributed with your project.

This is what the calendar looks like in Windows 10:

enter image description here

and this is how you interact with it:

enter image description here

Jeremy
  • 1
  • 85
  • 340
  • 366
Siddharth Rout
  • 147,039
  • 17
  • 206
  • 250
  • 12
    This question is being discussed on [meta](https://meta.stackoverflow.com/questions/380116/are-broad-seeming-yet-useful-self-answers-allowed) – BrakNicku Feb 12 '19 at 19:18
  • 3
    Do note that late builds of Office 365 ProPlus come with mscomct2.ocx, and the installer registers it for you. It's 64-bits if your Office installation is, and it's compatible with Win 10. – Erik A Feb 13 '19 at 07:49
  • 14
    @ErikA: That's nice but then it emphasizes on what I mentioned in the post above. You cannot distribute the file created in Office 365 which uses `mscomct2.ocx` with a Office 2013/2010/2007 user :) – Siddharth Rout Feb 13 '19 at 08:25

3 Answers3

56

The sample file (added at the end of the post) has a Userform, Module and a Class Module. To incorporate this into your project, simply export the Userform, Module and the Class Module from the sample file and import it into your project.

Class Module Code

In the Class Module (Let's call it CalendarClass) paste this code

Public WithEvents CommandButtonEvents As MSForms.CommandButton

'~~> Unload the form when the user presses Escape
Private Sub CommandButtonEvents_KeyPress(ByVal KeyAscii As MSForms.ReturnInteger)
    If Not f Is Nothing Then If KeyAscii = 27 Then Unload f
End Sub

'~~> This section delas with showing/displaying controls
'~~> and updating different labels
Private Sub CommandButtonEvents_Click()
    f.Label6.Caption = CommandButtonEvents.Tag

    If Left(CommandButtonEvents.Name, 1) = "Y" Then
        If Len(Trim(CommandButtonEvents.Caption)) <> 0 Then
            CurYear = Val(CommandButtonEvents.Caption)                
            With f
                .HideAllControls
                .ShowMonthControls

                .Label4.Caption = CurYear
                .Label5.Caption = 2

                .CommandButton1.Visible = False
                .CommandButton2.Visible = False
            End With
        End If
    ElseIf Left(CommandButtonEvents.Name, 1) = "M" Then
        Select Case UCase(CommandButtonEvents.Caption)
            Case "JAN": CurMonth = 1
            Case "FEB": CurMonth = 2
            Case "MAR": CurMonth = 3
            Case "APR": CurMonth = 4
            Case "MAY": CurMonth = 5
            Case "JUN": CurMonth = 6
            Case "JUL": CurMonth = 7
            Case "AUG": CurMonth = 8
            Case "SEP": CurMonth = 9
            Case "OCT": CurMonth = 10
            Case "NOV": CurMonth = 11
            Case "DEC": CurMonth = 12
        End Select

        f.HideAllControls
        f.ShowSpecificMonth
    End If
End Sub

Module Code

In the Module (Let's call it CalendarModule) paste this code

Option Explicit

Public Const GWL_STYLE = -16
Public Const WS_CAPTION = &HC00000

#If VBA7 Then
    #If Win64 Then
        Public Declare PtrSafe Function GetWindowLongPtr Lib "user32" Alias _
        "GetWindowLongPtrA" (ByVal hwnd As LongPtr, ByVal nIndex As Long) As LongPtr

        Public Declare PtrSafe Function SetWindowLongPtr Lib "user32" Alias _
        "SetWindowLongPtrA" (ByVal hwnd As LongPtr, ByVal nIndex As Long, _
        ByVal dwNewLong As LongPtr) As LongPtr
    #Else
        Public Declare PtrSafe Function GetWindowLongPtr Lib "user32" Alias _
        "GetWindowLongA" (ByVal hwnd As LongPtr, ByVal nIndex As Long) As LongPtr

        Private Declare Function SetWindowLongPtr Lib "user32" Alias _
        "SetWindowLongA" (ByVal hwnd As LongPtr, ByVal nIndex As Long, _
        ByVal dwNewLong As LongPtr) As LongPtr
    #End If

    Public Declare PtrSafe Function DrawMenuBar Lib "user32" _
    (ByVal hwnd As LongPtr) As LongPtr

    Private Declare PtrSafe Function FindWindow Lib "user32" Alias _
    "FindWindowA" (ByVal lpClassName As String, _
    ByVal lpWindowName As String) As LongPtr

    Private Declare PtrSafe Function SetTimer Lib "user32" _
    (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr, _
    ByVal uElapse As LongPtr, ByVal lpTimerFunc As LongPtr) As LongPtr

    Public Declare PtrSafe Function KillTimer Lib "user32" _
    (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr) As LongPtr

    Public TimerID As LongPtr

    Dim lngWindow As LongPtr, lFrmHdl As LongPtr
#Else

    Public Declare Function GetWindowLong _
    Lib "user32" Alias "GetWindowLongA" ( _
    ByVal hwnd As Long, ByVal nIndex As Long) As Long

    Public Declare Function SetWindowLong _
    Lib "user32" Alias "SetWindowLongA" ( _
    ByVal hwnd As Long, ByVal nIndex As Long, _
    ByVal dwNewLong As Long) As Long

    Public Declare Function DrawMenuBar _
    Lib "user32" (ByVal hwnd As Long) As Long

    Public Declare Function FindWindowA _
    Lib "user32" (ByVal lpClassName As String, _
    ByVal lpWindowName As String) As Long

    Public Declare Function SetTimer Lib "user32" ( _
    ByVal hwnd As Long, ByVal nIDEvent As Long, _
    ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long

    Public Declare Function KillTimer Lib "user32" ( _
    ByVal hwnd As Long, ByVal nIDEvent As Long) As Long

    Public TimerID As Long
    Dim lngWindow As Long, lFrmHdl As Long
#End If

Public TimerSeconds As Single, tim As Boolean
Public CurMonth As Integer, CurYear As Integer
Public frmYr As Integer, ToYr As Integer

Public f As frmCalendar

Enum CalendarThemes
    Venom = 0
    MartianRed = 1
    ArcticBlue = 2
    Greyscale = 3
End Enum

Sub Launch()
    Set f = frmCalendar

    With f
        .Caltheme = Greyscale
        .LongDateFormat = "dddd dd. mmmm yyyy" '"dddd mmmm dd, yyyy" etc
        .ShortDateFormat = "dd/mm/yyyy"  '"mm/dd/yyyy" or "d/m/y" etc
        .Show
    End With
End Sub

'~~> Hide the title bar of the userform
Sub HideTitleBar(frm As Object)
    #If VBA7 Then
        Dim lngWindow As LongPtr, lFrmHdl As LongPtr
        lFrmHdl = FindWindow(vbNullString, frm.Caption)
        lngWindow = GetWindowLongPtr(lFrmHdl, GWL_STYLE)
        lngWindow = lngWindow And (Not WS_CAPTION)
        Call SetWindowLongPtr(lFrmHdl, GWL_STYLE, lngWindow)
        Call DrawMenuBar(lFrmHdl)
    #Else
        Dim lngWindow As Long, lFrmHdl As Long
        lFrmHdl = FindWindow(vbNullString, frm.Caption)
        lngWindow = GetWindowLong(lFrmHdl, GWL_STYLE)
        lngWindow = lngWindow And (Not WS_CAPTION)
        Call SetWindowLong(lFrmHdl, GWL_STYLE, lngWindow)
        Call DrawMenuBar(lFrmHdl)
    #End If
End Sub

'~~> Start Timer
Sub StartTimer()
    '~~ Set the timer for 1 second
    TimerSeconds = 1
    TimerID = SetTimer(0&, 0&, TimerSeconds * 1000&, AddressOf TimerProc)
End Sub

'~~> End Timer
Sub EndTimer()
    On Error Resume Next
    KillTimer 0&, TimerID
End Sub

'~~> Update Time
#If VBA7 And Win64 Then ' 64 bit Excel under 64-bit windows  ' Use LongLong and LongPtr
    Public Sub TimerProc(ByVal hwnd As LongPtr, ByVal uMsg As LongLong, _
    ByVal nIDEvent As LongPtr, ByVal dwTimer As LongLong)
        frmCalendar.Label1.Caption = Split(Format(Time, "h:mm:ss AM/PM"))(0)
        frmCalendar.Label2.Caption = Split(Format(Time, "h:mm:ss AM/PM"))(1)
    End Sub
#ElseIf VBA7 Then ' 64 bit Excel in all environments
    Public Sub TimerProc(ByVal hwnd As LongPtr, ByVal uMsg As Long, _
    ByVal nIDEvent As LongPtr, ByVal dwTimer As Long)
        frmCalendar.Label1.Caption = Split(Format(Time, "h:mm:ss AM/PM"))(0)
        frmCalendar.Label2.Caption = Split(Format(Time, "h:mm:ss AM/PM"))(1)
    End Sub
#Else ' 32 bit Excel
    Public Sub TimerProc(ByVal hwnd As Long, ByVal uMsg As Long, _
    ByVal nIDEvent As Long, ByVal dwTimer As Long)
        frmCalendar.Label1.Caption = Split(Format(Time, "h:mm:ss AM/PM"))(0)
        frmCalendar.Label2.Caption = Split(Format(Time, "h:mm:ss AM/PM"))(1)
    End Sub
#End If

'~~> Improvement suggested by T.M (https://stackoverflow.com/users/6460297/t-m)
'(1) Get weekday name
Function wday(ByVal wd&, ByVal lang As String) As String
    ' Purpose: get weekday in "DDD" format
    wday = Application.Text(DateSerial(6, 1, wd), cPattern(lang) & "ddd")    ' the first day in year 1906 starts with a Sunday
End Function

'~~> Improvement suggested by T.M (https://stackoverflow.com/users/6460297/t-m)
'(2) Get month name
Function mon(ByVal mo&, ByVal lang As String) As String
    ' Example call: mon(12, "1031") or mon(12, "de")
    mon = Application.Text(DateSerial(6, mo, 1), cPattern(lang) & "mmm")
End Function

'~~> Improvement suggested by T.M (https://stackoverflow.com/users/6460297/t-m)
'(3) International patterns
Function cPattern(ByVal ctry As String) As String
    ' Purpose: return country code pattern for above functions mon() and wday()
    ' Codes: see https://msdn.microsoft.com/en-us/library/dd318693(VS.85).aspx
    ctry = LCase(Trim(ctry))
    Select Case ctry
        Case "1033", "en-us": cPattern = "[$-409]" ' English (US)
        Case "1031", "de": cPattern = "[$-C07]" ' German
        Case "1034", "es": cPattern = "[$-C0A]" ' Spanish
        Case "1036", "fr": cPattern = "[$-80C]" ' French
        Case "1040", "it": cPattern = "[$-410]" ' Italian
        ' more ...
    End Select
End Function

Userform Code

The Userform (Let's call it frmCalendar) code is too big to be posted here. Please refer to the sample file.

Screenshot

enter image description here

Themes

enter image description here

Highlights

  1. No need to register any dll/ocx.
  2. Easily distributable. It is FREE.
  3. No Administratior Rights required to use this.
  4. You can select a skin for the calendar widget. One can choose from 4 themes Venom, MartianRed, ArticBlue and GreyScale.
  5. Choose Language to see Month/Day name. Support for 4 languages.
  6. Specify Long and Short date formats

Sample File

Sample File

Acknowlegements @Pᴇʜ, @chrisneilsen and @T.M. for suggesting improvements.

What's New:

Bugs reported by @RobinAipperspach and @Jose fixed

Siddharth Rout
  • 147,039
  • 17
  • 206
  • 250
  • 2
    Some of your `PrtSave` declarations are wrong and don't work. Some of the `Long`s have to be converted to `LongPtr` (actually all the pointers, but not the rest of the `Long`!). Check it up at http://www.cadsharp.com/docs/Win32API_PtrSafe.txt. • Idea: Push it to github maybe? You could checkin the exported userform/module files so it could be easily forked. Nice work :) – Pᴇʜ Feb 12 '19 at 13:00
  • 1
    Thanks. :) Do not have 64 bit excel so couldn't check it. Will try and get my hands on one or if you have then you can check it for me? – Siddharth Rout Feb 12 '19 at 13:19
  • @SiddharthRout Added it as an answer. You should test if 32bit still works since I had to change some variable declarations to `LongPtr` too. – Pᴇʜ Feb 12 '19 at 13:48
  • @SiddharthRout I don't know if this is a 64bit related issue but I get a force close eg. if I click on `Jan` but it works eg. if I click on `Apr` (reproducible). I'm also on a german locale so it might be a possible issue too. I think something is still wrong but I didn't find it. – Pᴇʜ Feb 12 '19 at 14:31
  • @Pᴇʜ: Did you change just the `#If VBA7 Then` part or something else as well? – Siddharth Rout Feb 12 '19 at 14:35
  • @SiddharthRout also `HideTitleBar` and `TimerProc` as their code needed to adapt to the different declarations, as well as the `TimerID` variable – Pᴇʜ Feb 12 '19 at 14:37
  • @Pᴇʜ: also reverifying it with https://www.jkp-ads.com/articles/apideclarations.asp – Siddharth Rout Feb 12 '19 at 14:41
  • @Sid. Firstly, thanks for posting this. This is the sort of content that makes SO useful. Couple of suggestions: 1) while I understand why you removed the form titlebar, this has the side effect of making it immovable. Ok for a popup from the system tray, but not so great from a vba app point of view. Could this be optional? 2) the selected date display at the bottom of the form format could be configurable, or inherited from user settings. 3) cosmetic, but the W10 calender without boarders around the buttons looks much better (might be too hard to replicate with the available controls) – chris neilsen Feb 13 '19 at 09:35
  • 3
    Thanks for your suggestions. Will include 1) Form Movable without Title bar 2) Date display as per user choice in Version 3. Regarding the 3rd point, I believe and I could be wrong, borderless commandbuttons in VBA are not possible. I thought of using FindWindow API but then the VBA controls except the userform do not have hwnd (handle). I can use images but then in absense of a mouse, it may be difficult tabbing through them. @chrisneilsen. – Siddharth Rout Feb 13 '19 at 09:43
  • Extremely helpful solution:-) - Regarding Chris' proposition, you might be interested in a recent answer demonstrating a simple drag functionality in forms with hidden title bar at [Right to left userforms in Excel VBA](https://stackoverflow.com/questions/52781069/right-to-left-userforms-in-excel-vba/52806751#52806751) including further hints to API calls. @SiddharthRout - – T.M. Feb 13 '19 at 18:42
  • Friendly hints: a) seems you reversed the `CommandButton1/2` Up/Down logic; b) suggest localized month abbrevations, too - @SiddharthRout – T.M. Feb 13 '19 at 20:07
  • 3
    Oh BTW @T.M. You do not need APIs to move a borderless/titleless form :) `UserForm_MouseDown` with `UserForm_MouseMove` will take care of it :) – Siddharth Rout Feb 14 '19 at 05:19
  • 1
    @Pᴇʜ What does `Format(Date, "dddd mmmm dd, yyyy")` in your system return in immediate window? – Siddharth Rout Feb 14 '19 at 07:44
  • 3
    @SiddharthRout `Format$(Date, "dddd mmmm dd, yyyy")` returns `Donnerstag Februar 14, 2019` But actually germans would prefer `Donnerstag 14. Februar 2019` – Pᴇʜ Feb 14 '19 at 07:45
  • 1
    @chrisneilsen: Version 3 Uploaded. Thanks for the inputs – Siddharth Rout Feb 14 '19 at 08:24
  • 1
    @Pᴇʜ Version 3 Uploaded. Thanks for the inputs – Siddharth Rout Feb 14 '19 at 08:24
  • Further hints: Added a simplified `ChangeLanguage` procedure, as manual array maintenance within `CalendarModule.LanguageTranslations` shows some false names in *May* (LCID 1031 Mai, 1034 may (mayo), 1040 mag (maggio), 1036 mai). BTW it's sort of confusing that the **up arrow** changes to prior dates and *vice versa*. – T.M. Feb 14 '19 at 11:54
  • 3
    Thanks @T.M.: I will incorporate them in Ver4. The up Arrow takes you to prev month as it does in the win 10 calendar – Siddharth Rout Feb 14 '19 at 11:57
  • @SiddharthRout There is an issue on the Class Module, because the months in Select Case are in English. So if you change the language, the calendar won't work properly when you choose a month. – danieltakeshi Jun 12 '19 at 19:47
  • 2
    @danieltakeshi: It should support the 4 languages that I mentioned above. For other languages, you will have to tweak it. :) – Siddharth Rout Jun 14 '19 at 11:56
  • @SiddharthRout noticed that when I click on 03/01/2019 it outputs 04/05/2019. Not sure if this is a glitch? – PP8 Jul 10 '19 at 01:51
  • @Jose: Fixed. Thanks – Siddharth Rout Jul 10 '19 at 04:11
  • @RobinAipperspach. Fixed – Siddharth Rout Jul 10 '19 at 04:11
6

This is my first post here. I felt compelled to share as the loss of the calendar in Excel was a huge deal and this calendar SiddhartRout created is incredible. So, MANY thanks to @SiddhartRout for putting together this really amazing calendar. I made changes to the cosmetics but most of the underlying meat of it is still all Siddhart's work with some minor changes to meet my use case.

Cosmetic changes:

  • Replaced ALL of the buttons with borderless labels so it looks a lot more like the Windows 10 calendar
  • The border of the labels will appear/disappear on mouse enter/exit
  • I grayed out days that aren't for the current month. The 'gray out' is a different color that matches better for each theme.
  • Modified the theme colors to my liking. Added a label to click for cycling through the themes.
  • Changed the font to Calibri
  • added color change on mouse entry to month/year and arrow controls
  • Use this site for all of you color code needs --> RGB Color Codes

Code Changes

  • Optimized the Property Let Caltheme making it easier to setup and add theme colors or entirely new themes
  • I couldn't get the 'ESC to exit' to work reliably so i replaced it with an 'X'. It stopped crashing as much as well.
  • Removed the localization stuff as i'll never need it
  • Changing from buttons to labels required modifying some object variables where needed throughout the project
  • Added public variables used to store RGB values allowing use of theme colors throughout the project providing for more consistent and easier application of selected theme
  • User selected theme stored in the hidden sheet so it's persistent between runs
  • Removed the checkmark button & launch directly from a click on any day.

Screenshots of each theme:

Venom 2 MartianRed 2
ArcticBlue 2 GreyScale 2

Download link for code:

logicworkz
  • 79
  • 1
  • 3
  • 1
    I would recommend zipping all the files and then attaching the link here or better still leave them in the excel file and attach the link to the excel file here so that people can diretly test the Excel file. Good Work! – Siddharth Rout Dec 19 '19 at 16:56
  • Good idea. Thx! – logicworkz Dec 19 '19 at 18:53
  • 1
    New version uploaded. Dealt with a bug that was allowing the the days of the current month to be grayed out as if they belonged to the previous month. – logicworkz Jan 06 '20 at 18:18
  • I love this, but it was a bit tricky for me to actually "use" the code. I made some adjustments and posted on my site for my own referece, but perhaps you could take a look and upload a newer/better version w/ the ideas of my adjustments such as, using debug.print vs msgbox, unload form after date picked, and example call sub utilizing a public date variable. Thanks soo much tho everybody for a proper portable solution with a beautiful UI. See https://www.freesoftwareservers.com/display/FREES/Date-Picker+-+UserForm+-+Excel+VBA – FreeSoftwareServers Jan 22 '21 at 20:38
  • @logicworkz one thing I thought of straight away is how can I setup a default date, basically I'd like to have it set to 90 days ago as the "selected date", but allow user to override w/ date-picker. – FreeSoftwareServers Jan 22 '21 at 20:43
  • once a user chooses a date how do I get that to populate a textbox? It just show a pop up msgbox that I want to delete – vanilla_skies Nov 11 '21 at 21:51
5

Get international day & month names

This answer is intended to be helpful to Sid's approach regarding internationalization; so it doesn't repeat the other code parts which I consider to be clear enough building a UserForm. If wanted, I can delete it after incorporation in Vers. 4.0.

Just in addition to Sid's valid solution I demonstrate a simplified code to get international weekday and month names - c.f. Dynamically display weekday names in native Excel language

Modified ChangeLanguage procedure in form's module frmCalendar

Sub ChangeLanguage(ByVal LCID As Long)
    Dim i&
    '~~> Week Day Name
     For i = 1 To 7
         Me.Controls("WD" & i).Caption = Left(wday(i, LCID), 2)
     Next i
    '~~> Month Name
     For i = 1 To 12
         Me.Controls("M" & i).Caption = Left(mon(i, LCID), 3)
     Next i
End Sub

Called Functions in CalendarModule

These three functions could replace the LanguageTranslations() function. Advantage: short code, less memory, easier maintenance, correct names

'(1) Get weekday name
Function wday(ByVal wd&, ByVal lang As String) As String
' Purpose: get weekday in "DDD" format
  wday = Application.Text(DateSerial(6, 1, wd), cPattern(lang) & "ddd")    ' the first day in year 1906 starts with a Sunday
End Function

'(2) Get month name
Function mon(ByVal mo&, ByVal lang As String) As String
' Example call: mon(12, "1031") or mon(12, "de")
  mon = Application.Text(DateSerial(6, mo, 1), cPattern(lang) & "mmm")
End Function

'(3) International patterns
Function cPattern(ByVal ctry As String) As String
' Purpose: return country code pattern for above functions mon() and wday()
' Codes: see https://msdn.microsoft.com/en-us/library/dd318693(VS.85).aspx
ctry = lcase(trim(ctry))
Select Case ctry
  Case "1033", "en-us"
    cPattern = "[$-409]" ' English (US)
  Case "1031", "de"
    cPattern = "[$-C07]" ' German
  Case "1034", "es"
    cPattern = "[$-C0A]" ' Spanish
  Case "1036", "fr"
    cPattern = "[$-80C]" ' French
  Case "1040", "it"
    cPattern = "[$-410]" ' Italian
' more ...
End Select
End Function
T.M.
  • 9,436
  • 3
  • 33
  • 57
  • 5
    I have incorporated your suggestions. I am not planning to update that post anymore. Thanks for all your suggesitons. Much appreciated. – Siddharth Rout Feb 18 '19 at 06:56