Pinning to the taskbar a "chained process"
Asked Answered
R

2

4

Let's say I have two programs called launcher.exe and launchee.exe. The launcher display a button which -when clicked- starts launchee.exe and launchee.exe is a simple hello world program.

If I do nothing to prevent it, when the user will "pin to the taskbar" the hello world program, it will pin launchee.exe and will not go through the launcher to start the application.

What is the best way to tell Windows to pin launcher.exe and not launchee.exe ?

To make things concrete here's an example of implementation of launcher.exe in C#:

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Diagnostics;

public class Launcher : Form
{
    static public void Main ()
    {
        Application.Run (new Launcher ());
    }

    public Launcher ()
    {
        Button b = new Button ();
        b.Text = "Launch";
        b.Click += new EventHandler (Button_Click);
        Controls.Add (b);
    }

    private void Button_Click (object sender, EventArgs e)
    {
        Process.Start("launchee.exe");
        System.Environment.Exit(0);
    }
}

and launchee.exe:

using System;
using System.Drawing;
using System.Windows.Forms;

public class Launchee : Form
{
    static public void Main ()
    {
        Application.Run (new Launchee ());
    }

    public Launchee ()
    {
        Label b = new Label();
        b.Text = "Hello World !";
        Controls.Add (b);
    }
}
Rambo answered 31/1, 2018 at 10:23 Comment(2)
It seems that either my question is too poorly written to interest anyone or my answer is the only way out there to solve the problem. I hope that at least the answer I wrote will be useful to someone else.Rambo
related: #3829745Croaky
R
4

I propose an anwser based on binding the low level API to access the AppUserModelID. I find this solution fragile and messy. It is largely inspired by the Windows Api CodePack that seems to have been discontinued by Microsoft. I hope someone will propose a cleaner solution.

Its purpose is to set the AppUserId to be "Stackoverflow.chain.process.pinning" and manually set the RelaunchCommand as well as the DisplayName properties (they have to be set together according to AppUserModelID).

To use it in the example implementation, one needs to call TaskBar.SetupLauncher(this) and TaskBar.SetupLaunchee(this) respectively in the Launcher and Launchee constructors.

using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

internal struct PropertyKey
{
  Guid formatId;
  int propertyId;

  internal PropertyKey(Guid guid, int propertyId)
  {
      this.formatId = guid;
      this.propertyId = propertyId;
  }
}

[StructLayout(LayoutKind.Explicit)]
internal struct PropVariant
{
  [FieldOffset(0)] internal ushort vt;
  [FieldOffset(8)] internal IntPtr pv;
  [FieldOffset(8)] internal UInt64 padding;

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

  internal PropVariant(string value)
  {
      this.vt = (ushort)VarEnum.VT_LPWSTR;
      this.padding = 0;
      this.pv = Marshal.StringToCoTaskMemUni(value);
  }

  internal void Clear()
  {
    PropVariantClear (ref this);
  }
}

[ComImport,
Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IPropertyStore
{
  int GetCount([Out] out uint propertyCount);
  void GetAt([In] uint propertyIndex, [Out, MarshalAs(UnmanagedType.Struct)] out PropertyKey key);
  void GetValue([In, MarshalAs(UnmanagedType.Struct)] ref PropertyKey key, [Out, MarshalAs(UnmanagedType.Struct)] out PropVariant pv);
  void SetValue([In, MarshalAs(UnmanagedType.Struct)] ref PropertyKey key, [In, MarshalAs(UnmanagedType.Struct)] ref PropVariant pv);
  void Commit();
}

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

  internal static class Key {
    private static Guid propGuid = new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3");
    internal static PropertyKey AppId = new PropertyKey(propGuid, 5);
    internal static PropertyKey RelaunchCommand = new PropertyKey(propGuid, 2);
    internal static PropertyKey DisplayName = new PropertyKey(propGuid, 4);
  }

  private static void ClearValue(IPropertyStore store, PropertyKey key) {
      var prop = new PropVariant();
      prop.vt = (ushort)VarEnum.VT_EMPTY;
      store.SetValue(ref key, ref prop);
  }

  private static void SetValue(IPropertyStore store, PropertyKey key, string value) {
    var prop = new PropVariant(value);
    store.SetValue(ref key, ref prop);
    prop.Clear();
  }

  internal static IPropertyStore Store(IntPtr handle) {
    IPropertyStore store;
    var store_guid = new Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99");
    int rc = SHGetPropertyStoreForWindow(handle, ref store_guid, out store);
    if (rc != 0) throw Marshal.GetExceptionForHR(rc);
    return store;
  }

  internal static void SetupLauncher(Form form) {
    IntPtr handle = form.Handle;
    var store = Store(handle);
    SetValue (store, Key.AppId, "Stackoverflow.chain.process.pinning");
    form.FormClosed += delegate { Cleanup(handle); };
  } 

  internal static void SetupLaunchee(Form form) {
    IntPtr handle = form.Handle;
    var store = Store(handle);
    SetValue (store, Key.AppId, "Stackoverflow.chain.process.pinning");
    string exePath = System.IO.Path.Combine(System.Windows.Forms.Application.StartupPath, "launcher.exe");
    SetValue (store, Key.RelaunchCommand, exePath);
    SetValue (store, Key.DisplayName, "Test");
    form.FormClosed += delegate { Cleanup(handle); };
  }

  internal static void Cleanup(IntPtr handle) {
    var store = Store(handle);
    ClearValue (store, Key.AppId);
    ClearValue (store, Key.RelaunchCommand);
    ClearValue (store, Key.DisplayName);
  }
}
Rambo answered 31/1, 2018 at 10:23 Comment(4)
Have you posted this on a gist, github, or other code host? I'm going to incorporate this into an self-updating sample app. Is this the best link to credit you for porting from C++ (or extracting from Win CodePack)?Croaky
Here's a gist if you want :) gist.github.com/mlasson/eca5ec98553ad1ac5d71ce7b05f9bc20Rambo
I still think there should be a better way to do that.Rambo
My app crashes after compiling in x64. You have a mistake in the PropVariant struct. I found a clue in Windows-API-Code-Pack-1.1/../PropVariant#L209. Solution: move the padding by 4 bytes [FieldOffset(12)] internal UInt64 padding; or set size [StructLayout(LayoutKind.Explicit, Size = 20)]. More in your gist.Schreibe
F
3

I had the same problem and finally managed to find a nice solution for this. Setting the RelaunchCommand was not working for me any more with the newest Windows 10 Update.

For simplicity i used "App" as name instead of "Launchee" as it could be confused with Launcher easily.

Short Version:

Launcher.exe and App.exe are grouped together in the taskbar. Laucher.exe does the update part and starts the App.exe as usual. If you choose 'Pin to taskbar' when the Launcher is running, it will pin the Launcher to the taskbar. If the App is already started and you pin this one to the taskbar, it will still pin the Launcher to the taskbar as they are grouped together.

Long Version:

That both Applications are grouped together in the taskbar, they share the same AppID. This could be done like described here: How to group different apps in Windows task bar?

The starter should have an UI that an icon is shown in the taskbar. In my case it is a SplashScreen as UI. It starts the App.exe and the Laucher waits two seconds until the App.exe is started that they share for a small amount of time the same symbol in the taskbar. Then the Launcher could close and if you pin the App afterwards it will pin the Launcher to the taskbar.

Here you could find an example Application which is started, it's a WPF App:

using System.Runtime.InteropServices;
using System.Windows;

namespace TestApp
{
    public partial class MainWindow : Window
    {
        [DllImport("shell32.dll", SetLastError = true)]
        private static extern void SetCurrentProcessExplicitAppUserModelID([MarshalAs(UnmanagedType.LPWStr)] string AppID);

        private const string AppID = "73660a02-a7ec-4f9a-ba25-c55ddbf60225"; // generate your own with: Guid.NewGuid();

        public MainWindow()
        {
            SetCurrentProcessExplicitAppUserModelID(AppID);
            InitializeComponent();
            Topmost = true; // to make sure UI is in front once
            Topmost = false;
        }
    }
}

Second WPF App which is the Launcher/Starter:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows;

namespace TestStarter
{
    public partial class MainWindow : Window
    {
        [DllImport("shell32.dll", SetLastError = true)]
        private static extern void SetCurrentProcessExplicitAppUserModelID([MarshalAs(UnmanagedType.LPWStr)] string AppID);

        private const string AppID = "73660a02-a7ec-4f9a-ba25-c55ddbf60225"; // generate your own with: Guid.NewGuid();

        public MainWindow()
        {
            SetCurrentProcessExplicitAppUserModelID(AppID);
            InitializeComponent();
            Process.Start(@"C:\Test\TestApp.exe");
            ExitAfterDelay();
        }

        private async void ExitAfterDelay()
        {
            await Task.Delay(2000);
            Environment.Exit(0);
        }
    }
}
Flagler answered 25/10, 2019 at 13:51 Comment(2)
Only works if you can modify the App code. For example, if you write a launcher for a game, you can't change the game.Kellykellyann
Your solution looks good and simple, but it has one drawback: if TestApp is launched first, after pinning it, the Launcher will not launch.Schreibe

© 2022 - 2024 — McMap. All rights reserved.