Windows PowerShell Tip of the Week

Here’s a quick tip on working with Windows PowerShell. These are published every week for as long as we can come up with new tips. If you have a tip you’d like us to share or a question about how to do something, let us know.

Find more tips in the Windows PowerShell Tip of the Week archive.

Creating a Graphical Date Picker

You know, nearly everyone has a weakness of some kind. For Superman, it’s Kryptonite; exposure to Kryptonite leaves the Man of Steel as weak as a kitten. For Wonder Woman, it’s her own magic lasso; if you can somehow manage to tie her up with her own lasso she becomes completely helpless. The Green Lantern is powerless against anything colored yellow. The Scripting Guys – well, for the Scripting Guys it’s also Kryptonite. And things colored yellow. But in addition to that, the Scripting Guys have another weakness: scripts that require users to enter dates.

It’s not that writing these scripts is hard, it’s just that it’s difficult to control the format users use when entering a date. For example, suppose a user enters a date like this:

3/11/2008

That seems straightforward enough: it’s obviously March 11, 2008.

Well, unless it’s November 3, 2008; after all, in many parts of the world the standard date format is this:

day/month/year

How do you know which one was meant here? You don’t.

And there are the dates that aren’t actually dates; what do you do with a date like April 31, 2008? And the people who have trouble spelling; we might know what a user means by Febuary 3, 2008 but the computer won’t. And, of course, there are – well, you get the idea. Any time you ask people to enter dates at the command prompt you’re asking for trouble.

The solution to this problem is actually simple: pop up a calendar and have users select a date from the calendar. That eliminates the problem of formatting; the computer will translate the date they picked into the correct format. That also eliminates the problem of selecting a non-existent date such as April 31st, along with the problem of misspelling the name of the month. In fact there’s just one problem that this doesn’t eliminate: there’s no way to pop up a calendar in Windows PowerShell.

Which means that the only thing we can do is – say, wait a minute. Who says there’s no way to popup a calendar in Windows PowerShell:

[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")

$objForm = New-Object Windows.Forms.Form

$objForm.Text = "Select a Date"
$objForm.Size = New-Object Drawing.Size @(190,190)
$objForm.StartPosition = "CenterScreen"

$objForm.KeyPreview = $True

$objForm.Add_KeyDown({
    if ($_.KeyCode -eq "Enter")
        {
            $dtmDate=$objCalendar.SelectionStart
            $objForm.Close()
        }
    })

$objForm.Add_KeyDown({
    if ($_.KeyCode -eq "Escape")
        {
            $objForm.Close()
        }
    })

$objCalendar = New-Object System.Windows.Forms.MonthCalendar
$objCalendar.ShowTodayCircle = $False
$objCalendar.MaxSelectionCount = 1
$objForm.Controls.Add($objCalendar)

$objForm.Topmost = $True

$objForm.Add_Shown({$objForm.Activate()})
[void] $objForm.ShowDialog()

if ($dtmDate)
    {
        Write-Host "Date selected: $dtmDate"
    }

What exactly is this, and why are we showing it to you? Well, let’s walk through the code and see if our explanation helps to answer your questions.

To begin with, we need to load the .NET Framework class System.Windows.Forms; that’s what this line of code does:

[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")

Don’t worry too much about the details there; just use the line of code as-is and everything should be fine. We will note that the [void] prevents class information from being displayed onscreen when System.Windows.Forms gets loaded. If we left [void] out we’d end up getting this message when we execute the LoadWithPartialName method:

GAC    Version        Location
---    -------        --------
True   v2.0.50727     C:\WINDOWS\assembly\GAC_MSIL\System.Windows.Forms\2.0.0.0__b77a5c561934e089\...

This probably falls under the category of “More information than we really need.” (Or really care about.) We then use the exact same approach to create an instance of the System.Drawing class:

[System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")

As soon as the two classes are loaded we use the New-Object cmdlet to create a new instance of the Forms class. (We need an instance of the Forms class because we have to have a container in which to place our calendar control.) After creating the blank form (using the object reference $objForm) we use this block of code to assign the form a caption (Text) and a size (190 pixels by 190 pixels), and to ensure that each time the form appears it will be centered on screen:

$objForm.Text = "Select a Date"
$objForm.Size = New-Object Drawing.Size @(190,190)
$objForm.StartPosition = "CenterScreen"

It’s nice that we can make a form appear on the screen. What would be even nicer is the ability to also make that form disappear from the screen; after all, we need to be able to get rid of the form either because: a) we don’t want to pick a date after all; or, b) we’ve picked a date and now want to continue on and actually do something with that date. In order to keep things as simple as possible, we decided to limit our form’s user interface to two things: pressing the ENTER key will serve the same purpose as clicking an OK button, and pressing the ESC key will serve the same purpose as pressing a Cancel button.

Note. Could we actually have an OK button and a Cancel button on our form? Sure; for more information, see our previous Windows PowerShell Tip of the Week.

In order to use keyboard commands like ENTER and ESC, the first thing we need to do is set the form’s KeyPreview property to True; that’s what we do with this line of code:

$objForm.KeyPreview = $True

We then need to define the actions to be taken if the user presses the ENTER key:

$objForm.Add_KeyDown({
    if ($_.KeyCode -eq "Enter")
        {
            $dtmDate=$objCalendar.SelectionStart
            $objForm.Close()
        }
    })

Here we’re using the Add_KeyDown method to tell the form what to do if the user presses the ENTER key (that is, if the Keycode of the key that was just pressed is equal to Enter). If the user presses ENTER we want to do two things:

First, we take the value of the calendar’s SelectionStart property and store it in a variable named $dtmDate. That’s simply a fancy way of saying we’re going to grab the date that the user selected and store it in the variable $dtmDate.

Second, we’re going to call the Close method and dismiss the form. We then define the actions to be taken if the user presses ESC:

$objForm.Add_KeyDown({
    if ($_.KeyCode -eq "Escape")
        {
            $objForm.Close()
        }
    })

Notice that, in this case, we aren’t going to grab the date that the user selected; that’s because, by pressing ESC, the user is telling us that he or she doesn’t want to select a date. Instead, all we’re going to do is close the form.

At this point we’re ready to do what we came here for in the first place: add a calendar control to our form. To do that, we start off by creating an instance of the System.Windows.Forms.MonthCalendar class. We then use these two lines of code to configure two properties of the calendar:

$objCalendar.ShowTodayCircle = $False
$objCalendar.MaxSelectionCount = 1

We set the ShowTodayCircle property to False for one simple reason: we don’t like the way the calendar looks when this value is True. (What difference does it make? Try the script with ShowTodayCircle set to $False, then try it again with ShowTodayCircle set to $True and see for yourself). We then set the MaxSelectionCount property to 1; that restricts the user to selecting a single date in the calendar.

Note. Does that mean you can select more than one date? As a matter of fact it does: you can actually select a range of dates. To do that, set the MaxSelectionCount property to the maximum interval allowed between the starting date and ending date. For example, if you want to allow no more than 10 days between the two dates then set MaxSelectionCount to 10. After that, use the SelectionStart property to determine the starting date and the SelectionEnd property to determine the ending date.

After configuring the property values we use the Add method to add the calendar control to the form:

$objForm.Controls.Add($objCalendar)

From there we set the TopMost property; this ensures that when we show the form the form will appear on top of any other windows on the screen:

$objForm.Topmost = $True

And then we use these two lines of code to actually bring up our calendar:

$objForm.Add_Shown({$objForm.Activate()})
[void] $objForm.ShowDialog()

The calendar should look a little something like this:

Incidentally, our calendar shows only one month at a time because that’s all we wanted it to show. What if we wanted to see, say, four months at a time? Well, in that case, we need to make two modifications to the script. First, we need to increase the size of our form:

$objForm.Size = New-Object Drawing.Size @(370,330)

Second, we need to set the CalendarDimensions property to some combination of four; for example, this command displays two calendars across and two calendars down:

$objCalendar.CalendarDimensions = New-Object Drawing.Size @(2,2)

That’s going to result in a calendar that looks like this:

If you’d prefer to start the calendar with the current month (March, 2008) then set the MinDate property like so:

$objCalendar.MinDate = Get-Date

Etc., etc., etc.

Note. For a complete list of calendar properties and methods, see the .NET Framework SDK.

After the user chooses a date and presses ENTER all we have to do is determine – and echo back – the selected date:

if ($dtmDate)
    {
        Write-Host "Date selected: $dtmDate"
    }

Here we’re using an if statement to determine whether or not $dtmDate has a value; if it doesn’t, that means the user pressed ESC. If it does have a value, then we simply echo back that value, like so:

03/18/2008 00:00:00

If you don’t like that formatting, well, simply use a formatting that you do like. Take these two lines of code:

$dtmDateLong = $dtmDate.ToLongDateString()
$dtmDateShort = $dtmDate.ToShortDateString()

The first line converts $dtmDate to the following format:

Thursday, March 13, 2008

Meanwhile, the second line gives us this kind of formatting:

3/13/2008

It’s entirely up to you.

We could go on and on, but we’re out of time for this week. But don’t worry; we know there is a lot of interest in creating GUI applets (like a date picker) using Windows PowerShell. Therefore, we’re working on a whole bunch of samples that will show you how to pop up dialog boxes that use everything from check boxes to radio buttons to list views. Stay tuned!