Rob Traynere
Rob Traynere

Reputation: 704

Separating taskbar icon for new form in PowerShell

When you create a windows form in PowerShell, it will group with the host console window, even if you change the icon for the form.

How do I separate the new form's taskbar icon out from the PowerShell console icon?

What happens on left, desired effect on right: enter image description here

Example code:

[void][Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic')
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

$form = New-Object System.Windows.Forms.Form
$form.Size = New-Object System.Drawing.Size(300,200)
$form.ShowInTaskbar = $true
$form.Icon = New-Object system.drawing.icon 'c:\icon.ico'
$form.Text = 'New taskbar icon plz'

$form.BringToFront()
$form.ShowDialog()

The only thing I saw that was somewhat helpful was a reference that changing the "Application ID" would separate this out, but the references are all C code.

https://msdn.microsoft.com/en-us/magazine/dd942846.aspx

Please give PowerShell answers, or if the answers include C# code or calls to other APIs, please explain how they work in PowerShell.

Upvotes: 5

Views: 1909

Answers (1)

TessellatingHeckler
TessellatingHeckler

Reputation: 29033

After cobbling together a lot of P/Invoke through C# magic from the internet, mostly from here and some from here, I ended up with this to add a type with one method you can use like [PSAppID]::SetAppIdForWindow($form.handle, "Your.AppId"):

$signature = @'
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;

public class PSAppID
{
    // https://emoacht.wordpress.com/2012/11/14/csharp-appusermodelid/
    // IPropertyStore Interface
    [ComImport,
        InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
        Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
    private interface IPropertyStore
    {
        uint GetCount([Out] out uint cProps);
        uint GetAt([In] uint iProp, out PropertyKey pkey);
        uint GetValue([In] ref PropertyKey key, [Out] PropVariant pv);
        uint SetValue([In] ref PropertyKey key, [In] PropVariant pv);
        uint Commit();
    }


    // PropertyKey Structure
    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    public struct PropertyKey
    {
        private Guid formatId;    // Unique GUID for property
        private Int32 propertyId; // Property identifier (PID)

        public Guid FormatId
        {
            get
            {
                return formatId;
            }
        }

        public Int32 PropertyId
        {
            get
            {
                return propertyId;
            }
        }

        public PropertyKey(Guid formatId, Int32 propertyId)
        {
            this.formatId = formatId;
            this.propertyId = propertyId;
        }

        public PropertyKey(string formatId, Int32 propertyId)
        {
            this.formatId = new Guid(formatId);
            this.propertyId = propertyId;
        }

    }


    // PropVariant Class (only for string value)
    [StructLayout(LayoutKind.Explicit)]
    public class PropVariant : IDisposable
    {
        [FieldOffset(0)]
        ushort valueType;     // Value type

        // [FieldOffset(2)]
        // ushort wReserved1; // Reserved field
        // [FieldOffset(4)]
        // ushort wReserved2; // Reserved field
        // [FieldOffset(6)]
        // ushort wReserved3; // Reserved field

        [FieldOffset(8)]
        IntPtr ptr;           // Value


        // Value type (System.Runtime.InteropServices.VarEnum)
        public VarEnum VarType
        {
            get { return (VarEnum)valueType; }
            set { valueType = (ushort)value; }
        }

        public bool IsNullOrEmpty
        {
            get
            {
                return (valueType == (ushort)VarEnum.VT_EMPTY ||
                        valueType == (ushort)VarEnum.VT_NULL);
            }
        }

        // Value (only for string value)
        public string Value
        {
            get
            {
                return Marshal.PtrToStringUni(ptr);
            }
        }


        public PropVariant()
        { }

        public PropVariant(string value)
        {
            if (value == null)
                throw new ArgumentException("Failed to set value.");

            valueType = (ushort)VarEnum.VT_LPWSTR;
            ptr = Marshal.StringToCoTaskMemUni(value);
        }

        ~PropVariant()
        {
            Dispose();
        }

        public void Dispose()
        {
            PropVariantClear(this);
            GC.SuppressFinalize(this);
        }

    }

    [DllImport("Ole32.dll", PreserveSig = false)]
    private extern static void PropVariantClear([In, Out] PropVariant pvar);


    [DllImport("shell32.dll")]
    private static extern int SHGetPropertyStoreForWindow(
        IntPtr hwnd,
        ref Guid iid /*IID_IPropertyStore*/,
        [Out(), MarshalAs(UnmanagedType.Interface)] out IPropertyStore propertyStore);

    public static void SetAppIdForWindow(int handle, string AppId)
    {
        Guid iid = new Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99");
        IPropertyStore prop;
        int result1 = SHGetPropertyStoreForWindow((IntPtr)handle, ref iid, out prop);

        // Name = System.AppUserModel.ID
        // ShellPKey = PKEY_AppUserModel_ID
        // FormatID = 9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3
        // PropID = 5
        // Type = String (VT_LPWSTR)
        PropertyKey AppUserModelIDKey = new PropertyKey("{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}", 5);
    
        PropVariant pv = new PropVariant(AppId);
        
        uint result2 = prop.SetValue(ref AppUserModelIDKey, pv);
        
        Marshal.ReleaseComObject(prop);
    }
}
'@

Add-Type -TypeDefinition $signature

And then with your form:

[void][Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic')
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

$form = New-Object System.Windows.Forms.Form
$form.Size = New-Object System.Drawing.Size(300,200)
$form.ShowInTaskbar = $true
$form.visible = $true

[PSAppID]::SetAppIdForWindow($form.Handle, "YourName.App")

It splits off into its own taskbar entry.

  • Raymond Chen's blog says the format of the AppID should be "CompanyName.ProductName.SubProduct.VersionInformation where the Sub­Product is optional, and the Version­Information is present only if you want different versions of your app to be treated as distinct." and no longer than 128 characters.
  • I think this Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99") is supposed to be a Windows Shell ID, of IPropertyStore, but I don't know how to get it without just writing it in.
  • Same for the AppUserModelIDKey magic GUID which comes from some Win32 propkey.h maybe?

See also:

Upvotes: 6

Related Questions