Microsoft Scripting Guy Ed Wilson here. It has been a great week with some excellent guest bloggers. It is not that I took the week off; rather, I have spent the week working on some special projects I wanted to complete. The great thing about working with guest bloggers is that I get to tap into world-class writers who are experts in their field. This week we have had some excellent SharePoint articles and SQL server articles. In addition to reading and trying out the commands from the articles, I decided to take some time to work on a really cool script I have been wanting to write for some time.
The New-TeaTimer.ps1 script falls into the really cool category. The complete code is seen here.
New-TeaTimer.ps1 function new-teatimer { Param([int]$seconds = 240) Start-Sleep -Seconds $seconds } #end function new-teatimer function New-BalloonTip { Param( [string]$Title = "tea is ready", [string]$Text = "get your tea", [int]$Timeout = 4000, [int]$eventTimeout = 5 ) Add-Type -AssemblyName system.windows.forms $toast = New-Object system.windows.forms.notifyicon $toast.icon = [system.drawing.systemicons]::information $toast.balloonTipTitle = $Title $toast.visible = $true $toast.BalloonTipText = $Text $toast.ShowBalloonTip($Timeout) Register-ObjectEvent -InputObject $toast -EventName BalloonTipClicked ` -SourceIdentifier ballon_clicked if(Wait-Event -Timeout $eventTimeout -SourceIdentifier ballon_clicked) { Remove-Event -SourceIdentifier ballon_clicked Unregister-Event -SourceIdentifier ballon_clicked $toast.Dispose() } else { Unregister-Event -SourceIdentifier ballon_clicked $toast.Dispose() } } #end function new-balloontip # *** Entry Point to script *** $st = Get-Date new-teatimer $et = Get-Date New-BalloonTip (New-TimeSpan -Start $st -End $et).totalSeconds
New-TeaTimer.ps1
function new-teatimer { Param([int]$seconds = 240) Start-Sleep -Seconds $seconds } #end function new-teatimer function New-BalloonTip { Param( [string]$Title = "tea is ready", [string]$Text = "get your tea", [int]$Timeout = 4000, [int]$eventTimeout = 5 ) Add-Type -AssemblyName system.windows.forms $toast = New-Object system.windows.forms.notifyicon $toast.icon = [system.drawing.systemicons]::information $toast.balloonTipTitle = $Title $toast.visible = $true $toast.BalloonTipText = $Text $toast.ShowBalloonTip($Timeout) Register-ObjectEvent -InputObject $toast -EventName BalloonTipClicked ` -SourceIdentifier ballon_clicked if(Wait-Event -Timeout $eventTimeout -SourceIdentifier ballon_clicked) { Remove-Event -SourceIdentifier ballon_clicked Unregister-Event -SourceIdentifier ballon_clicked $toast.Dispose() } else { Unregister-Event -SourceIdentifier ballon_clicked $toast.Dispose() } } #end function new-balloontip # *** Entry Point to script *** $st = Get-Date new-teatimer $et = Get-Date New-BalloonTip (New-TimeSpan -Start $st -End $et).totalSeconds
The New-TeaTimer.ps1 script begins with a function called new-teatimer. This function basically calls the Start-Sleep cmdlet. The reason for creating a separate function is because most of the time I let my tea steep for four minutes. Therefore, what I have is a timer that defaults to four minutes. To time my tea, all I need to do is to call the new-teatimer function. I do not need to calculate how many seconds are in four minutes; I just call the function and leave it at that. The new-teatimer function is shown here:
function new-teatimer { Param([int]$seconds = 240) Start-Sleep -Seconds $seconds } #end function new-teatimer
The first thing that is done in the New-BalloonTip function is to define a number of parameters. The title is the bold portion of the balloon, and the test parameter governs the text that is displayed in the body of the balloon. The timeout governs how long the balloon displays before disappearing, and the eventtimeout controls how long the script will wait for the event to timeout. This portion of the script is shown here:
function New-BalloonTip { Param( [string]$Title = "tea is ready", [string]$Text = "get your tea", [int]$Timeout = 4000, [int]$eventTimeout = 5 )
The forms assembly from the system.windows .NET Framework namespace is not loaded by default. The easiest way to load the assembly is to use the Add-Type cmdlet, and specify the assembly name. The New-Object cmdlet is used to create a new instance of the notifyicon class—the class that makes the popup toast that appears in the message area. The icon used by the notifyicon .NET Framework class is selected from the systemicons class that is found in the system.drawing .NET Framework namespace. Each icon in the systemicons class is a static property from this class. This is shown here:
PS C:\> Add-Type -AssemblyName system.windows.forms PS C:\> [system.drawing.systemicons] | Get-Member -MemberType property –Static TypeName: System.Drawing.SystemIcons Name MemberType Definition ---- ---------- ---------- Application Property static System.Drawing.Icon Application {get;} Asterisk Property static System.Drawing.Icon Asterisk {get;} Error Property static System.Drawing.Icon Error {get;} Exclamation Property static System.Drawing.Icon Exclamation {get;} Hand Property static System.Drawing.Icon Hand {get;} Information Property static System.Drawing.Icon Information {get;} Question Property static System.Drawing.Icon Question {get;} Shield Property static System.Drawing.Icon Shield {get;} Warning Property static System.Drawing.Icon Warning {get;} WinLogo Property static System.Drawing.Icon WinLogo {get;} PS C:\>
PS C:\> Add-Type -AssemblyName system.windows.forms PS C:\> [system.drawing.systemicons] | Get-Member -MemberType property –Static
TypeName: System.Drawing.SystemIcons Name MemberType Definition ---- ---------- ---------- Application Property static System.Drawing.Icon Application {get;} Asterisk Property static System.Drawing.Icon Asterisk {get;} Error Property static System.Drawing.Icon Error {get;} Exclamation Property static System.Drawing.Icon Exclamation {get;} Hand Property static System.Drawing.Icon Hand {get;} Information Property static System.Drawing.Icon Information {get;} Question Property static System.Drawing.Icon Question {get;} Shield Property static System.Drawing.Icon Shield {get;} Warning Property static System.Drawing.Icon Warning {get;} WinLogo Property static System.Drawing.Icon WinLogo {get;}
PS C:\>
The remaining lines of code set properties that have names that are relatively self-explanatory. This portion of the function is shown here:
Add-Type -AssemblyName system.windows.forms $toast = New-Object system.windows.forms.notifyicon $toast.icon = [system.drawing.systemicons]::information $toast.balloonTipTitle = $Title $toast.visible = $true $toast.BalloonTipText = $Text $toast.ShowBalloonTip($Timeout)
To receive an event when the balloon is clicked, use the Register-ObjectEvent cmdlet. The BalloonTipClicked event occurs when the user clicks the balloon. Other NotifyIcon events are available and would be wired up in the same manner as the BalloonTipClicked event. The sourceidentifier property value is used to track the event, and is a string that can be anything I wish to use. The input object is the new notifyicon instance that is stored in the $toast variable. This portion of the script is shown here:
Register-ObjectEvent -InputObject $toast -EventName BalloonTipClicked ` -SourceIdentifier ballon_clicked
If an event is generated by clicking the balloon, I want to remove the event from the event queue and unregister the event. When I have completed those two actions, I wish to call the dispose method of the notifyicon class to cause the balloon to disappear. This portion of the code is shown here:
if(Wait-Event -Timeout $eventTimeout -SourceIdentifier ballon_clicked)
{ Remove-Event -SourceIdentifier ballon_clicked Unregister-Event -SourceIdentifier ballon_clicked $toast.Dispose() }
{
Remove-Event -SourceIdentifier ballon_clicked
Unregister-Event -SourceIdentifier ballon_clicked
$toast.Dispose()
}
On the other hand, it is entirely possible that the event may time out, or the balloon could be closed out by clicking the “x” in the upper right hand corner of the balloon. If this should happen, the close BalloonTipClosed event is generated, and not the BalloonTipClicked event. Therefore, to get rid of the balloon, it is necessary to unregister the event and call the dispose method. This portion of the function is shown here:
else { Unregister-Event -SourceIdentifier ballon_clicked $toast.Dispose() } } #end function new-balloontip
else
} #end function new-balloontip
The entry point to the script gets a datetime object and stores it in a variable named $st. It then calls the new-teatimer function. It then calls the Get-Date cmdlet once again and stores a datetime object in the $et variable. These two timestamps are called in order to verify that the new-teatimer is working properly. Because tea gets bitter if it is allowed to steep too long, it is important to see how well the new-teatimer function is working. On my computer, the value between the desired timer and the actual timer was milliseconds, which is good enough for a cup of Darjeeling tea. The New-BalloonTip function is called, and finally the total number of seconds from the timespan object that is used to compare the two datetime values is queried from the New-TimeSpan cmdlet. This portion of the script is shown here:
$st = Get-Date new-teatimer $et = Get-Date New-BalloonTip (New-TimeSpan -Start $st -End $et).totalSeconds
When the script runs, the balloon shown in the following image appears in the notification area.
Join me tomorrow for another Weekend Scripter article. I would love for you to follow us on Twitter and Facebook. If you have any questions, send email to us at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson and Craig Liebendorfer, Scripting Guys
Why use sleep? PowerShell can do this asynchronously. Aren't you guys supposed to promote PowerShell's features and take a leap forward from old style polling?
@Robert -
I saw your comment on twitter the other day as well as the link to your blog. The reason I used sleep here, is that for my application it was just fine. For most people, there is an aweful lot of new information in this article, and I try not to introduce too many new concepts at once ... in other words, I like to avoid information overload. I am planning on updating the post in the next few weeks, and will incorporate the async operation in that post. As for being a shell hog? I do not find that an issue as I routinely have the ISE, and several Windows PowerShell consoles open at the same time.
Thanks for your comment,
ed