Fixing shortcuts to the Start screen in Windows 10 for the current user

This article is a kind of proof of concept, how you can programmatically pin (unpin) a shortcut on the start screen for the current user without restarting or logging out of the account. As you know, with the release of Windows 10 October 2018, Microsoft quietly closed access to the API for unpinning (pinning) shortcuts from the Start screen and taskbar: from now on, this can only be done manually.





() , - . , , , . , , 51201, ยซ ยป, %SystemRoot%\system32\shell32.dll.





ResourcesExtract.





# Extract a localized string from shell32.dll
$Signature = @{
	Namespace = "WinAPI"
	Name = "GetStr"
	Language = "CSharp"
	MemberDefinition = @"
[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();
}
"@
}

if (-not ("WinAPI.GetStr" -as [type]))
{
	Add-Type @Signature -Using System.Text
}

# Pin to Start: 51201
# Unpin from Start: 51394
$LocalizedString = [WinAPI.GetStr]::GetString(51201)

# Trying to pin the Command Prompt shortcut to Start
$Target = Get-Item -Path "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\System Tools\Command Prompt.lnk"
$Shell = New-Object -ComObject Shell.Application
$Folder = $Shell.NameSpace($Target.DirectoryName)
$file = $Folder.ParseName($Target.Name)
$Verb = $File.Verbs() | Where-Object -FilterScript {$_.Name -eq $LocalizedString}
$Verb.DoIt()
      
      



Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))





, , API, , ยซ &ยป, .





- , , , Microsoft bloatware. , โ€ฆ





PowerShell- Windows 10 . . . , , , GPO .





, XML. , , : Import-StartLayout -LayoutPath "D:\Layout.xml .





, ยซ ยป (Prevent users from customizing their Start Screen), XML . , :





  • ;





  • XML, ( ) ;





  • Using the policy, temporarily disable the ability to edit the home screen layout;





  • Restart the "Start" menu;





  • Open the "Start" menu programmatically so that its layout is saved in the registry;





  • Turn off the policy so that you can edit the home screen layout;





  • And open the Start menu again.





Voila! In this example, we have customized the Start screen on the fly by pinning three shortcuts to it: Control Panel, Devices and Printers, and PowerShell.





Entire code
<#
	.SYNOPSIS
	Configure the Start tiles

	.PARAMETER ControlPanel
	Pin the "Control Panel" shortcut to Start

	.PARAMETER DevicesPrinters
	Pin the "Devices & Printers" shortcut to Start

	.PARAMETER PowerShell
	Pin the "Windows PowerShell" shortcut to Start

	.PARAMETER UnpinAll
	Unpin all the Start tiles

	.EXAMPLE
	.\Pin.ps1 -Tiles ControlPanel, DevicesPrinters, PowerShell

	.EXAMPLE
	.\Pin.ps1 -UnpinAll

	.EXAMPLE
	.\Pin.ps1 -UnpinAll -Tiles ControlPanel, DevicesPrinters, PowerShell

	.EXAMPLE
	.\Pin.ps1 -UnpinAll -Tiles ControlPanel

	.EXAMPLE
	.\Pin.ps1 -Tiles ControlPanel -UnpinAll

	.LINK
	https://github.com/farag2/Windows-10-Sophia-Script

	.NOTES
	Separate arguments with comma
	Current user
#>
[CmdletBinding()]
param
(
	[Parameter(
		Mandatory = $false,
		Position = 0
	)]
	[switch]
	$UnpinAll,

	[Parameter(
		Mandatory = $false,
		Position = 1
	)]
	[ValidateSet("ControlPanel", "DevicesPrinters", "PowerShell")]
	[string[]]
	$Tiles,

	[string]
	$StartLayout = "$PSScriptRoot\StartLayout.xml"
)

begin
{
	# Unpin all the Start tiles
	if ($UnpinAll)
	{
		Export-StartLayout -Path $StartLayout -UseDesktopApplicationID

		[xml]$XML = Get-Content -Path $StartLayout -Encoding UTF8 -Force
		$Groups = $XML.LayoutModificationTemplate.DefaultLayoutOverride.StartLayoutCollection.StartLayout.Group

		foreach ($Group in $Groups)
		{
			# Removing all groups inside XML
			$Group.ParentNode.RemoveChild($Group) | Out-Null
		}

		$XML.Save($StartLayout)
	}
}

process
{
	# Extract strings from shell32.dll using its' number
	$Signature = @{
		Namespace = "WinAPI"
		Name = "GetStr"
		Language = "CSharp"
		MemberDefinition = @"
[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();
}
"@
	}

	if (-not ("WinAPI.GetStr" -as [type]))
	{
		Add-Type @Signature -Using System.Text
	}

	# Extract the localized "Devices and Printers" string from shell32.dll
	$DevicesPrinters = [WinAPI.GetStr]::GetString(30493)

	# We need to get the AppID because it's auto generated
	$Script:DevicesPrintersAppID = (Get-StartApps | Where-Object -FilterScript {$_.Name -eq $DevicesPrinters}).AppID

	$Parameters = @(
		# Control Panel hash table
		@{
			# Special name for Control Panel
			Name = "ControlPanel"
			Size = "2x2"
			Column = 0
			Row = 0
			AppID = "Microsoft.Windows.ControlPanel"
		},
		# "Devices & Printers" hash table
		@{
			# Special name for "Devices & Printers"
			Name = "DevicesPrinters"
			Size   = "2x2"
			Column = 2
			Row    = 0
			AppID  = $Script:DevicesPrintersAppID
		},
		# Windows PowerShell hash table
		@{
			# Special name for Windows PowerShell
			Name = "PowerShell"
			Size = "2x2"
			Column = 4
			Row = 0
			AppID = "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe"
		}
	)

	# Valid columns to place tiles in
	$ValidColumns = @(0, 2, 4)
	[string]$StartLayoutNS = "http://schemas.microsoft.com/Start/2014/StartLayout"

	# Add pre-configured hastable to XML
	function Add-Tile
	{
		param
		(
			[string]
			$Size,

			[int]
			$Column,

			[int]
			$Row,

			[string]
			$AppID
		)

		[string]$elementName = "start:DesktopApplicationTile"
		[Xml.XmlElement]$Table = $xml.CreateElement($elementName, $StartLayoutNS)
		$Table.SetAttribute("Size", $Size)
		$Table.SetAttribute("Column", $Column)
		$Table.SetAttribute("Row", $Row)
		$Table.SetAttribute("DesktopApplicationID", $AppID)

		$Table
	}

	if (-not (Test-Path -Path $StartLayout))
	{
		# Export the current Start layout
		Export-StartLayout -Path $StartLayout -UseDesktopApplicationID
	}

	[xml]$XML = Get-Content -Path $StartLayout -Encoding UTF8 -Force

	foreach ($Tile in $Tiles)
	{
		switch ($Tile)
		{
			ControlPanel
			{
				$ControlPanel = [WinAPI.GetStr]::GetString(12712)
				Write-Verbose -Message ("The `"{0}`" shortcut is being pinned to Start" -f $ControlPanel) -Verbose
			}
			DevicesPrinters
			{
				$DevicesPrinters = [WinAPI.GetStr]::GetString(30493)
				Write-Verbose -Message ("The `"{0}`" shortcut is being pinned to Start" -f $DevicesPrinters) -Verbose

				# Create the old-style "Devices and Printers" shortcut in the Start menu
				$Shell = New-Object -ComObject Wscript.Shell
				$Shortcut = $Shell.CreateShortcut("$env:APPDATA\Microsoft\Windows\Start menu\Programs\System Tools\$DevicesPrinters.lnk")
				$Shortcut.TargetPath = "control"
				$Shortcut.Arguments = "printers"
				$Shortcut.IconLocation = "$env:SystemRoot\system32\DeviceCenter.dll"
				$Shortcut.Save()

				Start-Sleep -Seconds 3
			}
			PowerShell
			{
				Write-Verbose -Message ("The `"{0}`" shortcut is being pinned to Start" -f "Windows PowerShell") -Verbose
			}
		}

		$Parameter = $Parameters | Where-Object -FilterScript {$_.Name -eq $Tile}
		$Group = $XML.LayoutModificationTemplate.DefaultLayoutOverride.StartLayoutCollection.StartLayout.Group | Where-Object -FilterScript {$_.Name -eq "Sophia Script"}

		# If the "Sophia Script" group exists in Start
		if ($Group)
		{
			$DesktopApplicationID = ($Parameters | Where-Object -FilterScript {$_.Name -eq $Tile}).AppID

			if (-not ($Group.DesktopApplicationTile | Where-Object -FilterScript {$_.DesktopApplicationID -eq $DesktopApplicationID}))
			{
				# Calculate current filled columns
				$CurrentColumns = @($Group.DesktopApplicationTile.Column)
				# Calculate current free columns and take the first one
				$Column = (Compare-Object -ReferenceObject $ValidColumns -DifferenceObject $CurrentColumns).InputObject | Select-Object -First 1
				# If filled cells contain desired ones assign the first free column
				if ($CurrentColumns -contains $Parameter.Column)
				{
					$Parameter.Column = $Column
				}
				$Group.AppendChild((Add-Tile @Parameter)) | Out-Null
			}
		}
		else
		{
			# Create the "Sophia Script" group
			[Xml.XmlElement]$Group = $XML.CreateElement("start:Group", $StartLayoutNS)
			$Group.SetAttribute("Name","Sophia Script")
			$Group.AppendChild((Add-Tile @Parameter)) | Out-Null
			$XML.LayoutModificationTemplate.DefaultLayoutOverride.StartLayoutCollection.StartLayout.AppendChild($Group) | Out-Null
		}
	}

	$XML.Save($StartLayout)
}

end
{
	# Temporarily disable changing the Start menu layout
	if (-not (Test-Path -Path HKCU:\SOFTWARE\Policies\Microsoft\Windows\Explorer))
	{
		New-Item -Path HKCU:\SOFTWARE\Policies\Microsoft\Windows\Explorer -Force
	}
	New-ItemProperty -Path HKCU:\SOFTWARE\Policies\Microsoft\Windows\Explorer -Name LockedStartLayout -Value 1 -Force
	New-ItemProperty -Path HKCU:\SOFTWARE\Policies\Microsoft\Windows\Explorer -Name StartLayoutFile -Value $StartLayout -Force

	Start-Sleep -Seconds 3

	# Restart the Start menu
	Stop-Process -Name StartMenuExperienceHost -Force -ErrorAction Ignore

	Start-Sleep -Seconds 3

	# Open the Start menu to load the new layout
	$wshell = New-Object -ComObject WScript.Shell
	$wshell.SendKeys("^{ESC}")

	Start-Sleep -Seconds 3

	# Enable changing the Start menu layout
	Remove-ItemProperty -Path HKCU:\SOFTWARE\Policies\Microsoft\Windows\Explorer -Name LockedStartLayout -Force -ErrorAction Ignore
	Remove-ItemProperty -Path HKCU:\SOFTWARE\Policies\Microsoft\Windows\Explorer -Name StartLayoutFile -Force -ErrorAction Ignore

	Remove-Item -Path $StartLayout -Force

	Stop-Process -Name StartMenuExperienceHost -Force -ErrorAction Ignore

	Start-Sleep -Seconds 3

	# Open the Start menu to load the new layout
	$wshell = New-Object -ComObject WScript.Shell
	$wshell.SendKeys("^{ESC}")
}
      
      



The Windows 10 Sophia Script GitHub page , which also uses this method.





Many thanks to iNNOKENTIY21 for help in implementing the method.








All Articles