Skip to content
This repository has been archived by the owner on May 28, 2021. It is now read-only.

Start menu tiles and taskbar app pinning / unpinning #8

Closed
Disassembler0 opened this issue May 8, 2017 · 7 comments
Closed

Start menu tiles and taskbar app pinning / unpinning #8

Disassembler0 opened this issue May 8, 2017 · 7 comments
Labels

Comments

@Disassembler0
Copy link
Owner

Disassembler0 commented May 8, 2017

I’d like to find a universal, reliable, reversible and repeatable way to pin / unpin tiles in start menu and shortcuts in taskbar.

The Problem

Microsoft makes it hard to achieve this function programmatically as they believe that application developers and publishers would abuse this mechanics to force their applications to every place possible. According to MS representative, pinning should be left entirely up to the user.
Now, I'm using Windows for quite some time and I'm aware that there are plenty of ways how the user interaction can be simulated using various macros, P/Invoke calls and other shady techniques. Pinning shouldn't be any different. I've already seen plenty of solutions like this one. The downside is, that such solution is not universal for all languages as both names and verbs (actions) are localized.

The Research

I've spent two days digging into this. So far I have found that there are universal, language independent verbs TaskbarPin, TaskbarUnpin and PinToStartScreen as described on Microsoft TechNet blog. Funny thing is, that TaskbarPin has been deliberately broken in KB3093266, and PinToStartScreen works as a toggle (i.e. unpins, if the app is already pinned and vice versa) and I haven't found any way how to get the current pinning status. I have also found that the tile pinning/unpinning is most likely handled by C:\Windows\SysWOW64\appresolver.dll (CLSID {470C0EBD-5D73-4d58-9CED-E91E22E23282}) but there does not seem to be any exposed function or entry point which could be called directly. The tile database is stored in %LocalAppData%\TileDataLayer\Database and there is also some registry cache under HKCU\Software\Microsoft\Windows\CurrentVersion\CloudStore\Store\Cache\DefaultAccount neither of which seem to be helpful in any way.

Another approach would be to use group policy to enforce start tiles and taskbar icons as described in Microsoft Docs and related document about the XML format. The user can still be allowed to modify the tiles using <DefaultLayoutOverride LayoutCustomizationRestrictionType="OnlySpecifiedGroups">. This would also solve a problem with reversibility quite elegantly, as the local tile database would remain unchanged and the tweak function would just enable or disable the policy. The only problem with this approach is that it's not possible to enforce an empty layout. At least one tile is required, which kinda defeats the purpose.

As for the taskbar, it has its store in HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Taskband and auxiliary folder %AppData%\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar where it stores *.lnk files (shortcuts). Taskbar can be emptied simply by removing registry key FavoritesResolve and setting key Favorites to 0xff (binary). But I'd like to make possible to remove the shortcuts one by one, which works for the applications, but seems problematic for the default File Explorer shortcut, as it's actually a PIDL (namespace) link to shell:::{52205fd8-5dfb-447d-801a-d0b52f2e83e1} and I have no idea how to handle it as application object. Moreover Shell.Application objects do not seem to list "Pin to Taskbar" in their Verbs() even though right-clicking the same item shows it.

The Workaround

Eventually, I have returned to the solution linked above and tried to figure out how it can be made universal and international. I have found that the strings like "Pin to Start" are defined in shell32.dll.mui, so I'm now (ab)using P/Invoke calls to extract the strings from DLL, which automatically loads the MUI for current user's language. The resource string IDs are as follows:

5386 = Pin to taskbar
5387 = Unpin from taskbar
51201 = Pin to start
51394 = Unpin from start

The P/Invoke part for PowerShell (actually C# / .NET) is then

$getstring = @'
    [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
    public static extern IntPtr GetModuleHandle(string lpModuleName);

    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    internal static extern int LoadString(IntPtr hInstance, uint uID, StringBuilder lpBuffer, int nBufferMax);

    public static string GetString(uint strId) {
        IntPtr intPtr = GetModuleHandle("shell32.dll");
        StringBuilder sb = new StringBuilder(255);
        LoadString(intPtr, strId, sb, sb.Capacity);
        return sb.ToString();
    }
'@
$getstring = Add-Type $getstring -PassThru -Name GetStr -Using System.Text

and the actual string can be loaded using the GetString method and ID mentioned above.

$unpinFromStart = $getstring[0]::GetString(51394)

This string then can be used instead the hardcoded ones to match and execute the proper verb.

$apps = (New-Object -Com Shell.Application).NameSpace("shell:::{4234d49b-0245-4df3-b780-3893943456e1}").Items()
$apps | Where { $_.Path -like "Microsoft.MicrosoftEdge*" } | ForEach { $_.Verbs() | Where {$_.Name -eq $unpinFromStart} | ForEach {$_.DoIt()}}
$apps | Where { $_.Path -like "Microsoft.WindowsStore*" } | ForEach { $_.Verbs() | Where {$_.Name -eq $unpinFromStart} | ForEach {$_.DoIt()}}
...

Or simply unpin all applications in one go.

(New-Object -Com Shell.Application).NameSpace("shell:::{4234d49b-0245-4df3-b780-3893943456e1}").Items() | ForEach { $_.Verbs() | Where {$_.Name -eq $unpinFromStart} | ForEach {$_.DoIt()}}

The same approach works also for the taskbar unpinning, with the exception of File Explorer link mentioned above. For taskbar pinning, the most straightforward (and incredibly ugly) solution is to store the original Favorites blobs in the script, which adds roughly 15 kB.

Update:

I have found relatively nice and clean workaround for taskbar pinning. It's based on reintroducing the pinning handler in 'all items' class scope of HKCU and then invoking it on whatever item you want to pin.

# Bring pinning / unpinning handler into '*' class scope
$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

# Pin whatever you want to pin
# ....InvokeVerb("pin")

# Remove the handler
Remove-Item -LiteralPath "HKCU:Software\Classes\*\shell\pin" -Recurse

Few more details on its usage can be found in issue #147.

The Question

Now, I'd like to know from you, admins, powerusers and other users of the script, if this workaround is acceptable. There are several points I'd like to raise:

  1. I particularly don't like the part where the P/Invoke code needs to be loaded separately before the actual pin/unpin function calls. This piece of code is too big and clunky to be repeated in every function which would want to use it, so it makes sense to have in only once and then reuse it. This basically creates internal dependency, which is something I'd like to generally avoid (for reasons specified in FAQ).
  2. The script is already quite difficult to read for users without any programming background as it is now. Adding such functionality complicates things even more.
  3. It's still not possible to unpin applications which have been suggested by Windows and are not installed (like Drawboard PDF, Asphalt 8: Airborne, Fallout Shelter, and others, depending on your location) as these are stored only in the tile database, which is out of my reach (or its CLSID has not been discovered yet, if there even is one, which it probably isn't).
  4. Proper reversibility of these tweaks is virtually impossible as the tiles can be pinned back, but the sizes, categories, order and other configuration will not be reverted to the original state.
  5. Reversibility is even more problematic if you consider that different editions of Windows in different geographical locations have different default sets of tiles. They can theoretically be read from C:\Users\Default\AppData\Local\Microsoft\Windows\Shell\DefaultLayouts.xml which would add even more complexity.

Please let me know by voting (thumbs up/down) or commenting if the branch app-pining should be merged into master as it is. Any additional research or ideas how to make it harder easier, better, faster, stronger are welcome.

@jszabo98
Copy link

You can do the taskbar pins manually coding an xml file and then running import-startlayout: https://4sysops.com/archives/pin-apps-to-the-taskbar-in-windows-10-1607-with-group-policy/ I wish I could modify the All Apps list and hide things like Edge though without uninstalling it.

@Disassembler0
Copy link
Owner Author

@jszabo98 Yeah, that's the 2nd paragraph in "The Research". The problem here is that you can't create completely empty layout using this approach and the tiles you force via GPO can't be edited by users. :( That's why I've went with the P/Invoke abuse, which gets the work done, but certainly is not what I'd call "elegant".

@jszabo98
Copy link

You can just set the taskbar pins with powershell import-startlayout, and users can do what they want with it. You're right. I tried to make an empty one, but File Explorer and IE were still there I guess from my last attempt.

@alirobe
Copy link
Contributor

alirobe commented Jul 29, 2017

I like that you've done the work, and would like to use it myself... but I also feel it's too complex to include in the base script, even if it is a function.

Perhaps rename the current .ps1 to "ConfigureWindows.ps1" and create a new file called "ConfigureWindowsStartMenu.ps1"? This file could have more disclaimers, its own tweak variable, and executor. This allows most users to easily ignore it.

@tmc-uk
Copy link

tmc-uk commented Nov 13, 2017

Hi
I'm actually loving this script!
I have been using your example to remove all Start Menu Tiles from Win10 :)

I've also been using a MS solution to Pin to the Start Menu and both together work perfectly.

Now that i've got this far i'd like to Pin items to the Taskbar (and maybe remove all first where possible), i've had a fiddle with the ID but noting seems to happen. I'd rather do it this way than XML. I use CentraStage/AEM and love automaton :) so when i'm setting up a new user or computer most of it is completed by scripts.

Hope you or someone could point me in the right direction, in the mean time i shall continue to adjust for code!

@Disassembler0
Copy link
Owner Author

As it turns out, %LocalAppData%\TileDataLayer\Database no longer exists on 1709 and the tiles are now likely stored somewhere in Cortana database. Luckily, changes in the registry cache in HKCU\Software\Microsoft\Windows\CurrentVersion\CloudStore\Store\Cache\DefaultAccount seem to be permanent now. The tile groups can be purged using some hexediting, so the script does just that without any advanced P/Invoke magic. Let's see for how long will the tweak work :)

If anyone finds a better way, I'm still eager to see it.

@farag2
Copy link

farag2 commented Nov 19, 2019

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

5 participants