Thursday, April 30, 2015

How to pin Powershell to the taskbar with... Powershell

Last month I have been involved in a large Windows deployment project. One of the requirements was to be able to deliver Windows 7 computers with the Windows Powershell icon pinned to the taskbar.
 
That was not a difficult problem to solve, and, even if Windows 7 is now old stuff (especially after Windows 8.1 came out both on computers and Windows Phones) this project was nevertheless a good occasion to learn some Powershell basics that I feel like sharing with you here today.
 
The first thing to understand is that the Windows Taskbar is a user-oriented feature that Microsoft doesn't want system administrators or program installers to modify in place of the end-user. In other words, since most users don't want programs putting junk in their taskbar, Windows doesn't support you doing as such. That's the historical reason why I suppose there is no existing API to my knowledge to modify the Windows Taskbar.

Happily enough, the Shell.Application COM object comes to the rescue. Ok, I agree that's a bit old method of interacting with the Desktop by mimicking mouse-clicking, and some of you will point out that this is so VBScript. This is nonetheless the only possible way to achieve what we have been tasked for. And, for those that are curious, it's good to know that all the code I'll show below works against Windows 8 systems. What else?

Let's begin. The Shell.Application COM object is a powerful object that allow interaction with the Windows Shell. It provides a set of automation objects that can be used to access many of the Shell's features and dialog boxes.

For our task, we are going to use PowerShell to create this Shell.Application object and through it we will manipulate the Explorer programmatically.

All COM objects are created through the command: New-Object -ComObject. There really are a lot of options and possibilities for New-Object -ComObject. You could for instance open Outlook (with Outlook.Application), or Word (with Word.Application), or Excel (with Excel.Application). For our purpose we specifically need a Shell.Application type of object. Let's see how that's done.

$Shell = New-Object -ComObject Shell.Application
Once you have established this kind of connection with the Windows Shell, you can perfom most of the interaction with Windows Explorer as you'd do with your keyboard and mouse.

You could for instance open the Windows Explorer and browse the C:\ folder:

$Shell.open("C:\")
Or you could open the Windows Help:

$Shell.Help()


As you should know, Open() and Help() are methods of the Shell object. There are other useful methods, which can be investigated with Get-Member:

$Shell | Get-Member

   TypeName: System.__ComObject#{866738b9-6cf2-4de8-8767-f794ebe74f4e}

Name                 MemberType Definition
----                 ---------- ----------
AddToRecent          Method     void AddToRecent (Variant, string)
BrowseForFolder      Method     Folder BrowseForFolder (int, string, int, Variant)
CanStartStopService  Method     Variant CanStartStopService (string)
CascadeWindows       Method     void CascadeWindows ()
ControlPanelItem     Method     void ControlPanelItem (string)
EjectPC              Method     void EjectPC ()
Explore              Method     void Explore (Variant)
ExplorerPolicy       Method     Variant ExplorerPolicy (string)
FileRun              Method     void FileRun ()
FindComputer         Method     void FindComputer ()
FindFiles            Method     void FindFiles ()
FindPrinter          Method     void FindPrinter (string, string, string)
GetSetting           Method     bool GetSetting (int)
GetSystemInformation Method     Variant GetSystemInformation (string)
Help                 Method     void Help ()
IsRestricted         Method     int IsRestricted (string, string)
IsServiceRunning     Method     Variant IsServiceRunning (string)
MinimizeAll          Method     void MinimizeAll ()
NameSpace            Method     Folder NameSpace (Variant)
Open                 Method     void Open (Variant)
RefreshMenu          Method     void RefreshMenu ()
ServiceStart         Method     Variant ServiceStart (string, Variant)
ServiceStop          Method     Variant ServiceStop (string, Variant)
SetTime              Method     void SetTime ()
ShellExecute         Method     void ShellExecute (string, Variant, Variant, Variant, Variant)
ShowBrowserBar       Method     Variant ShowBrowserBar (string, Variant)
ShutdownWindows      Method     void ShutdownWindows ()
Suspend              Method     void Suspend ()
TileHorizontally     Method     void TileHorizontally ()
TileVertically       Method     void TileVertically ()
ToggleDesktop        Method     void ToggleDesktop ()
TrayProperties       Method     void TrayProperties ()
UndoMinimizeALL      Method     void UndoMinimizeALL ()
Windows              Method     IDispatch Windows ()
WindowsSecurity      Method     void WindowsSecurity ()
WindowSwitcher       Method     void WindowSwitcher ()
Application          Property   IDispatch Application () {get}
Parent               Property   IDispatch Parent () {get}
Between them you can see methods like ToggleDesktop(), which corresponds to pressing Win+D, or MinimizeAll(), which corresponds to Win+M. In our case we want first use the NameSpace method to create and return a Folder object for the folder where Windows Powershell resides: C:\Windows\System32\WindowsPowershell\v1.0\
 
The NameSpace method accepts as a parameter the path of the folder you work on as well as one of the ShellSpecialFolderConstants values. Here some examples:

(new-object -comobject shell.application).NameSpace(0x21).Self.Path
C:\Users\administrator\AppData\Roaming\Microsoft\Windows\Cookies
or

(new-object -comobject shell.application).NameSpace(0x25).Self.Path
C:\Windows\System32
In the previous example 0x25 is an hex code referring to the Windows System folder, usually c:\Windows\System32.

Unfortunately for the moment there is no special folder hex code for the Windows Powershell folder as far as I know. So the full path must be hard-coded in the string:

(New-Object -ComObject shell.application).Namespace('C:\Windows\System32\WindowsPowershell\v1.0\')
Let's see what the available methods are for the returned Folder object:

(New-Object -ComObject shell.application).Namespace('C:\Windows\System32\WindowsPowershell\v1.0\') | Get-Member

   TypeName: System.__ComObject#{a7ae5f64-c4d7-4d7f-9307-4d24ee54b841}

Name                       MemberType Definition
----                       ---------- ----------
CopyHere                   Method     void CopyHere (Variant, Variant)
DismissedWebViewBarricade  Method     void DismissedWebViewBarricade ()
GetDetailsOf               Method     string GetDetailsOf (Variant, int)
Items                      Method     FolderItems Items ()
MoveHere                   Method     void MoveHere (Variant, Variant)
NewFolder                  Method     void NewFolder (string, Variant)
ParseName                  Method     FolderItem ParseName (string)
Synchronize                Method     void Synchronize ()
Application                Property   IDispatch Application () {get}
HaveToShowWebViewBarricade Property   bool HaveToShowWebViewBarricade () {get}
OfflineStatus              Property   int OfflineStatus () {get}
Parent                     Property   IDispatch Parent () {get}
ParentFolder               Property   Folder ParentFolder () {get}
Self                       Property   FolderItem Self () {get}
ShowWebViewBarricade       Property   bool ShowWebViewBarricade () {get} {set}
Title                      Property   string Title () {get}
Before any of these methods can be executed, we must use ParseName to create an object that represents an item inside this folder.

(New-Object -ComObject shell.application).Namespace('C:\Windows\System32\WindowsPowershell\v1.0\').parsename('powershell.exe')

Application  : System.__ComObject
Parent       : System.__ComObject
Name         : powershell.exe
Path         : C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
GetLink      :
GetFolder    :
IsLink       : False
IsFolder     : False
IsFileSystem : True
IsBrowsable  : False
ModifyDate   : 27/09/2013 04:13:50
Size         : 471040
Type         : Application
The returned object has four methods:

ExtendedProperty Method     Variant ExtendedProperty (string)
InvokeVerb       Method     void InvokeVerb (Variant)
InvokeVerbEx     Method     void InvokeVerbEx (Variant, Variant)
Verbs            Method     FolderItemVerbs Verbs ()
As you can see there are two ways to call linked verbs: InvokeVerb() and Verbs().

InvokeVerb() is the direct method to work on a FolderItem object. To pin or unpin items to the taskbar it takes as value the verbs as they are described under HKEY_CLASSES_ROOT\CLSID\{90AA3A4E-1CBA-4233-B8BB-535773D48449}.
That's because the action of pinning and unpinning items is managed by the 'Pin to Taskbar' handler which has the very secret CLSID {90AA3A4E-1CBA-4233-B8BB-535773D48449}. The two shown verbs are:

taskbarpin;taskbarunpin


So the code to pin or unpin is the following:

(New-Object -ComObject shell.application).Namespace('C:\Windows\System32\WindowsPowershell\v1.0\').parsename('powershell.exe').invokeverb('TaskbarPin')

(New-Object -ComObject shell.application).Namespace('C:\Windows\System32\WindowsPowershell\v1.0\').parsename('powershell.exe').invokeverb('TaskbarUnPin')
The alternative is to use the Verbs() method. The listed actions match what you would get by right clicking on an item in Windows Explorer:

(New-Object -ComObject shell.application).Namespace('C:\Windows\System32\WindowsPowershell\v1.0\').parsename('powershell.exe').verbs() | select Name

Name
----
&Open
Run as &administrator

Scan for Viruses...
Pin to Tas&kbar
Pin to Start Men&u
Restore previous &versions

Cu&t
&Copy
Create &shortcut
&Delete
Rena&me
P&roperties
Please note that the ampersand (&) that appears in some verbs is not a typo. Some verbs actually have the ampersand because it's used for the hot key of the context menu.

The oneliner here is longer than the one that leverages InvokeVerb():

((new-object -com "Shell.Application").Namespace('C:\Windows\System32\WindowsPowershell\v1.0\').Parsename('powershell.exe').Verbs() | ? {$_.Name -eq 'Pin to Tas&kbar'}).doit()
To make it more readable and understandable, let's split it up:

$shell = new-object -com "Shell.Application"
$folder = $shell.Namespace('C:\Windows\System32\WindowsPowershell\v1.0\')
$item = $folder.Parsename('powershell.exe')
$verb = $item.Verbs() | ? {$_.Name -eq 'Pin to Tas&kbar'}
if ($verb) {$verb.DoIt()}
Now you might be wondering what the 'DoIt' line is for. Well, it's a bit of running in circles but in simple words, DoIt() executes a verb on the FolderItem associated with the verb. Nothing else.
So to resume, the script does the pinning in three steps:
  1. Use the Verbs() method of a FolderItem object to get a list of verbs applicable to the target.
  2. Filter the verbs down to the one with a name matching the expected action.
  3. Execute the DoIt() method to invoke the verb.
When you execute the code above, it's just like Powershell is doing the clicking for you and your pinned apps on the taskbar get saved in both locations below:

In the registry under:
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Taskband

In the hidden TaskBar folder:
C:\Users\Username\AppData\Roaming\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar
 
Let's end this post with a few information on the Shell.Application. If you want to find out all the possible ComObjects on your PC with Powershell, just check in the registry under HKLM:\Software\Classes. We can perform a bit of pattern matching to filter down to the objects that matches the naming syntax of ComObjects with '^\w+\.\w+$', which can be read like two groups of words separated by a dot.

Here's the code to find them all:

Get-ChildItem HKLM:\Software\Classes -ErrorAction SilentlyContinue | ? {
   $_.PSChildName -match '^\w+\.\w+$' -and (Test-Path -Path "$($_.PSPath)\CLSID")
} | Select-Object -ExpandProperty PSChildName
Shell.Application is one of them:

Get-ChildItem HKLM:\Software\Classes | ? PSChildName -Match 'Shell.Application'

    Hive: HKEY_LOCAL_MACHINE\Software\Classes


Name                           Property
----                           --------
Shell.Application              (default) : Shell Automation Service

Now that we know where it is stored in the Windows Registry, we can also see its CLSID, which is {13709620-C279-11CE-A49E-444553540000}, as you can see opening the key. This value can be used to perform the pinning in another way that I am going to show you.

First of all we have to convert the CLSID to a type, which is done like this:

$Type= [Type]::GetTypeFromCLSID('13709620-C279-11CE-A49E-444553540000')
Then we will leverage the [System.Activator] class, which is generally used to create types of objects. Its CreateInstance method create an instance of the object. This is usually used in .NET Late Binding, as explained here, but I think it's worth having a look:

$Object = [Activator]::CreateInstance($Type)
Then we can proceed like seen before defining the path of the file, its name and the verb to apply:

[Activator]::CreateInstance([Type]::GetTypeFromCLSID('13709620-C279-11CE-A49E-444553540000')).Namespace('C:\Windows\System32\WindowsPowershell\v1.0\').ParseName('powershell.exe').InvokeVerb('taskbarpin')
That's kind of a long one-liner, and unfortunately there is no way of making it shorter as far as I can tell. You should now be able to read it, because you know the basics concepts I have just shown you. I hope you learned something. Stay tuned for more.

1 comment:

  1. Really awesome post, do you have any pointers why i cant get it to work in Windows 10?

    ReplyDelete

Related Posts Plugin for WordPress, Blogger...