Закрепление на панели задач "цепочки процессов"

Допустим, у меня есть две программы под названием launcher.exe а также launchee.exe, Панель запуска отображает кнопку, которая при нажатии запускается launchee.exe а также launchee.exe простая программа Hello World.

Если я ничего не сделаю, чтобы предотвратить это, когда пользователь "закрепит на панели задач" программу hello world, он будет закреплен launchee.exe и не пойдет через лаунчер для запуска приложения.

Каков наилучший способ сказать Windows, чтобы закрепить launcher.exe, а не launchchee.exe?

Чтобы конкретизировать, вот пример реализации launcher.exe в 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);
    }
}

а также 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);
    }
}

1 ответ

Я предлагаю anwser, основанный на привязке низкоуровневого API для доступа к AppUserModelID. Я нахожу это решение хрупким и грязным. Он в значительной степени вдохновлен Windows Api CodePack, который, похоже, был прекращен Microsoft. Я надеюсь, что кто-то предложит более чистое решение.

Его цель - установить AppUserId равным "Stackru.chain.process.pinning" и вручную установить RelaunchCommand, а также свойства DisplayName (они должны быть установлены вместе в соответствии с AppUserModelID).

Чтобы использовать его в примере реализации, нужно вызвать TaskBar.SetupLauncher(this) а также TaskBar.SetupLaunchee(this) соответственно в Launcher а также Launchee Конструкторы.

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, "Stackru.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, "Stackru.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);
  }
}

У меня была та же проблема, и мне наконец удалось найти для нее хорошее решение. Установка RelaunchCommand больше не работала для меня с последним обновлением Windows 10.

Для простоты я использовал имя "Приложение" вместо "Лаунчи", так как его легко можно было спутать с Лаунчером.

Укороченная версия:

Launcher.exe и App.exe сгруппированы на панели задач. Laucher.exe выполняет часть обновления и запускает App.exe как обычно. Если вы выберете "Закрепить на панели задач", когда запущена программа запуска, она закрепит ее на панели задач. Если приложение уже запущено, и вы закрепите его на панели задач, оно все равно будет прикреплять панель запуска к панели задач, поскольку они сгруппированы вместе.

Длинная версия:

Оба приложения сгруппированы на панели задач и имеют один и тот же AppID. Это можно сделать, как описано здесь: Как сгруппировать различные приложения на панели задач Windows?

У стартера должен быть пользовательский интерфейс, значок которого отображается на панели задач. В моем случае это SplashScreen как пользовательский интерфейс. Он запускает App.exe, и Laucher ждет две секунды, пока не запустится App.exe, который в течение небольшого времени использует один и тот же символ на панели задач. Затем программа запуска может закрываться, и если вы закрепите приложение после этого, она закрепит панель запуска на панели задач.

Здесь вы можете найти пример запущенного приложения, это приложение WPF:

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;
        }
    }
}

Второе приложение WPF, которое является 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);
        }
    }
}
Другие вопросы по тегам