将“链接进程”固定到任务栏

4
假设我有两个程序,名为launcher.exelaunchee.exe。启动器显示一个按钮,当点击该按钮时,会启动launchee.exe,而launchee.exe是一个简单的“hello world”程序。
如果我不采取任何措施来防止它,当用户将“固定到任务栏”时,它将固定launchee.exe,并且不会通过启动器启动应用程序。
那么,告诉Windows将启动器固定到任务栏而不是launchee.exe的最佳方法是什么?
为了使事情更具体化,这里有一个C#实现launcher.exe的示例:
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);
    }
}

似乎我的问题写得太糟糕了,没有引起任何人的兴趣,或者我的答案是解决问题的唯一方法。我希望我写的答案至少对其他人有用。 - Marc
启动一个子进程和 Windows 7 任务栏。 - yzorg
2个回答

4
我提出了一种基于绑定低级API来访问AppUserModelID的答案。我发现这个解决方案很脆弱和混乱。它在很大程度上受到Windows Api CodePack的启发,但似乎已被微软停止支持。我希望有人能提出更清晰的解决方案。
它的目的是将AppUserId设置为"Stackoverflow.chain.process.pinning",并手动设置RelaunchCommand以及DisplayName属性(根据AppUserModelID,它们必须一起设置)。
要在示例实现中使用它,需要在LauncherLaunchee构造函数中分别调用TaskBar.SetupLauncher(this)TaskBar.SetupLaunchee(this)
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);
  }
}

你把这个发布到gist、github或其他代码托管平台了吗?我将把它整合到一个自更新的示例应用程序中。这是最好的链接来表彰你从C++移植(或从Win CodePack中提取)吗? - yzorg
2
如果您想要,这里有一个 gist :) https://gist.github.com/mlasson/eca5ec98553ad1ac5d71ce7b05f9bc20 - Marc
我仍然认为应该有更好的方法来做到那一点。 - Marc

3

我曾经遇到同样的问题,最终找到了一个好的解决方案。在最新的Windows 10更新中,设置RelaunchCommand不再起作用。

为了简单起见,我使用了“App”作为名称,而不是“Launchee”,因为后者容易与Launcher混淆。

简短版本:

Launcher.exe 和 App.exe 在任务栏中分组在一起。Laucher.exe 完成更新部分并像往常一样启动 App.exe。如果您在运行Launcher时选择“将其固定到任务栏”,它将把Launcher固定到任务栏上。如果App已经启动并将其固定到任务栏,它仍会将Launcher与之分组并固定到任务栏上。

详细版本:

这两个应用程序在任务栏中分组在一起,共享相同的AppID。可以按照此处描述的方式实现: 如何在Windows任务栏中分组不同的应用程序?

启动器应该有一个UI,其中在任务栏中显示一个图标。在我的情况下,它是一个SplashScreen UI。它启动App.exe,等待两秒钟直到App.exe启动,然后它们在任务栏中共享同一个符号一段时间。然后启动器可以关闭,如果您之后将App固定到任务栏上,它将会把Launcher固定到任务栏上。

在这里,您可以找到一个启动的示例应用程序,它是一个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应用程序是启动器/启动器:

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

只有在您可以修改应用程序代码的情况下才能起作用。例如,如果您为游戏编写启动器,则无法更改游戏。 - Ray
你的解决方案看起来不错而且简单,但是它有一个缺点:如果先启动TestApp,然后将其固定到启动器上,启动器将无法启动。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接