Creating Pop-ups by Using PowerShell


Summary: Guest blogger and PowerShell MVP, Chrissy LeMaire, talks about creating pop-ups with Windows PowerShell.

Microsoft Scripting Guy, Ed Wilson, is here. Welcome today a brand new guest blogger, Chrissy LeMaire.

Chrissy is a systems engineer and PowerShell MVP. Always an avid scripter, she attended the Monad session at the Microsoft Professional Developers Conference in Los Angeles in 2005. She has worked and played with PowerShell ever since. You can follow her on Twitter at @cl, and read more about her work with PowerShell on her blog, blog.netnerds.net:\>.

Here’s Chrissy…

A few weeks ago, I built a huge SQL Server lab that ended up maxing out my virtual environment’s resources. My resource usage alarms were going off, but I couldn’t see them unless I logged in to the admin client. I asked myself, “Wouldn’t it be nice if I had a little notification icon pop-up that would tell me how much RAM and CPU each of my hosts was using? I bet PowerShell can do that.” And sure enough, it can.

Image of menu

PowerShell GUIs, WPF, and XAML

Like .NET, there are a variety of ways to create GUIs in PowerShell. Earlier in the year, I learned how to build GUIs in PowerShell from three primary sources: the Hey, Scripting Guy! Blog post, I've Got a PowerShell Secret: Adding a GUI to Scripts, Boe Prox, and Denniver Reining. With these resources, I learned that I could build a Windows Presentation Framework (WPF) form in Visual Studio, copy the resulting XAML into PowerShell, then manipulate the GUI with variables like $window and $listview. Finally, GUI programming was within reach.

This is the first post in a series of two, and it builds heavily on the three previous resources, so if you’re unfamiliar with PowerShell GUIs and XAML, you may want to review those concepts before reading further.

When creating GUIs in PowerShell, I recommend using WPF over alternatives such as Windows Forms (WinForms) because WPF is extremely flexible, and it separates the GUI design from the coding process. This means that you can build the GUI first in Visual Studio, instead of using the command line. WPF forms can be designed by using Visual Studio Community, which Microsoft provides free of charge to individual developers, open source projects, educational institutions, and small professional teams.

Why not use C#?

“C# is about optimizing computer resources. PowerShell is about optimizing human resources.”
     ~ Jeffrey Snover

Although it’s true that C# is great for making GUIs, PowerShell is already in my toolbox. I’m not a developer, and I’ll never get around to learning GUI development in C#. For me, PowerShell is an easier solution and on top of it all, it’s a lot of fun.

Intro to pop-ups

In the following tutorial, I’ll be using the term “pop-up” loosely. WPF actually does have a Popup class, but after some testing, the WPF Window seems most appropriate. This is because pop-ups are really intended to provide details about UI elements, much like a ToolTip or an “alt” tag in HTML.

The placement of WPF Windows is also easier and more consistent than a Popup placement. Consistent placement is especially important when the Window is expected to appear in the lower right part of the screen, as is often the case with notification icon pop-ups.

 Sounds good—what are we making?

There are endless possibilities for what this application can contain. In this post, we’ll keep it simple, but useful, by creating a pop-up that shows your computer’s disk-free space. We’ll also use mouse clicks and a notification icon to make the pop-up window appear and disappear.

Image of menu

This will set the stage for an even cooler monitoring application that that will alert you if you’re running low on disk space. The monitoring application will be detailed in the second post of this series.

Because I intend for the pop-up to be flexible and visually appealing, there will be a little extra code. In the end, my hope is that you can easily reuse this pop-up without much fuss for your own project.  Some of this code will be in image format. For a full text version of the code, please see Creating Popup Windows in PowerShell using WPF in the Script Center Repository.

Building this pop-up in PowerShell is comprised of ten steps.

  1. Load the required assemblies.
  2. Create and populate the disk-free space PowerShell custom object.
  3. Extract the PowerShell icon to use in the Notification area.
  4. Create a form in Visual Studio, taking careful note to name the required objects.
  5. Copy the resulting XAML, cleaning it up a little, and transforming it into a PowerShell object.
  6. Dynamically populate and draw the ListView control box.
  7. Position the window in the lower-right corner of the primary screen.
  8. Add the notification icon.
  9. Add double -click, lost-focus, and exit events.
  10. Hide the PowerShell host process, and run the script as an application.

Let the coding begin

First thing’s first. We’ll be using some .NET assemblies, so let’s load them up:

Add-Type -AssemblyName PresentationFramework, System.Drawing, System.Windows.Forms

We will use the PresentationFramework assembly for the XAML form, the System.Drawing assembly to extract the PowerShell icon, and the System.Windows.Forms assembly to create the Notify icon host application. WPF is ideal for creating the pop-up window, but it cannot create the Notify icon.

 Create and populate the disk-free space object

We already know what we want to see, so it’s time to create the object that displays this information. Boe Prox’s post, Locating Mount Points Using PowerShell, is my go-to for a good code snippet about this topic.

Let’s populate a PowerShell custom object named $itemsource with the following disk information values:  Name, Label, Total GB, and Free GB.

$localdisks = Get-WmiObject Win32_Volume -Filter "DriveType='3'"

$itemsource = @()

foreach ($disk in ($localdisks| Sort-Object -Property Name)) {

    if (!$disk.name.StartsWith("\\")) {

              $itemsource += [PSCustomObject]@{

                     Name = $disk.Name

                     Label = $disk.Label

                     Total = "$([Math]::Round($disk.Capacity /1GB,1)) GB"

                     Free = "$([Math]::Round($disk.FreeSpace /1GB,1)) GB"

              }

       }

}

Setting the column order will allow us to control how the columns are ordered later in the script:

$columnorder = 'Name', 'Label', 'Total', 'Free'

Extract the PowerShell icon

We’ll create the notification icon (notifyicon), which is built into Windows forms. You can use any icon, and in this case, I chose to use the PowerShell icon. So let’s extract the icon from the PowerShell executable:

$icon = [System.Drawing.Icon]::ExtractAssociatedIcon("$pshome\powershell.exe")

Create the WPF window

I mentioned that I learned how to create GUIs in PowerShell by reading WPF articles by the Scripting Guys and Boe Prox. Now that I know how to do this, I can put it into action by using Visual Studio to build my form.

Ultimately, I chose ListView to display my information. When displaying information, it’s generally accepted that using ListView is ideal for read-only data, while DataGrid is ideal for CRUD (create, read, update, delete). For more information, see WPF DataGrid Practical Examples.

Image of menu

I found ListView to be rather flexible, because different types of objects can be used to populate it, including (but not limited to) PowerShell custom objects, data tables, and hash tables. In this case, we’ll be using a custom object.

One major drawback with a ListView control box is that customizing it can be a bit painful, especially if you want it to have a sleek, chromeless appearance. The code to modify the header row (GridViewColumnHeader) alone takes up about 50% of the XAML code that is used in this post.

Designing a WPF form in Visual Studio is surprisingly simple. I won’t go over it in its entirety, but basically, it’s:

Start > Visual Studio 2015 > New Project > Visual C# > Windows > Classic Desktop > WPF Application

The resulting XAML from the default form can be converted into usable PowerShell code with a few changes.

Image of menu

The XAML

Here is the resulting XAML from the form that I built:

Image of command output

Note  To copy this code, see Creating Popup Windows in PowerShell using WPF, which contains the code in plain text.

This form would be rather straightforward were it not for the GridViewColumnHeader style. This necessary chunk of code ensures that the form looks appealing. Otherwise, the column header would be distracting, as can be seen here:

Image of menu

Transform XAML to PowerShell variables

Take note of any place in the XAML that says Name. We can convert these nodes (Window, Grid, and ListView) to PowerShell variables by parsing the XML object and creating a variable for each result, as shown in the following code:

$window = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))
$xaml.SelectNodes("//*[@Name]") | ForEach-Object { Set-Variable -Name ($_.Name) –Value $window.FindName($_.Name) -Scope Script }

If you’re interested in a breakdown of how this works, you can read more in this post: I've Got a PowerShell Secret: Adding a GUI to Scripts.

 Dynamically populate the ListView control box

Now that we have access to the $window, $grid, and $listview variables, we can populate $listview with the $itemsource object we created earlier. Then, we’ll set the width of $listview to take up 90% of $grid.

$listview.ItemsSource = $itemsource

$listview.Width = $grid.width*.9

At this point, we’ve only set the ItemSource object, but the layout must still be built using GridView. If you recall, I wanted this to be as dynamic as possible, so we’ll add the GridView for the ListView by using PowerShell. This technique is akin to using C#’s "code-behind" (see How to: Add an Event Handler Using Code).

$gridview = New-Object System.Windows.Controls.GridView

foreach ($column in $columnorder) {

    $gridcolumn = New-Object System.Windows.Controls.GridViewColumn

    $gridcolumn.Header = $column

    $gridcolumn.Width = $grid.width*.20

    $gridbinding = New-Object System.Windows.Data.Binding $column

    $gridcolumn.DisplayMemberBinding = $gridbinding

    $gridview.AddChild($gridcolumn)

}

$listview.view = $gridview

The previous section of code creates the GridView, adds columns (Name, Label, Total, Free) to GridView, sets the width of each column, then associates the column with the actual data contained within $listview.ItemSource ($itemsource) on a column name by column name basis.

Here is a visual representation of the pop-up, which is now populated by a ListView control box.

Image of pop-up

…well, almost. We still need to tell the pop-up window to appear in the lower-right corner. More on that shortly.

Create notification icon and add right-click

NotifyIcon is the icon that you’ll interact with in your task bar. To add a right-click (Exit), we will create a ContextMenu, then populate $contextmenu with a $menuitem that reads Exit and responds to clicks.

$notifyicon = New-Object System.Windows.Forms.NotifyIcon

$notifyicon.Text = "Disk Usage"

$notifyicon.Icon = $icon

$notifyicon.Visible = $true

$menuitem = New-Object System.Windows.Forms.MenuItem

$menuitem.Text = "Exit"

 

$contextmenu = New-Object System.Windows.Forms.ContextMenu

$notifyicon.ContextMenu = $contextmenu

$notifyicon.contextMenu.MenuItems.AddRange($menuitem)

Image of menu

Add double-click and exit events

Now that we have all of our objects ready, we will add event handlers. Events are members of an object, such as properties and methods. Event handling is basically “do this when that happens,” for instance, “Set the pop-up window to visible when the notification icon is clicked.”

How do you know what events an object supports? Pipe the object to Get-Member -MemberType Event:

$notifyicon | Get-Member -MemberType Event

Alternatively, you can search the web for phrases such as msdn notifyicon events, and you’ll find an easy-to-read webpage with a list of all of the events for any object that supports events (see NotifyIcon Events). If you’re interested in learning more about events, check out this article by PowerShell MVP, June Blender: The Methods that Register Events.

# Close the pop-up window if it's double clicked

$window.Add_MouseDoubleClick({

            $window.Hide()

})

# Close the pop-up window if any other window is clicked

$window.Add_Deactivated({

            $window.Hide()

})

# When Exit is clicked, close everything and kill the PowerShell process

$menuitem.add_Click({

            $notifyicon.Visible = $false

            $window.Close()

            Stop-Process $pid

})

# Show window when the notifyicon is clicked with the left mouse button

# Recall that the right mouse button brings up the contextmenu

$notifyicon.add_Click({

   if ($_.Button -eq [Windows.Forms.MouseButtons]::Left) {

            # reposition each time, in case the resolution changes

            $window.Left = $([System.Windows.SystemParameters]::WorkArea.Width-$window.Width)

            $window.Top = $([System.Windows.SystemParameters]::WorkArea.Height-$window.Height)

            $window.Show()

            $window.Activate()

   }
})

Ensuring that the pop-up window always appears above the notification icon can be tricky, especially when you’re working with multiple monitors. To handle this placement, the previous code determines the proper coordinates each time the notification icon is clicked. This ensures that the pop-up will appear where it’s expected, even if the display resolution changes after the application is first launched.

Hide PowerShell host process and run the application

Because this is a long-running process, we don’t want the PowerShell window to stay in our task bar. To hide the PowerShell process, we’ll use a technique from Microsoft that calls some C# code (see Show-PowerShell/Hide-PowerShell).

$windowcode = '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);'

$asyncwindow = Add-Type -MemberDefinition $windowcode -name Win32ShowWindowAsync -namespace Win32Functions -PassThru

$null = $asyncwindow::ShowWindowAsync((Get-Process -PID $pid).MainWindowHandle, 0)

When the PowerShell process is hidden, we can do a little garbage collection, then launch the actual application. Forcing a quick garbage collection is not required, but it can release unused resources so that the application starts with less RAM usage.

[System.GC]::Collect()

Next, we’ll create a Windows Form object called ApplicationContext to “contain” the entire application (see Doing a NotifyIcon program the right way). This is the proper way to run windowless NotifyIcon tasks and it makes the application more responsive, especially the Exit action for NotifyIcon.

$appContext = New-Object System.Windows.Forms.ApplicationContext

[void][System.Windows.Forms.Application]::Run($appContext)

And now we’re done! Want to see this in action? Copy and paste the entire script from the Script Center Repository (see Creating Popup Windows in PowerShell using WPF) or download the .ps1 and execute.

A couple more things

Initially, when PowerShell starts, you might notice that it will be using a bit of RAM (35-100 MB). Over time, however, this number is drastically reduced, usually to about 5 or 6 MB.

Image of menu

Also, if you find that the icon disappears, you can customize the Notification area to show it all the time. In Taskbar and Start Menu Properties, click Customize, and setting the icon to On.

Image of menu

Now, you’re set!

Imagine all of the things you can see with a quick glance now that you know how to do it so easily using PowerShell. DBAs can quickly see if important jobs have failed, Hyper-V admins can get an overview of host resource usage, and storage admins can easily see if disk space is running low or if a disk has failed.

In the next post of this series, I’ll explore monitors and alerts by examining a real-world example that polls for low disk space and triggers an alert when the disk capacity dips below 20%.

~ Chrissy

Thanks Chrissy. I'm looking forward to the next blog post.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 

Comments (6)

  1. jrv says:

    Nice demo of XAML. Neat tricks to get PS to behave with the notify.

  2. Jacob says:

    This is really cool. Thank you!

  3. kenny says:

    Thanks for sharing

  4. Robin says:

    Super psyched for the next part. Can’t wait! This will have endless use cases for our managed server infrastructure. Thanks!

  5. Ola Holtberget says:

    Great work, really helpful!
    Is it meant to update the diskspace value dynamically as the diskspace changes?
    I need to restart the script for it to read an updated value.

Skip to main content