fix: 修复任务栏固定和右键菜单图标不显示问题

This commit is contained in:
2026-01-17 16:26:16 +08:00
parent 50955e84c7
commit 74ca8e4d57
4 changed files with 416 additions and 47 deletions

305
TaskbarIntegration.cs Normal file
View File

@@ -0,0 +1,305 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace TimerApp
{
internal static class TaskbarIntegration
{
private const string AppId = "TimerApp";
private const string DisplayName = "TimerApp";
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int SetCurrentProcessExplicitAppUserModelID(string AppID);
[DllImport("shell32.dll")]
private static extern void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);
private const uint SHCNE_ASSOCCHANGED = 0x08000000;
private const uint SHCNF_IDLIST = 0x0000;
public static void InitializeProcess()
{
TrySetAppUserModelId();
}
public static void InitializeShortcuts()
{
string? iconPath = TryEnsureIconFile();
if (iconPath is null)
return;
TryEnsureStartMenuShortcut(iconPath);
TryUpdatePinnedTaskbarShortcuts(iconPath);
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, IntPtr.Zero, IntPtr.Zero);
}
private static void TrySetAppUserModelId()
{
try
{
SetCurrentProcessExplicitAppUserModelID(AppId);
}
catch
{
}
}
private static string? TryEnsureIconFile()
{
try
{
string dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "TimerApp");
Directory.CreateDirectory(dir);
string iconPath = Path.Combine(dir, "TimerApp.ico");
IconGenerator.WriteClockIco(iconPath, 256);
return iconPath;
}
catch
{
return null;
}
}
private static void TryEnsureStartMenuShortcut(string iconPath)
{
try
{
string exePath = Environment.ProcessPath ?? string.Empty;
if (string.IsNullOrWhiteSpace(exePath) || !File.Exists(exePath))
return;
string programsDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.StartMenu),
"Programs"
);
Directory.CreateDirectory(programsDir);
string lnkPath = Path.Combine(programsDir, "TimerApp.lnk");
CreateOrUpdateShortcut(lnkPath, exePath, iconPath, AppId);
}
catch
{
}
}
private static void TryUpdatePinnedTaskbarShortcuts(string iconPath)
{
try
{
string exePath = Environment.ProcessPath ?? string.Empty;
if (string.IsNullOrWhiteSpace(exePath) || !File.Exists(exePath))
return;
string pinnedDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Microsoft",
"Internet Explorer",
"Quick Launch",
"User Pinned",
"TaskBar"
);
if (!Directory.Exists(pinnedDir))
return;
foreach (string lnkPath in Directory.EnumerateFiles(pinnedDir, "*.lnk", SearchOption.TopDirectoryOnly))
{
if (!TryGetShortcutTarget(lnkPath, out string targetPath))
continue;
if (!string.Equals(Path.GetFullPath(targetPath), Path.GetFullPath(exePath), StringComparison.OrdinalIgnoreCase))
continue;
CreateOrUpdateShortcut(lnkPath, exePath, iconPath, AppId);
}
}
catch
{
}
}
private static bool TryGetShortcutTarget(string lnkPath, out string targetPath)
{
targetPath = string.Empty;
IShellLinkW? link = null;
try
{
link = (IShellLinkW)new ShellLink();
((IPersistFile)link).Load(lnkPath, 0);
var sb = new StringBuilder(260);
link.GetPath(sb, sb.Capacity, out _, 0);
string path = sb.ToString();
if (string.IsNullOrWhiteSpace(path))
return false;
targetPath = path;
return true;
}
catch
{
return false;
}
finally
{
if (link is not null)
Marshal.FinalReleaseComObject(link);
}
}
private static void CreateOrUpdateShortcut(string lnkPath, string exePath, string iconPath, string appId)
{
IShellLinkW? link = null;
IPropertyStore? store = null;
try
{
link = (IShellLinkW)new ShellLink();
link.SetPath(exePath);
link.SetIconLocation(iconPath, 0);
link.SetArguments(string.Empty);
link.SetWorkingDirectory(Path.GetDirectoryName(exePath) ?? string.Empty);
store = (IPropertyStore)link;
using var pvAppId = PropVariant.FromString(appId);
using var pvRelaunchCommand = PropVariant.FromString(exePath);
using var pvRelaunchName = PropVariant.FromString(DisplayName);
using var pvRelaunchIcon = PropVariant.FromString(iconPath);
store.SetValue(PropertyKey.PKEY_AppUserModel_ID, pvAppId);
store.SetValue(PropertyKey.PKEY_AppUserModel_RelaunchCommand, pvRelaunchCommand);
store.SetValue(PropertyKey.PKEY_AppUserModel_RelaunchDisplayNameResource, pvRelaunchName);
store.SetValue(PropertyKey.PKEY_AppUserModel_RelaunchIconResource, pvRelaunchIcon);
store.Commit();
((IPersistFile)link).Save(lnkPath, true);
}
finally
{
if (store is not null)
Marshal.FinalReleaseComObject(store);
if (link is not null)
Marshal.FinalReleaseComObject(link);
}
}
[ComImport]
[Guid("00021401-0000-0000-C000-000000000046")]
private class ShellLink
{
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214F9-0000-0000-C000-000000000046")]
private interface IShellLinkW
{
void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cch, out WIN32_FIND_DATAW pfd, uint fFlags);
void GetIDList(out IntPtr ppidl);
void SetIDList(IntPtr pidl);
void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cch);
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cch);
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cch);
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
void GetHotkey(out short pwHotkey);
void SetHotkey(short wHotkey);
void GetShowCmd(out int piShowCmd);
void SetShowCmd(int iShowCmd);
void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cch, out int piIcon);
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, uint dwReserved);
void Resolve(IntPtr hwnd, uint fFlags);
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("0000010B-0000-0000-C000-000000000046")]
private interface IPersistFile
{
void GetClassID(out Guid pClassID);
void IsDirty();
void Load([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, uint dwMode);
void Save([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, [MarshalAs(UnmanagedType.Bool)] bool fRemember);
void SaveCompleted([MarshalAs(UnmanagedType.LPWStr)] string pszFileName);
void GetCurFile([MarshalAs(UnmanagedType.LPWStr)] out string ppszFileName);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
private interface IPropertyStore
{
uint GetCount(out uint cProps);
uint GetAt(uint iProp, out PropertyKey pkey);
uint GetValue(ref PropertyKey key, out PropVariant pv);
uint SetValue(ref PropertyKey key, [In] PropVariant pv);
uint Commit();
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct WIN32_FIND_DATAW
{
public uint dwFileAttributes;
public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
public uint nFileSizeHigh;
public uint nFileSizeLow;
public uint dwReserved0;
public uint dwReserved1;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string cFileName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
public string cAlternateFileName;
}
[StructLayout(LayoutKind.Sequential)]
private struct PropertyKey
{
public Guid fmtid;
public uint pid;
public PropertyKey(Guid fmtid, uint pid)
{
this.fmtid = fmtid;
this.pid = pid;
}
public static PropertyKey PKEY_AppUserModel_ID { get; } = new PropertyKey(new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"), 5);
public static PropertyKey PKEY_AppUserModel_RelaunchCommand { get; } = new PropertyKey(new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"), 2);
public static PropertyKey PKEY_AppUserModel_RelaunchIconResource { get; } = new PropertyKey(new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"), 3);
public static PropertyKey PKEY_AppUserModel_RelaunchDisplayNameResource { get; } = new PropertyKey(new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"), 4);
}
[StructLayout(LayoutKind.Explicit)]
private sealed class PropVariant : IDisposable
{
[FieldOffset(0)]
private readonly ushort vt;
[FieldOffset(8)]
private readonly IntPtr pointerValue;
private PropVariant(string value)
{
vt = 31;
pointerValue = Marshal.StringToCoTaskMemUni(value);
}
public static PropVariant FromString(string value) => new PropVariant(value);
public void Dispose()
{
PropVariantClear(this);
}
[DllImport("ole32.dll")]
private static extern int PropVariantClear([In, Out] PropVariant pvar);
}
}
}