Закрепление на панели задач "цепочки процессов"
Допустим, у меня есть две программы под названием 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);
}
}
}