Pin program (with parameters) to Taskbar using PS

2020-07-23 06:50发布

问题:

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)
}

回答1:

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
    )
    $NoExtension = [System.IO.Path]::GetExtension($path) -eq ""
    $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'
        }
    }

    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