4

I am able to pin programs to the Windows 10 Taskbar using the below code (thanks to this StackOverflow question). However, if I try to add a command-line parameter to the program, like the example below, it doesn't work. It seems like the code presumes the target executable doesn't have any parameters.

$Target = "`"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`" --proxy-server=192.168.1.2:8080"
Param($Target)

$KeyPath1  = "HKCU:\SOFTWARE\Classes"
$KeyPath2  = "*"
$KeyPath3  = "shell"
$KeyPath4  = "{:}"
$ValueName = "ExplorerCommandHandler"
$ValueData = (Get-ItemProperty("HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\" +
  "Explorer\CommandStore\shell\Windows.taskbarpin")).ExplorerCommandHandler

$Key2 = (Get-Item $KeyPath1).OpenSubKey($KeyPath2, $true)
$Key3 = $Key2.CreateSubKey($KeyPath3, $true)
$Key4 = $Key3.CreateSubKey($KeyPath4, $true)
$Key4.SetValue($ValueName, $ValueData)

$Shell = New-Object -ComObject "Shell.Application"
$Folder = $Shell.Namespace((Get-Item $Target).DirectoryName)
$Item = $Folder.ParseName((Get-Item $Target).Name)
$Item.InvokeVerb("{:}")

$Key3.DeleteSubKey($KeyPath4)
if ($Key3.SubKeyCount -eq 0 -and $Key3.ValueCount -eq 0) {
    $Key2.DeleteSubKey($KeyPath3)
}
MKANET
  • 573
  • 6
  • 27
  • 51

1 Answers1

6

2021 Edit:

At its core, all my function did was to use the "pin" verb called from a shell.application ComObject. This used to work in 2020 when I wrote my answer but seems to be now rendered obsolete.

    $Shell = New-Object -ComObject "Shell.Application"
    $Folder = $Shell.Namespace((Get-Item $ShortcutPath).DirectoryName)
    $Item = $Folder.ParseName((Get-Item $ShortcutPath).Name)
    $Item.InvokeVerb("pin")

There was years ago an official windows API to do just that but it was also removed.

Original answer

Here's a function that will do the following:

  • Use the full path provide to create a temporary shortcut.
  • Add the arguments / Icon / Hotkey and description if any
  • Invoke the pin verb on the temporary shortcut to create the pinned item.

The pinned item will reference your application and not the temporary shortcut (which has been deleted by then anyway)

To use, just fill out the parameters (Only Path is mandatory)

Example using all parameters & splatting

$PinParams = @{
    Path         = 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
    Arguments    = '-incognito'
    Name         = 'Chrome Incognito'
    Description  = 'Launch Chrome (Incognito)'
    Hotkey       = 'ALT+CTRL+K'
    IconLocation = 'C:\Windows\system32\shell32.dll,22'
    RunAsAdmin   =  $true
}
New-PinnedItem @PinParams 

Simple example

New-PinnedItem -Arguments '-incognito' -Path 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'

Support for name only for all items in $env:Path / installed application

New-PinnedItem -Path 'notepad.exe' # Works because c:\windows\system32 is in $env:path
New-PinnedItem -Path 'chrome.exe' # Works because install path in installed appliation
New-PinnedItem -Path 'chrome' # Automatically assume *.exe if no extension provided

Support launching Powershell commands

# Internet options CPL
$inetcpl = @{
  Command      = { Start-Process inetcpl.cpl }
  Name         = 'inetcpl'
  IconLocation = 'C:\Windows\system32\shell32.dll,99'
}

# Win + R
New-PinnedItem @inetcpl

$runcmd = @{
  Command      = { $obj = New-Object -ComObject Shell.Application; $obj.FileRun() }
  Name         = 'Run'
  IconLocation = 'C:\Windows\system32\shell32.dll,25'
}
New-PinnedItem @runcmd 

#Multiline will automatically be converted to single line behind the scene.
New-PinnedItem -name 'test' -Command {
  Write-Host 'test'
  pause
} -WindowStyle Normal

--

Function Definition

Function New-PinnedItem {
    [CmdletBinding()]
    param (
        [ValidateScript( { $_.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars()) -eq -1 })]
        [Parameter(ParameterSetName = 'Path')]
        [Parameter(Mandatory, ParameterSetName = 'Command')]
        [String]$Name,
        [Parameter(Mandatory, ParameterSetName = 'Path')]
        [ValidateNotNullOrEmpty()]
        [String]$Path,
        [Parameter(Mandatory, ParameterSetName = 'Command')]
        [scriptblock]$Command,
        [ValidateSet('Normal', 'Minimized', 'Maximized')]
        [String]$WindowStyle = 'Normal',
        [String]$Arguments,
        [String]$Description,
        [String]$Hotkey,
        [String]$IconLocation,
        [Switch]$RunAsAdmin,
        [String]$WorkingDirectory,
        [String]$RelativePath
    )

    $pinHandler = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\CommandStore\shell\Windows.taskbarpin" -Name "ExplorerCommandHandler"
    New-Item -Path "HKCU:Software\Classes\*\shell\pin" -Force | Out-Null
    Set-ItemProperty -LiteralPath "HKCU:Software\Classes\*\shell\pin" -Name "ExplorerCommandHandler" -Type String -Value $pinHandler

    if ($PSCmdlet.ParameterSetName -eq 'Command') {
        #$Path = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
        $Path = "powershell.exe"
        $Arguments = ('-NoProfile -Command "&{{{0}}}"' -f ($Command.ToString().Trim("`r`n") -replace "`r`n", ';'))
        if (!$PsBoundParameters.ContainsKey('WindowStyle')) {
            $WindowStyle = 'Minimized'
        }
    }
    $NoExtension = [System.IO.Path]::GetExtension($path) -eq ""    
    if (!(Test-Path -Path $Path)) {
        if ($NoExtension) {
            $Path = "$Path.exe"
            
        }
        $Found = $False
        $ShortName = [System.IO.Path]::GetFileNameWithoutExtension($path)
        # testing against installed programs (Registry)
        $loc = Get-ChildItem HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall
        $names = ($loc | foreach-object { Get-ItemProperty $_.PsPath }).Where( { ![String]::IsNullOrWhiteSpace($_.InstallLocation) })
        $InstallLocations1, $InstallLocations2 = $names.Where( { $_.DisplayName -Like "*$ShortName*" }, 'split') 
        $InstallLocations1 = $InstallLocations1 | Select -ExpandProperty InstallLocation
        $InstallLocations2 = $InstallLocations2 | Select -ExpandProperty InstallLocation
        Foreach ($InsLoc in $InstallLocations1) {
            if (Test-Path -Path "$Insloc\$path") {
                $Path = "$Insloc\$path"
                $Found = $true
                break
            }
        }
        if (! $found) {
            $Result = $env:Path.split(';').where( { Test-Path -Path "$_\$Path" }, 'first') 
            if ($Result.count -eq 1) { $Found = $true }
        }

        # Processing remaining install location (less probable outcome)
        if (! $found) {
            Foreach ($InsLoc in $InstallLocations2) {
                if (Test-Path -Path "$Insloc\$path") {
                    $Path = "$Insloc\$path"
                    $Found = $true
                    exit for
                }
            }
        }

        if (!$found) {
            Write-Error -Message "The path $Path does not exist"
            return 
        }

    }


    if ($PSBoundParameters.ContainsKey('Name') -eq $false) {
        $Name = [System.IO.Path]::GetFileNameWithoutExtension($Path)
    }

    $TempFolderName = "tmp$((48..57 + 97..122| get-random -Count 4 |% {[char][byte]$_}) -join '')"
    $TempFolderPath = "$env:temp\$TempFolderName"
    $ShortcutPath = "$TempFolderPath\$Name.lnk"
    [Void](New-Item -ItemType Directory -Path $TempfolderPath)


    if ($Path.EndsWith(".lnk")) {
        Copy-Item -Path $Path -Destination $ShortcutPath
        $obj = New-Object -ComObject WScript.Shell 
        $link = $obj.CreateShortcut($ShortcutPath) 
    }
    else {
        $obj = New-Object -ComObject WScript.Shell 
        $link = $obj.CreateShortcut($ShortcutPath) 
        $link.TargetPath = $Path
    }

    switch ($WindowStyle) {
        'Minimized' { $WindowstyleID = 7 }
        'Maximized' { $WindowstyleID = 3 }
        'Normal' { $WindowstyleID = 1 }
    }

    $link.Arguments = $Arguments
    $Link.Description = $Description
    if ($PSBoundParameters.ContainsKey('IconLocation')) { $link.IconLocation = $IconLocation }
    $link.Hotkey = "$Hotkey"
    $link.WindowStyle = $WindowstyleID
    if ($PSBoundParameters.ContainsKey('WorkingDirectory')) { $link.WorkingDirectory = $WorkingDirectory }
    if ($PSBoundParameters.ContainsKey('RelativePath')) { $link.RelativePath = $RelativePath }
    $link.Save()

    if ($RunAsAdmin) {
        $bytes = [System.IO.File]::ReadAllBytes($ShortcutPath)
        $bytes[0x15] = $bytes[0x15] -bor 0x20 #set byte 21 (0x15) bit 6 (0x20) ON
        [System.IO.File]::WriteAllBytes($ShortcutPath, $bytes)
    }

    $Shell = New-Object -ComObject "Shell.Application"
    $Folder = $Shell.Namespace((Get-Item $ShortcutPath).DirectoryName)
    $Item = $Folder.ParseName((Get-Item $ShortcutPath).Name)
    $Item.InvokeVerb("pin")

    Remove-Item -LiteralPath  "HKCU:Software\Classes\*\shell\pin\" -Recurse   
    Remove-item -path $ShortcutPath
    Remove-Item -Path $TempFolderPath 
    [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject([System.__ComObject]$shell)
    [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject([System.__ComObject]$obj)
}

To conclude, for your needs, you'd call it like this:

New-PinnedItem -Path 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe' -Arguments '--proxy-server=192.168.1.2:8080'

Additional considerations

It looks like there is two things important to consider when pinning something.

  • The full path of the application
  • The arguments passed down

Other parameters are inconsequential to the PIN action. Any PIN action called with the same set of full path and arguments will be compared to other pins and either pin (if not found) or unpinned (if found) without consideration for Name / IconLocation / Hotkey / etc...

Note that if you use the function to pin an item which is already open (eg: Chrome), the pin / unpin action will be performed on the current instance if the path / arguments match, meaning it could appear as if didn't worked but if you look at the pin status of the opened app (or close it), you should see that the behavior changed from unpinned to pinned or pinned to unpinned (if already pinned)

Additional notes

Pin Data is stored in 2 locations

  • HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Taskband
  • $env:APPDATA\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar

You could easily swap 2 taskbars set of pin or more by making use of them.

Here's a snippet on code to view the favorite data as Hex / string

$Bytes = Get-ItemPropertyValue -LiteralPath  "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Taskband\" -name  'Favorites'
# Data as Hex
[System.BitConverter]::ToString($bytes) 

# A look at the data
[System.Text.Encoding]::UTF8.GetString($Bytes)

References

[MS-SHLLINK]: Shell Link (.LNK) Binary File Format

Create a Run As Administrator shortcut

Sage Pourpre
  • 9,932
  • 3
  • 27
  • 39
  • Thank you. I ran the above script with desk administrator privileges. The script ran without throwing any exceptions. However, my currently pinned Chrome shortcut doesn't get modified. I even restarted the Explorer.exe process (which I would prefer not to do). I tried this one separate Windows 10 (64bit) PC's. Is there a prerequisite I might be missing? – MKANET Jan 21 '20 at 20:49
  • @MKANET I edited my answer. Can you uncomment the Start-Sleep I added there ? My thought is that the shortcut took more time to create than the script did to finish. With the 5 second delay, it should cover that possibility. If this work, let me know and I'll edit my answer with something better than an arbitrary 5 sec. – Sage Pourpre Jan 21 '20 at 21:25
  • @MKANET Also, if the shortcut was already pinned when you ran the script, try to remove it and run again to see if it makes a difference. This is something of interest for me too so I'll take a deeper look in a couple of hours but I'd be interested to know if the suggestions above work or not. – Sage Pourpre Jan 21 '20 at 21:26
  • I commented out the start-sleep line; and, ran the script again.... still no difference, even when unpinning it first. I also restarted explorer, ran as desktop admin... nothing changes. Does this work for you when you try it? – MKANET Jan 21 '20 at 21:47
  • @MKANET See my edited answer. I think this is cleaner than my previous suggestion... I believe It should work for you. – Sage Pourpre Jan 22 '20 at 02:06
  • @"Sage Pourpre" ..just one minor thing... if the There's an icon with that name, it will get unpinned. – MKANET Jan 22 '20 at 17:35
  • @MKANET Are you pinning 2 time the same item (Eg: Chrome ?) If so, try setting the `-Name` parameter to something different for each (Eg: Chrome 1 ; Chrome 2) Even with your original script, I noticed the pin undid itself when calling a second time. However, if you use 2 different shortcut name, it seems to work properly. – Sage Pourpre Jan 22 '20 at 18:37
  • I decided to blindly pin the same icon name twice.. That will redo it without having more than one icon. – MKANET Jan 22 '20 at 20:00
  • @MKANET See additional notes / additional considerations. Name, contrarily to what I said prior, is irrelevant. What is considered is FullPath + Arguments. If you Pin something that have a match for these 2, it will disappear. That being said, I found a small logic mishaps if you attempted to pin a shortcut instead of an .exe that caused the arguments not to be accounted for, resulting in two PIN having the same target path and the arguments being ignored (only with *.lnk). That part is fixed now too. – Sage Pourpre Jan 25 '20 at 00:36
  • Added support for Powershell commands (pinned shortcut will execute a script) and missing attributes (WindowStyle,RelativePath and WorkingDirectory). – Sage Pourpre Jan 25 '20 at 05:30
  • @"Sage Pourpre" No wonder, I was pulling my hair out trying to reliably pin/unpin. I was getting shortcut names like "Chrome (2)". I'm looking forward to trying it. I'll post back. – MKANET Jan 25 '20 at 05:59
  • @"Sage Pourpre". This works very well. It's by far the best PowerShell script that pins/unpins icons to the Windows 10 taskbar. Hopefully, other people in the future can find this script easily via Google.com. Thank you. – MKANET Jan 26 '20 at 06:49
  • Is there any way to pin shortcut to Start via PowerShell? I know about syspin. – farag Nov 24 '20 at 15:13
  • I'm trying to use this but when I supply the command with -Path argument and the full path nothing happens. No errors show or exceptions. Am I missing something? – laneherby Jul 23 '21 at 16:31
  • @laneherby Looks like you are right. Unfortunately, Microsoft removed the "official API" to pin stuff to the taskbar. Since there, everything that existed is workarounds, including mine. It looks like using a shell.application comobject and the "pin" verb is no longer working. It was though in 2020 when I produced this answer. I will add a note at the top of my answer to indicate that it is now deprecated. – Sage Pourpre Jul 26 '21 at 00:18