Закрепляем ярлыки на начальном экране в Windows 10 у текущего пользователя

от автора

Эта статья — своего рода proof of concept, как можно закрепить программно (открепить) ярлык на начальном экране для текущего пользователя без перезапуска или выхода из учетной записи. Как вы знаете, с выходом Windows 10 October 2018 Microsoft без шума закрыл доступ к API открепления (закрепления) ярлыков от начального экрана и панели задач: отныне это можно сделать лишь вручную.

Ниже приведен пример кода для закрепления (открепления) ярлыка на начальный экран, который когда-то работал. Как можете видеть, в коде используется метод получения локализорованной строки, и для этого нам необходимо знать код строки, чтобы вызвать соответствующий пункт контекстного меню. В данном пример, чтобы закрепить ярлык командной строки, мы вызываем строку с кодом 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, добавляя необходимые нам ярлыки (ссылки должны вести на реально существующие ярлыки) и сохраняем;

  • С помощью политики временно выключаем возможность редактировать макет начального экрана;

  • Перезапускаем меню «Пуск»;

  • Программно открываем меню «Пуск», чтобы в реестре сохранился его макет;

  • Выключаем политику, чтобы можно было редактировать макет начального экрана;

  • И открываем меню «Пуск» опять.

Вуаля! В данном примере мы настроили начальный экран на лету, закрепив на него три ярлыка: Панель управления, устройства и принтеры и PowerShell.

Код целиком
<# 	.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}") }

Страница GitHub Windows 10 Sophia Script, где в том числе используется данный метод.

Огромное спасибо iNNOKENTIY21 за помощь в реализации метода.

ссылка на оригинал статьи https://habr.com/ru/post/550948/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *