2

I know how to create shortcuts using Windows Script Host (link). But what I need is to create shortcuts and placed at specific locations without having to use the mouse manually to place them. Is this possible through scripting (e.g., PowerShell, etc.)?

The reason is I like to have Windows setup scripted so that the setup script can be under change control (e.g., Git).

8

1 Answer 1

1

This is rather involved because working with Explorer views like the desktop requires using several COM objects that don't support scripting, but it can be done in PowerShell by declaring .NET representations of the native functions and structures in some embedded C#. Based on this Raymond Chen article, I wrote this script:

Param(
    [Parameter(Mandatory)][string]$ShortcutTitle,
    [Parameter(Mandatory)][int]$IconX,
    [Parameter(Mandatory)][int]$IconY,
    [Parameter(Mandatory)][string]$TargetPath,
    [Parameter()][string]$Arguments = '',
    [Parameter()][string]$WorkingDirectory = ''
)

# Declarations of COM interfaces, functions, and structures for interfacing with the shell
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential)]
public struct POINT {
    public int x;
    public int y;
}

[StructLayout(LayoutKind.Sequential)]
public struct STRRET {
    int uType;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 260)]
    byte[] cStr;
}

public class PInvoke {
    [DllImport("Shlwapi.dll")]
    public static extern int StrRetToStrW(ref STRRET pstr, IntPtr pidl, [MarshalAs(UnmanagedType.LPWStr)] ref string ppsz);
}

[ComImport]
[Guid("6d5140c1-7436-11ce-8034-00aa006009fa")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ComServiceProvider {
    [return: MarshalAs(UnmanagedType.IUnknown)]
    Object QueryService(ref Guid guidService, ref Guid riid);
}

[ComImport]
[Guid("000214e2-0000-0000-c000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IShellBrowser {
    IntPtr GetWindow();
    void UnusedContextSensitiveHelp();
    void UnusedInsertMenusSB();
    void UnusedSetMenuSB();
    void UnusedRemoveMenusSB();
    void UnusedSetStatusTextSB();
    void UnusedEnableModelessSB();
    void UnusedTranslateAcceleratorSB();
    void UnusedBrowseObject();
    void UnusedGetViewStateStream();
    void UnusedGetControlWindow();
    void UnusedSendControlMsg();
    [return: MarshalAs(UnmanagedType.IUnknown)]
    Object QueryActiveShellView();
    void UnusedOnViewWindowActive();
    void UnusedSetToolbarItems();
}

[ComImport]
[Guid("1af3a467-214f-4298-908e-06b03e0b39f9")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IFolderView2 {
    void UnusedGetCurrentViewMode();
    void UnusedSetCurrentViewMode();
    [return: MarshalAs(UnmanagedType.IUnknown)]
    Object GetFolder(ref Guid riid);
    IntPtr Item(int iItemIndex);
    int ItemCount(int uFlags);
    void UnusedItems();
    void UnusedGetSelectionMarkedItem();
    void UnusedGetFocusedItem();
    POINT GetItemPosition(IntPtr pidl);
    void UnusedGetSpacing();
    void UnusedGetDefaultSpacing();
    void UnusedGetAutoArrange();
    void UnusedSelectItem();
    void SelectAndPositionItems(int cidl, [MarshalAs(UnmanagedType.LPArray)] IntPtr[] apidl, [MarshalAs(UnmanagedType.LPArray)] POINT[] apt, int dwFlags);
    void UnusedSetGroupBy();
    void UnusedGetGroupBy();
    void UnusedSetViewProperty();
    void UnusedGetViewProperty();
    void UnusedSetTileViewProperties();
    void UnusedSetExtendedTileViewProperties();
    void UnusedSetText();
    void SetCurrentFolderFlags(int dwMask, int dwFlags);
    int GetCurrentFolderFlags();
    void UnusedGetSortColumnCount();
    void UnusedSetSortColumns();
    void UnusedGetSortColumns();
    void UnusedGetItem();
    void UnusedGetVisibleItem();
    void UnusedGetSelectedItem();
    void UnusedGetSelection();
    void UnusedGetSelectionState();
    void UnusedInvokeVerbOnSelection();
    void UnusedSetViewModeAndIconSize();
    void UnusedGetViewModeAndIconSize();
    void UnusedSetGroupSubsetCount();
    void UnusedGetGroupSubsetCount();
    void UnusedSetRedraw();
    void UnusedIsMoveInSameFolder();
    void UnusedDoRename();
}

[ComImport]
[Guid("000214e6-0000-0000-c000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IShellFolder {
    void UnusedParseDisplayName();
    void UnusedEnumObjects();
    void UnusedBindToObject();
    void UnusedBindToStorage();
    void UnusedCompareIDs();
    void UnusedCreateViewObject();
    void UnusedGetAttributesOf();
    void UnusedGetUIObjectOf();
    STRRET GetDisplayNameOf(IntPtr pidl, int uFlags);
    void UnusedSetNameOf();
}
"@

# Find the desktop window
$shellWindowsType = [type]::GetTypeFromCLSID('9ba05972-f6a8-11cf-a442-00a0c90a8f39')
$shellWindows = [activator]::CreateInstance($shellWindowsType)
$csidlDesktop = 0
$empty = $null
$desktopWindow = $shellWindows.FindWindowSW([ref]$csidlDesktop, [ref]$empty, 8, 0, 1)
$shellBrowser = [ComServiceProvider].GetMethod('QueryService').Invoke($desktopWindow, @([guid]'4c96be40-915c-11cf-99d3-00aa004ae837', [guid]'000214e2-0000-0000-c000-000000000046'))
$folderView = [IShellBrowser].GetMethod('QueryActiveShellView').Invoke($shellBrowser, @())
# Uncheck "auto arrange icons" so icons can be moved
[IFolderView2].GetMethod('SetCurrentFolderFlags').Invoke($folderView, @(1, 0))
# Note how many icons were displayed before so we can know when the new one appeared
$iconCount = [IFolderView2].GetMethod('ItemCount').Invoke($folderView, @(0))

# Determine the path for the new shortcut
$wshShell = New-Object -ComObject WScript.Shell
$desktopPath = $wshShell.SpecialFolders('Desktop')
$lnkPath = "$desktopPath\$ShortcutTitle.lnk"
If (-not (Test-Path $lnkPath)) {
    # If it doesn't already exist, create it with the WScript API
    $shortcut = $wshShell.CreateShortcut($lnkPath)
    $shortcut.TargetPath = $TargetPath
    $shortcut.Arguments = $Arguments
    $shortcut.WorkingDirectory = $WorkingDirectory
    $shortcut.Save()
    # Refresh the desktop and wait for Explorer to add an icon for the new shortcut
    $desktopWindow.Refresh()
    $newIconCount = $iconCount + 1
    $tries = 0
    While ($iconCount -lt $newIconCount -and $tries -lt 30) {
        $iconCount = [IFolderView2].GetMethod('ItemCount').Invoke($folderView, @(0))
        $tries += 1
        Start-Sleep -Milliseconds 100
    }
}

# Get a representation of the folder that allows reading item names
$shellFolder = [IFolderView2].GetMethod('GetFolder').Invoke($folderView, @([guid]'000214e6-0000-0000-c000-000000000046'))
# Iterate over the icons to find the new one
0..($iconCount - 1) | % {
    # Get the ID and name of this icon
    $pidl = [IFolderView2].GetMethod('Item').Invoke($folderView, @([int]$_))
    $strret = [IShellFolder].GetMethod('GetDisplayNameOf').Invoke($shellFolder, @($pidl, 0x1000))
    $name = ''
    If ([PInvoke]::StrRetToStrW([ref]$strret, $pidl, [ref]$name) -eq 0) {
        If ($name -eq $ShortcutTitle) {
            # If it's the one we made, set its position
            $point = [POINT]::new()
            $point.x = $IconX
            $point.y = $IconY
            [IFolderView2].GetMethod('SelectAndPositionItems').Invoke($folderView, @(1, [IntPtr[]]@($pidl), [POINT[]]@($point), 128))
            # No need to look at the remaining icons - exit the script
            Break
        }
    }
}

It uses the classic WScript.Shell scripting component to create the shortcut if it doesn't already exist, then uses the shell COM APIs to find and reposition the shortcut.

For example, if that big script was saved as positionedshortcut.ps1, the below command would create a somewhat silly shortcut called "Friendly Shell" near (1000, 400) that launches a command prompt with a greeting. If a shortcut with that name already existed, it would be moved back to that position but otherwise not changed.

powershell -ExecutionPolicy Bypass -c .\positionedshortcut.ps1 -ShortcutTitle 'Friendly Shell' -IconX 1000 -IconY 400 -TargetPath 'C:\Windows\System32\cmd.exe' -Arguments '/k echo Hi there!'

To set other shortcut properties, manipulate the $shortcut object like normal. The coordinates for the new $point are in pixels; if you need to adapt to different screen sizes, you can get the screen resolution from script too.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .