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() { if (PortableMode.IsPortable) return; 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); } } }