Эта статья — своего рода 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/
Добавить комментарий