Creating a Windows Application with PowerShell
- Written by Alexander Riedel
- Last Updated: 09 October 2019
- Created: 22 January 2019
- Hits: 131
Not all Windows applications are purely based on Windows Forms. Take Notepad for example; it uses a “form” if you search for a string, but otherwise it’s just a window with a menu and a text box.
PrimalScript and PowerShell Studio’s packager has had a Windows host like that for a while now. It basically directs all output from the packaged script into a RichText edit control (see this article). This works really well for a small script with a single type of output, but what if we want to enable the end-user to run different tasks at different times? Take this script for example:
You can probably tell that in most cases this script will output a large amount of text. It would likely be too much to ask the end-user to sift through the resulting output to find what is needed, so we need to provide a little more structure to select individual queries.
Just as we have done with system tray applications (see this article), we have added access to the menus of our Windows application host.
To get started, we add some code to add a “WMI Queries” entry to the top level menu:
As you may recall, in the System Tray application we had a GetContextMenuStrip call on the SAPIENHost object. This time we use the GetMenuStrip call. Why the distinction? Both calls return a menu, but they return different underlying object types. In this case we get the System.Windows.Forms.MenuStrip object. We wanted to keep things clean and not return a different object with the same call, but, most importantly, accessing the main window’s menu strip does not exclude also accessing the context menu of the current window. So we will make use of the GetContextMenuStrip call at a later time.
At any rate, we create a ToolStripMenuItem and add it to the applications main menu. This is done by inserting it into the parent’s Items collection. We want the new item to show up after the “Edit” entry, so we use the index 2 (File = 0, Edit = 1).
When we build and run this, we get the desired menu entry.
The next step is to create menu items for each of our functions:
We use two properties of the newly created menu items; Name and Text. Name is what we use internally to identify the item, and Text is what the user will see.
We are almost done with setting up the menus. Next we add these items to the drop down menu we inserted:
The method to add an item to a collection returns the index of the new item, so we assign that to a variable which prevents unwanted output.
Of course we need to associate these menu items with something to actually handle the menu event:
So we create a script block for each menu entry, which looks like this:
Each command handler sets a global variable to indicate which command needs to be executed next. It also uses the GetTextView() command to retrieve the RichTextBox object from the host to reset the content.
This is where it gets really interesting, but first let’s build and run to see what we have:
We now have the menus in place and the handlers for the menu topics, but that doesn’t really do anything yet. We need to dive a little deeper, but it is not complicated—just a little different than your normal, straightforward linear script.
Just as with a service or a tray application, if we want our script to handle menu events it cannot simply exit after setting everything up. We need a loop at the end of our script, just like we did for the tray app or a packaged Windows Service.
We use a loop and switch to handle the command set by the handler via the $global:NextCommand variable. Please understand that the ‘global’ part is essential, because the scope of your script and the scope of the event handler are different.
Why don’t we just execute the command directly in the handler? Because two things are at play here:
- For one, the outer Windows application and your PowerShell script each run in an individual thread. As the event handlers are called from the application thread, which handles the user interface, there are limits on what you can do directly.
- Secondly, most PowerShell commands use a pipeline. That pipeline is usually flushed at the end of a command. Since our script is not supposed to end before the application exists, that would never happen so we need to use “| Out-Host”.
PowerShell also strongly objects to executing a CIM command from another thread, so we really need to make sure we call it from the main thread.
You can of course put the actual code in their own functions or script blocks. You do not have to cram all code into the switch statement; we did that simply for illustration purposes.
The code for this sample is available for download.