How can I mute/unmute my sound from PowerShell - winapi

Trying to write a PowerShell cmdlet that will mute the sound at start, unless already muted, and un-mute it at the end (only if it wasn't muted to begin with).
Couldn't find any PoweShell or WMI object I could use. I was toying with using Win32 functions like auxGetVolume or auxSetVolume, but couldn't quite get it to work (how to read the values from an IntPtr?).
I'm using V2 CTP2. Any ideas folks?
Thanks!

Starting with Vista you have to use the Core Audio API to control the system volume. It's a COM API that doesn't support automation and thus requires a lot of boilerplate to use from .NET and PowerShell.
Anyways the code bellow let you access the [Audio]::Volume and [Audio]::Mute properties from PowerShell. This also work on a remote computer which could be useful. Just copy-paste the code in your PowerShell window.
Add-Type -TypeDefinition #'
using System.Runtime.InteropServices;
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IAudioEndpointVolume {
// f(), g(), ... are unused COM method slots. Define these if you care
int f(); int g(); int h(); int i();
int SetMasterVolumeLevelScalar(float fLevel, System.Guid pguidEventContext);
int j();
int GetMasterVolumeLevelScalar(out float pfLevel);
int k(); int l(); int m(); int n();
int SetMute([MarshalAs(UnmanagedType.Bool)] bool bMute, System.Guid pguidEventContext);
int GetMute(out bool pbMute);
}
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDevice {
int Activate(ref System.Guid id, int clsCtx, int activationParams, out IAudioEndpointVolume aev);
}
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDeviceEnumerator {
int f(); // Unused
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice endpoint);
}
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] class MMDeviceEnumeratorComObject { }
public class Audio {
static IAudioEndpointVolume Vol() {
var enumerator = new MMDeviceEnumeratorComObject() as IMMDeviceEnumerator;
IMMDevice dev = null;
Marshal.ThrowExceptionForHR(enumerator.GetDefaultAudioEndpoint(/*eRender*/ 0, /*eMultimedia*/ 1, out dev));
IAudioEndpointVolume epv = null;
var epvid = typeof(IAudioEndpointVolume).GUID;
Marshal.ThrowExceptionForHR(dev.Activate(ref epvid, /*CLSCTX_ALL*/ 23, 0, out epv));
return epv;
}
public static float Volume {
get {float v = -1; Marshal.ThrowExceptionForHR(Vol().GetMasterVolumeLevelScalar(out v)); return v;}
set {Marshal.ThrowExceptionForHR(Vol().SetMasterVolumeLevelScalar(value, System.Guid.Empty));}
}
public static bool Mute {
get { bool mute; Marshal.ThrowExceptionForHR(Vol().GetMute(out mute)); return mute; }
set { Marshal.ThrowExceptionForHR(Vol().SetMute(value, System.Guid.Empty)); }
}
}
'#
Usage sample:
PS C:\> [Audio]::Volume # Check current volume (now about 10%)
0,09999999
PS C:\> [Audio]::Mute # See if speaker is muted
False
PS C:\> [Audio]::Mute = $true # Mute speaker
PS C:\> [Audio]::Volume = 0.75 # Set volume to 75%
PS C:\> [Audio]::Volume # Check that the changes are applied
0,75
PS C:\> [Audio]::Mute
True
PS C:\>
There are more comprehensive .NET wrappers out there for the Core Audio API if you need one but I'm not aware of a set of PowerShell friendly cmdlets.
P.S. Diogo answer seems clever but it doesn't work for me.

Use the following commands on a ps1 powershell script:
$obj = new-object -com wscript.shell
$obj.SendKeys([char]173)

Alexandre's answer fit my situation, but his example does not work due to compilation errors regarding the namespace of 'var'. It seems that newer/different versions of .net may cause the example not to work. If you found that you received compilation errors, this is an alternative version to try for those cases:
Add-Type -Language CsharpVersion3 -TypeDefinition #'
using System.Runtime.InteropServices;
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IAudioEndpointVolume {
// f(), g(), ... are unused COM method slots. Define these if you care
int f(); int g(); int h(); int i();
int SetMasterVolumeLevelScalar(float fLevel, System.Guid pguidEventContext);
int j();
int GetMasterVolumeLevelScalar(out float pfLevel);
int k(); int l(); int m(); int n();
int SetMute([MarshalAs(UnmanagedType.Bool)] bool bMute, System.Guid pguidEventContext);
int GetMute(out bool pbMute);
}
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDevice {
int Activate(ref System.Guid id, int clsCtx, int activationParams, out IAudioEndpointVolume aev);
}
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDeviceEnumerator {
int f(); // Unused
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice endpoint);
}
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] class MMDeviceEnumeratorComObject { }
public class Audio {
static IAudioEndpointVolume Vol() {
var enumerator = new MMDeviceEnumeratorComObject() as IMMDeviceEnumerator;
IMMDevice dev = null;
Marshal.ThrowExceptionForHR(enumerator.GetDefaultAudioEndpoint(/*eRender*/ 0, /*eMultimedia*/ 1, out dev));
IAudioEndpointVolume epv = null;
var epvid = typeof(IAudioEndpointVolume).GUID;
Marshal.ThrowExceptionForHR(dev.Activate(ref epvid, /*CLSCTX_ALL*/ 23, 0, out epv));
return epv;
}
public static float Volume {
get {float v = -1; Marshal.ThrowExceptionForHR(Vol().GetMasterVolumeLevelScalar(out v)); return v;}
set {Marshal.ThrowExceptionForHR(Vol().SetMasterVolumeLevelScalar(value, System.Guid.Empty));}
}
public static bool Mute {
get { bool mute; Marshal.ThrowExceptionForHR(Vol().GetMute(out mute)); return mute; }
set { Marshal.ThrowExceptionForHR(Vol().SetMute(value, System.Guid.Empty)); }
}
}
'#
Usage is the same:
PS C:\> [Audio]::Volume # Check current volume (now about 10%)
0,09999999
PS C:\> [Audio]::Mute # See if speaker is muted
False
PS C:\> [Audio]::Mute = $true # Mute speaker
PS C:\> [Audio]::Volume = 0.75 # Set volume to 75%
PS C:\> [Audio]::Volume # Check that the changes are applied
0,75
PS C:\> [Audio]::Mute
True
PS C:\>

You could skin the cat another way by simply managing the Windows Audio Service. Stop it to mute, start it to unmute.

There does not seem to be a quick and easy way to adjust the volume.. If you have c++ experience, you could do something with this blog post, where Larry Osterman describes how to call the IAudioEndpointVolume interface from the platform api(for Vista, XP might be more difficult from what I've found in a few searches).
V2 does allow you to compile inline code (via Add-Type), so that might be an option.

I know it isn't PowerShell, but combining the answers from Michael and Diogo gives a one-line VBScript solution:
CreateObject("WScript.Shell").SendKeys(chr(173))
Slap this in mute.vbs, and you can just double-click to toggle mute
still works in Windows 10 (10586.104)
no need to Set-ExecutionPolicy as you might with PowerShell

Solution in vbscript:
Set WshShell = CreateObject("WScript.Shell")
For i = 0 To 50
WshShell.SendKeys(chr(174))
WScript.Sleep 100
Next
The keys reduce the volume by 2 each time.

I didn't find how to do this in PowerShell, but there is a command-line utility called NirCmd that will do the trick by running this command:
C:\utils\nircmd.exe mutesysvolume 0 # 1 to to unmute, 2 to toggle
NirCmd is available for free here:
http://www.nirsoft.net/utils/nircmd.html

Check out my answer to Change audio level from powershell?
Set-DefaultAudioDeviceMute

lemme try again, after getting some feedback on my answer
This is based on #frgnca's AudioDeviceCmdlets, found here: https://github.com/frgnca/AudioDeviceCmdlets
Here is code that will mute every recording device.
Import-Module .\AudioDeviceCmdlets
$audio_device_list = Get-AudioDevice -list
$recording_devices = $audio_device_list | ? {$_.Type -eq "Recording"}
$recording_devices
$recording_device_index = $recording_devices.Index | Out-String -stream
foreach ($i in $recording_device_index) {
$inti = [int]$i
Set-AudioDevice $inti | out-null -erroraction SilentlyContinue
Set-AudioDevice -RecordingMute 1 -erroraction SilentlyContinue
}
You import the AudioDeviceCmdlets dll, then save a list of all audio devices, filtered down into recording devices. You grab the index of all the recording devices and then iterate through each one, first setting the device to be your primary audio device, then setting that device to mute (this 2 step process is a limitation imposed by the dll).
To unmute everything, you change -RecordingMute 1 to RecordingMute 0
Similarly, to mute playback devices you can use this code:
Import-Module .\AudioDeviceCmdlets
$audio_device_list = Get-AudioDevice -list
$playback_devices = $audio_device_list | ? {$_.Type -eq "Playback"}
$playback_devices
$playback_device_index = $playback_devices.Index | Out-String -stream
foreach ($i in $playback_device_index) {
$inti = [int]$i
Set-AudioDevice $inti | out-null -erroraction SilentlyContinue
Set-AudioDevice -PlaybackMute 1 -erroraction SilentlyContinue
}
To unmute everything, you change -PlaybackMute 1 to PlaybackMute 0
This code comes from part of a bigger project I have which hooks up to a physical button/LED mute status display via Arduino to enable single button press to Mute/Unmute all system microphones (to help with Zoom, Meet, Teams, Discord, etc.) and to help people avoid embarrassing hot-mic incidents.
https://github.com/dakota-mewt/mewt
P.S. I really like some of the one-line solutions like Change audio level from powershell?
However, note that a single-line muting typically applies only to the single device that's currently set as the windows default. So if you have software that's capable of addressing different audio devices (like Zoom and Meet), and they happen to be using a non-default device, it may not work as desired.

In the second script above, to work in PowerShell 7 or PowerShell Core in the 1st line change:
-Language CsharpVersion3
to...
-Language Csharp
Working on W10

Here's a simple timed mute script using autohotkey (from autohotkey dotcom).
mbutton:: ; Trigger this script with a click on the mouse wheel
SoundSet, +1,, Mute ; Mute the PC sound
sleep, 27000 ; Wait 27 seconds for the annoying advertisement to run
SoundSet, +1,, Mute ; Unmute the PC sound

Related

How can two of the same model USB touch screens be differentiated on Windows 10?

I have two of the same model touch screen monitors connected to a Windows 10 Machine. The monitors are connected with HDMI for image and USB for touch input.
When I plug everything in and set it up using the build-in calibration "multidigimon.exe" I can set everything up so the touch screens work as expected.
However after a restart sometimes the touch inputs are registered on the wrong screen, so touching the right screen makes stuff happen on the left, and touching on the left screen makes stuff happen on the right screen.
I've already tried to see if I can find a way to have a script correct the problem, here is what I've figured out so far:
multidigimon.exe writes registry keys in HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Wisp\Pen\Digimon. As the key it uses the Windows Object Manager path that corresponds to the USB touch device. As the value it uses the Windows Object Manager path that corresponds to the Display device. (I can see both of them with WinObj under "GLOBAL??").
Exporting the two entries into a .reg file look like this:
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Wisp\Pen\Digimon]
"20-\\\\?\\HID#VID_1FF7&PID_0F27&Col04#a&25dfa661&0&0003#{4d1e55b2-f16f-11cf-88cb-001111000030}"="\\\\?\\DISPLAY#IVM1A3E#5&1778d8b3&1&UID260#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"
"20-\\\\?\\HID#VID_1FF7&PID_0F27&Col04#a&29d74c67&0&0003#{4d1e55b2-f16f-11cf-88cb-001111000030}"="\\\\?\\DISPLAY#IVM1A3E#5&1778d8b3&1&UID256#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"
It consists of mostly the device instance path that can be seen in device manager under details for the device. In this case HID\VID_1FF7&PID_0F27&Col04\A&25DFA661&0&0003 and HID\VID_1FF7&PID_0F27&COL04\A&29D74C67&0&0003 the \ replaced with # and the class GUID also appended after another #. Info in part from this stackoverflow answer.
Part of the device instance path is explained in this stackoverflow answer, but that only explains it for USB devices, what I'm dealing with is a HID device. So the VID_XXXX and PID_XXXX seem to mean the same thing, but ColXX is not explained, the part after the last \ is the instance specific id.
After a restart it's random what actual touch HID device gets what instance specific id. So sometimes the right touch screen has the device instance path HID\VID_1FF7&PID_0F27&Col04\A&25DFA661&0&0003 and sometimes it has HID\VID_1FF7&PID_0F27&COL04\A&29D74C67&0&0003, this seems pretty random*. The left touch screen gets the device instance path that the right one dosen't have.
*It probably depends on what screen starts up faster (they automatically turn on when the PC boots). As when I unplug the touch screen devices USB after boot and plug in one at a time, the first one always gets the same instance specific id.
Is there a way to tell the difference between the two devices? Maybe get information about what USB Port it is plugged into somehow?
Found a way to do it using PowerShell with embedded C# that uses the Win32 API.
Devices on Windows are on a device tree. Windows provides ways to navigate that tree in Cfgmgr32.h. Since in my case the devices were USB devices, I could use USB Device Tree Viewer to visualize and experiment before writing code.
Strangely the TouchScreens I connected don't identify as a single USB device, but instead as multiple USB Hubs behind one another with some devices connected (and some ports not connected), while the displays don't offer any USB Ports to plug something in. USB Device Tree Viwer also displays the Child devices of USB devices, even the HID devices I was looking for.
After testing for a while with unplugging and replugging the TouchScreens in different USB Ports and looking at what USB Device Tree Viwer showd my I could see that the Device IDs (in reality it is the Device instance ID) don't always stay exactly the same. The Device instance ID is is made up of the device ID and instance id, sometimes the instance id completly changed after replugging the USB devices.
I could also note the part of the Location IDs that stayed the same for the USB Ports even after replugging and restarting windows.
So the steps I needed to take in order to make the touch screens work across restarts are:
Find the HID Devices staring with the specified Vendor and Product ID, Followed by COL04 that is connected to the right USB Port (checkd with the Location ID / Location Path).
Remove the old probably wrong values under the HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon
Add the newly found device paths in the right format as values in there. (Luckily the display devices keep the exact same path after restarts). I looked at the format after normally applying the touch screen to display mapping with the build in tools and just hacked together the right format with string interpolation.
Get Windows to read in the new configuration. For that I found that wisptis.exe dosen't exist at all on my machine. After some testing I found that just terminating dwm.exe makes windows read in the new config. It also blacks the screen for a moment ad dwm.exe ist the desktop window manager, but doing it right after startup dosen't cause any problems. Even windows that are opened stay at the same position.
I put all of that together into the following script, I've used the windows task scheduler to run this script after the system is booted (I can't run it any earlier as the screens boot up slower than the PC, so that the USB devices are only "plugged in" after the PC is booted):
$code = #"
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
public class Program
{
// Required Constants from cfgmgr32.h
public const int CM_LOCATE_DEVNODE_NORMAL = 0x00000000;
public const int CR_SUCCESS = 0x00000000;
public const int CM_DRP_LOCATION_PATHS = 0x00000024;
// The function that finds all devices starting with a specified device id that are connected through the specified
// connection (location path). Since not all devices directly have a location path it walks up the devicetree untill
// a node that hase a location path is found and then checks that path if it matches.
public static List<string> GetDeviceIdStartingWithAt(string match, string connection)
{
var devices = FindAllDevicesStartingWith(match);
var ret = new List<string>();
foreach (var device in devices)
{
IntPtr cur = device;
while (!HasLocationPath(cur))
{
IntPtr next;
int result = CM_Get_Parent(out next, cur, 0);
if (result != CR_SUCCESS) {
break;
}
cur = next;
}
bool mark = false;
foreach (var location in GetLocationPath(cur))
{
if (location.StartsWith(connection))
{
mark = true;
}
}
if (mark)
{
ret.Add(GetDeviceId(device));
}
}
return ret;
}
static List<IntPtr> FindAllDevicesStartingWith(string match)
{
IntPtr rootDevice;
CM_Locate_DevNodeA(out rootDevice, "", CM_LOCATE_DEVNODE_NORMAL);
return FindMatchingChildren(match, rootDevice);
}
// Recursive function that gets all children for a device and filters using the DeviceId to only return children
// whose deviceId starts as requested
static List<IntPtr> FindMatchingChildren(string match, IntPtr device)
{
var children = GetAllChildren(device);
var ret = new List<IntPtr>();
foreach (var child in children)
{
if (GetDeviceId(child).StartsWith(match))
{
ret.Add(child);
}
ret.AddRange(FindMatchingChildren(match, child));
}
return ret;
}
// Function implementing the way to get all direct children of a specified node as described here:
// https://learn.microsoft.com/en-us/windows/win32/api/cfgmgr32/nf-cfgmgr32-cm_get_child
static List<IntPtr> GetAllChildren(IntPtr device)
{
IntPtr firstChild;
if (CM_Get_Child(out firstChild, device, 0) != CR_SUCCESS)
{
return new List<IntPtr>();
}
var ret = new List<IntPtr>();
ret.Add(firstChild);
IntPtr cur = firstChild;
int result;
do
{
IntPtr next;
result = CM_Get_Sibling(out next, cur, 0);
if (result == CR_SUCCESS)
{
ret.Add(next);
cur = next;
}
} while (result == CR_SUCCESS);
return ret;
}
// Just a quick helper function that checks if a device has a Location Path, because not all devices do. In my
// testing devices that have a device ID starting with HID mostly don't have a location.
static bool HasLocationPath(IntPtr device)
{
Microsoft.Win32.RegistryValueKind kind;
uint length = 0;
CM_Get_DevNode_Registry_Property(device, CM_DRP_LOCATION_PATHS, out kind, IntPtr.Zero, ref length, 0);
return length > 0;
}
// Wrapper to easily get the Location paths, the one starting with PCIROOT seems to stay the same across restarts
// and also identifies what port something is connected to. The value returned is of type REG_MULTI_SZ
// REG_MULTI_SZ is explained here: https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-value-types
static string[] GetLocationPath(IntPtr device)
{
Microsoft.Win32.RegistryValueKind kind;
uint length = 0;
CM_Get_DevNode_Registry_Property(device, CM_DRP_LOCATION_PATHS, out kind, IntPtr.Zero, ref length, 0);
if (length <= 0)
return Array.Empty<string>();
IntPtr buffer = Marshal.AllocHGlobal((int)length);
CM_Get_DevNode_Registry_Property(device, CM_DRP_LOCATION_PATHS, out kind, buffer, ref length, 0);
string ret = Marshal.PtrToStringUni(buffer, (int)length/2);
Marshal.FreeHGlobal(buffer);
return ret.Substring(0, ret.Length-2).Split('\0');
}
// Wrapper around CM_Get_Device_ID, dosen't check for errors
static string GetDeviceId(IntPtr device)
{
uint length = 0;
CM_Get_Device_ID_Size(ref length, device, 0);
IntPtr buffer = Marshal.AllocHGlobal((int)length + 1);
CM_Get_Device_ID(device, buffer, length + 1, 0);
string ret = Marshal.PtrToStringAnsi(buffer);
Marshal.FreeHGlobal(buffer);
return ret;
}
// Win32 Functions required to get data. More information can be found here:
// https://learn.microsoft.com/en-us/windows/win32/api/cfgmgr32/
[DllImport("cfgmgr32.dll", SetLastError = true)]
static extern int CM_Locate_DevNodeA(out IntPtr pdnDevInst, string pDeviceID, int ulFlags);
[DllImport("cfgmgr32.dll", SetLastError = true)]
static extern int CM_Get_DevNode_Registry_Property(
IntPtr deviceInstance,
uint property,
out Microsoft.Win32.RegistryValueKind pulRegDataType,
IntPtr buffer,
ref uint length,
uint flags);
[DllImport("cfgmgr32.dll", SetLastError = true)]
static extern int CM_Get_Parent(out IntPtr pdnDevInst, IntPtr dnDevInst, int uFlags);
[DllImport("cfgmgr32.dll", SetLastError = true)]
static extern int CM_Get_Child(out IntPtr pdnDevInst, IntPtr dnDevInst, int uFlags);
[DllImport("cfgmgr32.dll", SetLastError = true)]
static extern int CM_Get_Sibling(out IntPtr pdnDevInst, IntPtr dnDevInst, int uFlags);
[DllImport("cfgmgr32.dll", SetLastError = true)]
static extern int CM_Get_Device_ID(IntPtr pdnDevInst, IntPtr buffer, uint bufferLen, uint flags);
[DllImport("cfgmgr32.dll", SetLastError = true)]
static extern int CM_Get_Device_ID_Size(ref uint pulLen, IntPtr dnDevInst, uint flags);
}
"#
Add-Type -TypeDefinition $code -Language CSharp
# Find the two touch screen devices
do {
$left = [Program]::GetDeviceIdStartingWithAt("HID\VID_1FF7&PID_0F27&COL04", "PCIROOT(0)#PCI(0801)#PCI(0003)#USBROOT(0)#USB(1)#USB(3)")
$right = [Program]::GetDeviceIdStartingWithAt("HID\VID_1FF7&PID_0F27&COL04", "PCIROOT(0)#PCI(0801)#PCI(0004)#USBROOT(0)#USB(1)#USB(4)")
if ( ($left.Count -ge 1) -and ($right.Count -ge 1)) {
break
}
Start-Sleep 1
} while ($true)
if (($left.Count -gt 1) -or ($right.Count -gt 1)) {
# Too many matching devices found! Abort!
Return
}
# Truncate and convert them to lower case for use in the Registry Key
$left = $left.Substring(28, 12).ToLower()
$right = $right.Substring(28, 12).ToLower()
# Delete the old values in the registry
$properties = Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon
foreach ($property in $properties.PSObject.Properties) {
if ($property.Name.StartsWith("20-")) {
Remove-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon -Name $property.Name
}
}
# Adding the found devices in the registry. In my case the touch screens offer finger and pen input. The difference is Col04 and Col06 before the device instance id and after it 0003 or 0005
New-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon -Name "20-\\?\HID#VID_1FF7&PID_0F27&Col04#$left&0003#{4d1e55b2-f16f-11cf-88cb-001111000030}" -PropertyType String -Value "\\?\DISPLAY#IVM1A3E#5&1778d8b3&1&UID256#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"
New-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon -Name "20-\\?\HID#VID_1FF7&PID_0F27&Col06#$left&0005#{4d1e55b2-f16f-11cf-88cb-001111000030}" -PropertyType String -Value "\\?\DISPLAY#IVM1A3E#5&1778d8b3&1&UID256#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"
New-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon -Name "20-\\?\HID#VID_1FF7&PID_0F27&Col04#$right&0003#{4d1e55b2-f16f-11cf-88cb-001111000030}" -PropertyType String -Value "\\?\DISPLAY#IVM1A3E#5&1778d8b3&1&UID260#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"
New-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon -Name "20-\\?\HID#VID_1FF7&PID_0F27&Col06#$right&0005#{4d1e55b2-f16f-11cf-88cb-001111000030}" -PropertyType String -Value "\\?\DISPLAY#IVM1A3E#5&1778d8b3&1&UID260#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"
# To apply the settings change the Desktop Window Manager is stopped. This dosen't look great as both screens flash black for a second before a new dwm instance is automatically started.
# There is probably a way to send some message to the dwm to read the configuration, but I haven't found it anywhere.
Stop-Process -Name dwm -Force

Capturing output/error in PowerShell of process running as other user

This is a variation of a question that has been asked and answered before.
The variation lies in using UserName and Password to set up the System.Diagnostics.ProcessStartInfo object. In this case, we're not able to read the output and error streams of the process – which makes sense because the process does not belong to us!
But even so, we've spawned the process so it should be possible to capture the output.
I suspect that this is a duplicate but it seems to have been misunderstood in the answer section.
You can capture the output streams from an (invariably non-elevated) process you've launched with a different user identity, as the following self-contained example code shows:
Note:
Does not work if you execute the command via PowerShell remoting, such as via Invoke-Command -ComputerName, including JEA.
See the bottom section for a - cumbersome - workaround.
However, this workaround is not needed with JEA, as you report in a comment:
JEA sessions actually support RunAsCredential (in addition to virtual accounts and group-managed service accounts) such that we can simply run as the intended common user and thus obviate the need to change user context during the session.
Independently of whether you impersonate another user, you may also run into the infamous double-hop problem when accessing network resources in the remote session, as Ash notes.
The code prompts for the target user's credentials.
The working directory must be set to a directory path that the target user is permitted to access, and defaults to their local profile folder below - assuming it exists; adjust as needed.
Stdout and stderr output are captured separately, in full, as multi-line strings.
If you want to merge the two streams, call your target program via a shell and use its redirection features (2>&1).
The example call below performs a call to a shell, namely cmd.exe via its /c parameter, outputting one line to stdout, the other to stderr (>&2). If you modify the Arguments = ... line as follows, the stderr stream would be merged into the stdout stream:
Arguments = '/c "(echo success & echo failure >&2) 2>&1"'
The code works in both Windows PowerShell and PowerShell (Core) 7+, and guards against potential deadlocks by reading the streams asynchronously.[1]
In the install-on-demand, cross-platform PowerShell (Core) 7+ edition, the implementation is more efficient, as it uses dedicated threads to wait for the asynchronous tasks to complete, via ForEach-Object -Parallel.
In the legacy, ships-with-Windows Windows PowerShell edition, periodic polling, interspersed with Start-Sleep calls, must be used to see if the asynchronous tasks have completed.
If you only need to capture one stream, e.g., only stdout, if you've merged stderr into it (as described above), the implementation can be simplified to a synchronous $stdout = $ps.StandardOutput.ReadToEnd() call, as shown in this answer.
# Prompt for the target user's credentials.
$cred = Get-Credential
# The working directory for the new process.
# IMPORTANT: This must be a directory that the target user is permitted to access.
# Here, the target user's local profile folder is used.
# Adjust as needed.
$workingDir = Join-Path (Split-Path -Parent $env:USERPROFILE) $cred.UserName
# Start the process.
$ps = [System.Diagnostics.Process]::Start(
[System.Diagnostics.ProcessStartInfo] #{
FileName = 'cmd'
Arguments = '/c "echo success & echo failure >&2"'
UseShellExecute = $false
WorkingDirectory = $workingDir
UserName = $cred.UserName
Password = $cred.Password
RedirectStandardOutput = $true
RedirectStandardError = $true
}
)
# Read the output streams asynchronously, to avoid a deadlock.
$tasks = $ps.StandardOutput.ReadToEndAsync(), $ps.StandardError.ReadToEndAsync()
if ($PSVersionTable.PSVersion.Major -ge 7) {
# PowerShell (Core) 7+: Wait for task completion in background threads.
$tasks | ForEach-Object -Parallel { $_.Wait() }
} else {
# Windows PowerShell: Poll periodically to see when both tasks have completed.
while ($tasks.IsComplete -contains $false) {
Start-Sleep -MilliSeconds 100
}
}
# Wait for the process to exit.
$ps.WaitForExit()
# Sample output: exit code and captured stream contents.
[pscustomobject] #{
ExitCode = $ps.ExitCode
StdOut = $tasks[0].Result.Trim()
StdErr = $tasks[1].Result.Trim()
} | Format-List
Output:
ExitCode : 0
StdOut : success
StdErr : failure
If running as a given user WITH ELEVATION (as admin) is required:
By design, you cannot both request elevation with -Verb RunAs and run with a different user identity (-Credential) in a single operation - neither with Start-Process nor with the underlying .NET API, System.Diagnostics.Process.
If you request elevation and you're an administrator yourself, the elevated process will run as you - assuming you've confirmed the Yes / No form of the UAC dialog presented.
Otherwise, UAC will present a credentials dialog, requiring you to provide an administrator's credentials - and there's no way to preset these credentials, not even the username.
By design, you cannot directly capture output from an elevated process you've launched - even if the elevated process runs with your own identity.
However, if you launch a non-elevated process as a different user, you can capture the output, as shown in the top section.
To get what you're looking for requires the following approach:
You need two Start-Process calls:
The first one to launch an - of necessity - non-elevated process as the target user (-Credential)
A second one launched from that process to request elevation, which then elevates in the context of the target user, assuming they're an administrator.
Because you can only capture output from inside the elevated process itself, you'll need to launch your target program via a shell and use its redirection (>) features to capture output in files.
Unfortunately, this makes for a nontrivial solution, with many subtleties to consider.
Here's a self-contained example:
It executes the commands whoami and net session (which only succeeds in an elevated session) and captures their combined stdout and stderr output in file out.txt in the specified working directory.
It executes synchronously, i.e. it waits for the elevated target process to exit before continuing; if that isn't a requirement Remove -PassThru and the enclosing (...).WaitForExit(), as well as -Wait from the nested Start-Process call.
Note: The reason that -Wait cannot also be used in the outer Start-Process call is a bug, still present as of PowerShell 7.2.2. - see GitHub issue #17033.
As instructed in the source-code comments:
when you're prompted for the target user's credentials, be sure to specify an administrator's credentials, to ensure that elevation with that user's identity succeeds.
in $workingDir, specify a working directory that the target user is permitted to access, even from a non-elevated session. The target user's local profile is used by default - assuming it exists.
# Prompt for the target user's credentials.
# IMPORTANT: Must be an *administrator*
$cred = Get-Credential
# The working directory for both the intermediate non-elevated
# and the ultimate elevated process.
# IMPORTANT: This must be a directory that the target user is permitted to access,
# even when non-elevated.
# Here, the target user's local profile folder is used.
# Adjust as needed.
$workingDir = Join-Path (Split-Path $env:USERPROFILE) $cred.UserName
(Start-Process -PassThru -WorkingDirectory $workingDir -Credential $cred -WindowStyle Hidden powershell.exe #'
-noprofile -command Start-Process -Wait -Verb RunAs powershell \"
-noexit -command `"Set-Location -LiteralPath \`\"$($PWD.ProviderPath)\`\"; & { whoami; net session } 2>&1 > out.txt`"
\"
'#).WaitForExit()
Launching a process as another user in the context of PowerShell remoting (WinRM)
You yourself discovered this answer, which explains that the CreateProcessWithLogon() Windows API function - which .NET (and therefore PowerShell) uses under the hood when starting a process as another user - won't work in batch-logon scenarios, as used by services such as WinRM. A call to CreateProcessAsUser() is required instead, which can be passed a batch-logon user token explicitly created beforehand with LogonUser().
The following self-contained example builds on this C#-based answer, but there are important prerequisites and limitations:
The calling user account must be granted the SE_ASSIGNPRIMARYTOKEN_NAME aka "SeAssignPrimaryTokenPrivilege" aka "Replace a process level token" privilege.
Interactively, you can use secpol.msc to modify user privileges (Local Policy > User Rights Assignment); after modification, a logoff / reboot is required.
If the caller is missing this privilege, you'll get an error stating A required privilege is not held by the client.
The target user account must be a member of the Administrators group.
If the target user isn't in that group, you'll get an error stating The handle is invalid.
No attempt is made to the capture the target process' output in memory; instead, the process calls cmd.exe and uses its redirection operator (>) to send the output to a file.
# Abort on all errors.
$ErrorActionPreference = 'Stop'
Write-Verbose -Verbose 'Compiling C# helper code...'
Add-Type #'
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security;
public class ProcessHelper
{
static ProcessHelper()
{
UserToken = IntPtr.Zero;
}
private static IntPtr UserToken { get; set; }
// Launch and return right away, with the process ID.
// CAVEAT: While you CAN get a process object with Get-Process -Id <pidReturned>, and
// waiting for the process to exit with .WaitForExit() does work,
// you WON'T BE ABLE TO QUERY THE EXIT CODE:
// In PowerShell, .ExitCode returns $null, suggesting that an exception occurs,
// which PowerShell swallows.
// https://learn.microsoft.com/en-US/dotnet/api/System.Diagnostics.Process.ExitCode lists only two
// exception-triggering conditions, *neither of which apply here*: the process not having exited yet, the process
// object referring to a process on a remote computer.
// Presumably, the issue is related to missing the PROCESS_QUERY_LIMITED_INFORMATION access right on the process
// handle due to the process belonging to a different user.
public int StartProcess(ProcessStartInfo processStartInfo)
{
LogInOtherUser(processStartInfo);
Native.STARTUPINFO startUpInfo = new Native.STARTUPINFO();
startUpInfo.cb = Marshal.SizeOf(startUpInfo);
startUpInfo.lpDesktop = string.Empty;
Native.PROCESS_INFORMATION processInfo = new Native.PROCESS_INFORMATION();
bool processStarted = Native.CreateProcessAsUser(UserToken, processStartInfo.FileName, processStartInfo.Arguments,
IntPtr.Zero, IntPtr.Zero, true, 0, IntPtr.Zero, null,
ref startUpInfo, out processInfo);
if (!processStarted)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
UInt32 processId = processInfo.dwProcessId;
Native.CloseHandle(processInfo.hProcess);
Native.CloseHandle(processInfo.hThread);
return (int) processId;
}
// Launch, wait for termination, return the process exit code.
public int RunProcess(ProcessStartInfo processStartInfo)
{
LogInOtherUser(processStartInfo);
Native.STARTUPINFO startUpInfo = new Native.STARTUPINFO();
startUpInfo.cb = Marshal.SizeOf(startUpInfo);
startUpInfo.lpDesktop = string.Empty;
Native.PROCESS_INFORMATION processInfo = new Native.PROCESS_INFORMATION();
bool processStarted = Native.CreateProcessAsUser(UserToken, processStartInfo.FileName, processStartInfo.Arguments,
IntPtr.Zero, IntPtr.Zero, true, 0, IntPtr.Zero, null,
ref startUpInfo, out processInfo);
if (!processStarted)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
UInt32 processId = processInfo.dwProcessId;
Native.CloseHandle(processInfo.hThread);
// Wait for termination.
if (Native.WAIT_OBJECT_0 != Native.WaitForSingleObject(processInfo.hProcess, Native.INFINITE)) {
throw new Win32Exception(Marshal.GetLastWin32Error());
}
// Get the exit code
UInt32 dwExitCode;
if (! Native.GetExitCodeProcess(processInfo.hProcess, out dwExitCode)) {
throw new Win32Exception(Marshal.GetLastWin32Error());
}
Native.CloseHandle(processInfo.hProcess);
return (int) dwExitCode;
}
// Log in as the target user and save the logon token in an instance variable.
private static void LogInOtherUser(ProcessStartInfo processStartInfo)
{
if (UserToken == IntPtr.Zero)
{
IntPtr tempUserToken = IntPtr.Zero;
string password = SecureStringToString(processStartInfo.Password);
bool loginResult = Native.LogonUser(processStartInfo.UserName, processStartInfo.Domain, password,
Native.LOGON32_LOGON_BATCH, Native.LOGON32_PROVIDER_DEFAULT,
ref tempUserToken);
if (loginResult)
{
UserToken = tempUserToken;
}
else
{
Native.CloseHandle(tempUserToken);
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
}
private static String SecureStringToString(SecureString value)
{
IntPtr stringPointer = Marshal.SecureStringToBSTR(value);
try
{
return Marshal.PtrToStringBSTR(stringPointer);
}
finally
{
Marshal.FreeBSTR(stringPointer);
}
}
public static void ReleaseUserToken()
{
Native.CloseHandle(UserToken);
}
}
internal class Native
{
internal const Int32 LOGON32_LOGON_BATCH = 4;
internal const Int32 LOGON32_PROVIDER_DEFAULT = 0;
internal const UInt32 INFINITE = 4294967295;
internal const UInt32 WAIT_OBJECT_0 = 0x00000000;
internal const UInt32 WAIT_TIMEOUT = 0x00000102;
[StructLayout(LayoutKind.Sequential)]
internal struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public UInt32 dwProcessId;
public UInt32 dwThreadId;
}
[StructLayout(LayoutKind.Sequential)]
internal struct STARTUPINFO
{
public int cb;
[MarshalAs(UnmanagedType.LPStr)]
public string lpReserved;
[MarshalAs(UnmanagedType.LPStr)]
public string lpDesktop;
[MarshalAs(UnmanagedType.LPStr)]
public string lpTitle;
public UInt32 dwX;
public UInt32 dwY;
public UInt32 dwXSize;
public UInt32 dwYSize;
public UInt32 dwXCountChars;
public UInt32 dwYCountChars;
public UInt32 dwFillAttribute;
public UInt32 dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
public UInt32 nLength;
public IntPtr lpSecurityDescriptor;
public bool bInheritHandle;
}
[DllImport("advapi32.dll", SetLastError = true)]
internal extern static bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);
[DllImport("advapi32.dll", SetLastError = true)]
internal extern static bool CreateProcessAsUser(IntPtr hToken, string lpApplicationName,
string lpCommandLine, IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes, bool bInheritHandle, uint dwCreationFlags, IntPtr lpEnvironment,
string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true)]
internal extern static bool CloseHandle(IntPtr handle);
[DllImport("kernel32.dll", SetLastError = true)]
internal extern static UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
internal extern static bool GetExitCodeProcess(IntPtr hProcess, out UInt32 lpExitCode);
}
'#
# Determine the path for the file in which process output will be captured.
$tmpFileOutput = 'C:\Users\Public\tmp.txt'
if (Test-Path -LiteralPath $tmpFileOutput) { Remove-Item -Force $tmpFileOutput }
$cred = Get-Credential -Message "Please specify the credentials for the user to run as:"
Write-Verbose -Verbose "Running process as user `"$($cred.UserName)`"..."
# !! If this fails with "The handle is invalid", there are two possible reasons:
# !! - The credentials are invalid.
# !! - The target user isn't in the Administrators groups.
$exitCode = [ProcessHelper]::new().RunProcess(
[System.Diagnostics.ProcessStartInfo] #{
# !! CAVEAT: *Full* path required.
FileName = 'C:\WINDOWS\system32\cmd.exe'
# !! CAVEAT: While whoami.exe correctly reflects the target user, the per-use *environment variables*
# !! %USERNAME% and %USERPROFILE% still reflect the *caller's* values.
Arguments = '/c "(echo Hi from & whoami & echo at %TIME%) > {0} 2>&1"' -f $tmpFileOutput
UserName = $cred.UserName
Password = $cred.Password
}
)
Write-Verbose -Verbose "Process exited with exit code $exitCode."
Write-Verbose -Verbose "Output from test file created by the process:"
Get-Content $tmpFileOutput
Remove-Item -Force -ErrorAction Ignore $tmpFileOutput # Clean up.
[1] The output from a process' redirected standard streams is buffered, and the process is blocked from writing more data when a buffer fills up, in which case it has to wait for the reader of the stream to consume the buffer. Thus, if you try to synchronously read to the end of one stream, you may get stuck if the other stream's buffer fills up in the meantime, and therefore blocks the process from finishing writing to the first stream.

PowerShell script to open four different Windows Explorer paths and position each window

I am trying to write a script that opens up Windows Explorer four times (each window with a different path) and positions each window in a corner of the screen so that they cover equal areas. The script will be used on machines with different screen resolutions so ideally it should be based on window size, but this isn't an absolute must, just as long as they're not overlapping (I think the min screen resolution it will be used on is 1366x768).
I am new to PowerShell scripting so I've managed to get some things working, I'm just struggling to fit it all together. I was able to get four separate Windows Explorer windows to open where I wanted:
ii $path1
ii $path2
ii $path3
ii $path4
I was also able to open Internet Explorer to a set size using -ComObject:
$IE = New-Object -ComObject Internetexplorer.application
$IE.Left = 0
$IE.Width = 500
$IE.Top = 0
$IE.Height = 500
$IE.Navigate($URL)
$IE.Visible = $True
I can't get it to work with Windows Explorer though. I've also managed to get the resolution for the monitor:
$monitor = Get-Wmiobject Win32_Videocontroller
$monitor.CurrentHorizontalResolution
$monitor.CurrentVerticalResolution
Unfortunately this provides 2 resolutions because I have two monitors, and I'm not sure how to extract the resolution for just one of them. Additionally, I don't know what that would mean if the script was run on a machine with only one monitor.
So with what I currently have, I would specifically like to know:
How to open my Windows Explorer windows to a certain size and position as I have done with IE (doesn't have to be with ii either, anything that works)
How to extract a single value for monitor resolution that I can do some maths with to get the optimum sizing.
Thanks.
EDIT: I have found a quick-fix but it's not quite there. By examining the commands available to me with the shell, and came up with this:
$Shell = New-Object -ComObject Shell.Application
ii $path1
ii $path2
ii $path3
ii $path4
$Shell.TileHorizontally()
This should work, because it's likely that the user will have only these four windows open and nothing else. Unfortunately, TileHorizontally()works on all open windows, but it looks like the ii commands don't execute fast enough or something, because they don't tile when the script is run. They do if you run it a second time, once the script is already open. I thought that the -Wait option would help me here, but it doesn't seem to work with ii. Anyone see a solution to this?
EDIT 2: My current code, using Set-Window with doc comments removed:
Function Set-Window {
[OutputType('System.Automation.WindowInfo')]
[cmdletbinding()]
Param (
[parameter(ValueFromPipelineByPropertyName=$True)]
$ProcessId,
[int]$X,
[int]$Y,
[int]$Width,
[int]$Height,
[switch]$Passthru
)
Begin {
Try{
[void][Window]
} Catch {
Add-Type #"
using System;
using System.Runtime.InteropServices;
public class Window {
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("User32.dll")]
public extern static bool MoveWindow(IntPtr handle, int x, int y, int width, int height, bool redraw);
}
public struct RECT
{
public int Left; // x position of upper-left corner
public int Top; // y position of upper-left corner
public int Right; // x position of lower-right corner
public int Bottom; // y position of lower-right corner
}
"#
}
}
Process {
$Rectangle = New-Object RECT
$Handle = (Get-Process -Id $ProcessId).MainWindowHandle
$Return = [Window]::GetWindowRect($Handle,[ref]$Rectangle)
If (-NOT $PSBoundParameters.ContainsKey('Width')) {
$Width = $Rectangle.Right - $Rectangle.Left
}
If (-NOT $PSBoundParameters.ContainsKey('Height')) {
$Height = $Rectangle.Bottom - $Rectangle.Top
}
If ($Return) {
$Return = [Window]::MoveWindow($Handle, $x, $y, $Width, $Height,$True)
}
If ($PSBoundParameters.ContainsKey('Passthru')) {
$Rectangle = New-Object RECT
$Return = [Window]::GetWindowRect($Handle,[ref]$Rectangle)
If ($Return) {
$Height = $Rectangle.Bottom - $Rectangle.Top
$Width = $Rectangle.Right - $Rectangle.Left
$Size = New-Object System.Management.Automation.Host.Size -ArgumentList $Width, $Height
$TopLeft = New-Object System.Management.Automation.Host.Coordinates -ArgumentList $Rectangle.Left, $Rectangle.Top
$BottomRight = New-Object System.Management.Automation.Host.Coordinates -ArgumentList $Rectangle.Right, $Rectangle.Bottom
If ($Rectangle.Top -lt 0 -AND $Rectangle.Left -lt 0) {
Write-Warning "Window is minimized! Coordinates will not be accurate."
}
$Object = [pscustomobject]#{
Id = $ProcessId
Size = $Size
TopLeft = $TopLeft
BottomRight = $BottomRight
}
$Object.PSTypeNames.insert(0,'System.Automation.WindowInfo')
$Object
}
}
}
}
Get-Process -Id (Start-Process -FilePath C:\windows\explorer.exe -ArgumentList "." -Wait -Passthru).Id | Set-Window -X 500 -Y 500 -Height 500 -Width 500 -Passthru

Open file explorer at coordinates

When I boot up my computer I open several file explorers and sort them around the screen to help speed up my workflow. It's not time consuming, only tedious, and I'd like a small program to do it for me. I know how to open an explorer, but I don't see any positional arguments.
Is there a way to spawn a file explorer at a set of screen coordinates, or move it programatically after it opens? Preferably with python 3+, but batch will work as well.
That was simultaneously easier and harder than I thought it was going to be. Everything is commented, let me know if you have any more questions. This is a PowerShell/batch hybrid script (so save it as a .bat file) because PowerShell is disabled on systems by default or something.
<# :
:: Based on https://gist.github.com/coldnebo/1148334
:: Converted to a batch/powershell hybrid via http://www.dostips.com/forum/viewtopic.php?p=37780#p37780
:: Array comparison from http://stackoverflow.com/a/6368667/4158862
#echo off
setlocal
set "POWERSHELL_BAT_ARGS=%*"
if defined POWERSHELL_BAT_ARGS set "POWERSHELL_BAT_ARGS=%POWERSHELL_BAT_ARGS:"=\"%"
endlocal & powershell -NoLogo -NoProfile -Command "$_ = $input; Invoke-Expression $( '$input = $_; $_ = \"\"; $args = #( &{ $args } %POWERSHELL_BAT_ARGS% );' + [String]::Join( [char]10, $( Get-Content \"%~f0\" ) ) )"
goto :EOF
#>
# Create an instance of the Win32 API object to handle and manipulate windows
Add-Type #"
using System;
using System.Runtime.InteropServices;
public class Win32 {
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
}
"#
# Get a list of existing Explorer Windows
$previous_array = #()
$shell_object = New-Object -COM 'Shell.Application'
foreach($old_window in $shell_object.Windows())
{
$previous_array += $old_window.HWND
}
# Open four more Explorer Windows in the current directory
explorer
explorer
explorer
explorer
# Pause for 1 second so that the windows have time to finish opening
sleep 1
# Get the list of new Explorer Windows
$new_array = #()
foreach($new_window in $shell_object.Windows())
{
$new_array += $new_window.HWND
}
# Compare the two arrays and only process the new windows
$only_new = Compare-Object -ReferenceObject $previous_array -DifferenceObject $new_array -PassThru
# MoveWindow takes HWND value, X-position on screen, Y-position on screen, window width, and window height
# I've just hard-coded the values, adjust them to suit your needs
[Win32]::MoveWindow($only_new[0],0,0,960,540,$true)
[Win32]::MoveWindow($only_new[1],960,0,960,540,$true)
[Win32]::MoveWindow($only_new[2],0,540,960,540,$true)
[Win32]::MoveWindow($only_new[3],960,540,960,540,$true)

Windows 7 - Taskbar - Pin or Unpin Program Links

As in title, is there any Win32 API to do that?
Don't do this.
I'm 99% sure there isn't an official API for it, for exactly the same reason that there wasn't programmatic access to the old Start Menu's pin list.
In short, most users don't want programs putting junk in their favorites, quick launch, taskbar, etc. so Windows doesn't support you doing as such.
I'm trying to implement a VirtuaWin (opensource virtual desktop software) plugin that allows me to pin different buttons to different virtual desktops. Completely valid reason to use this.
Found the way to pin/unpin it already:
Following code snippet is taken from Chromium shortcut.cc file, nearly unchanged, see also the ShellExecute function at the MSDN
bool TaskbarPinShortcutLink(const wchar_t* shortcut) {
int result = reinterpret_cast<int>(ShellExecute(NULL, L"taskbarpin", shortcut,
NULL, NULL, 0));
return result > 32;
}
bool TaskbarUnpinShortcutLink(const wchar_t* shortcut) {
int result = reinterpret_cast<int>(ShellExecute(NULL, L"taskbarunpin",
shortcut, NULL, NULL, 0));
return result > 32;
}
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
Seems pretty straightforward if you know the shortcut. For me though this is not sufficient, I also need to iterate over existing buttons and unpin and repin them on different desktops.
In the comments of a Code Project article it says all you have to do is create a symbolic link in the folder "C:\Users\Username\AppData\Roaming\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar".
But it appears to generally be unsociable practice, as the other comments here have noted.
You can pin/unpin apps via Windows Shell verbs:
http://blogs.technet.com/deploymentguys/archive/2009/04/08/pin-items-to-the-start-menu-or-windows-7-taskbar-via-script.aspx
For API, there is a script-friendly COM library for working with the Shell:
http://msdn.microsoft.com/en-us/library/bb776890%28VS.85%29.aspx
Here is an example written in JScript:
// Warning: untested and probably needs correction
var appFolder = "FOLDER CONTAINING THE APP/SHORTCUT";
var appToPin = "FILENAME OF APP/SHORTCUT";
var shell = new ActiveXObject("Shell.Application");
var folder = shell.NameSpace(appFolder);
var folderItem = folder.ParseName(appToPin);
var itemVerbs = folderItem.Verbs;
for(var i = 0; i < itemVerbs.Count; i++)
{
// You have to find the verb by name,
// so if you want to support multiple cultures,
// you have to match against the verb text for each culture.
if(itemVerbs[i].name.Replace(/&/, "") == "Pin to Start Menu")
{
itemVerbs[i].DoIt();
}
}
Just to put some links on the info as microsoft now offer an official documentation on "Taskbar Extensions" :
A small set of applications are pinned
by default for new installations.
Other than these, only the user can
pin further applications; programmatic
pinning by an application is not
permitted.
So Kevin Montrose answer is the correct one : DON'T.
It works, but not for all OS, e.g. Windows 10:
[DllImport("kernel32.dll")]
private static extern IntPtr LoadLibrary(string dllName);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern int LoadString(IntPtr hInstance, uint uID, StringBuilder lpBuffer, int nBufferMax);
private static void PinUnpinTaskBar(string filePath, bool pin)
{
if (!File.Exists(filePath))
throw new FileNotFoundException(filePath + " not exists!");
int MAX_PATH = 255;
var actionIndex = pin ? 5386 : 5387; // 5386 is the DLL index for"Pin to Tas&kbar", ref. http://www.win7dll.info/shell32_dll.html
StringBuilder szPinToStartLocalized = new StringBuilder(MAX_PATH);
IntPtr hShell32 = LoadLibrary("Shell32.dll");
LoadString(hShell32, (uint)actionIndex, szPinToStartLocalized, MAX_PATH);
string localizedVerb = szPinToStartLocalized.ToString();
// create the shell application object
dynamic shellApplication = Activator.CreateInstance(Type.GetTypeFromProgID("Shell.Application"));
string path = Path.GetDirectoryName(filePath);
string fileName = Path.GetFileName(filePath);
dynamic directory = shellApplication.NameSpace(path);
dynamic link = directory.ParseName(fileName);
dynamic verbs = link.Verbs();
for (int i = 0; i < verbs.Count(); i++)
{
dynamic verb = verbs.Item(i);
if ((pin && verb.Name.Equals(localizedVerb)) || (!pin && verb.Name.Contains(localizedVerb)))
{
verb.DoIt();
break;
}
}
}
I found there is no offical API to do that, but someone has do it through VBScript.
http://blog.ananthonline.net/?p=37
Thanks.
this folder contains shortcut of pinned application
C:\Users\Your-User-Name\AppData\Roaming\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar

Resources