using System.Runtime.InteropServices; namespace KronaStaffTerminal.Services; public sealed class TrayService : IDisposable { private const uint CallbackMessage = 0x8001; private const uint NimAdd = 0; private const uint NimModify = 1; private const uint NimDelete = 2; private const uint NifMessage = 1; private const uint NifIcon = 2; private const uint NifTip = 4; private const uint NifInfo = 16; private const uint WmLButtonDblClk = 0x0203; private const uint WmRButtonUp = 0x0205; private const uint MfString = 0; private const uint MfSeparator = 0x0800; private const uint TpmReturnCmd = 0x0100; private const uint ImageIcon = 1; private const uint LrLoadFromFile = 0x0010; private const uint LrDefaultSize = 0x0040; private const uint OpenCommand = 1001; private const uint ExitCommand = 1002; private readonly Action _showWindow; private readonly Action _requestExit; private readonly WindowProcedure _windowProcedure; private readonly IntPtr _windowHandle; private readonly IntPtr _iconHandle; private NotifyIconData _iconData; private bool _disposed; public TrayService(Action showWindow, Action requestExit) { _showWindow = showWindow; _requestExit = requestExit; _windowProcedure = HandleWindowMessage; var className = $"KronaStaffTerminal.Tray.{Environment.ProcessId}"; var windowClass = new WindowClass { Size = (uint)Marshal.SizeOf(), WindowProcedure = Marshal.GetFunctionPointerForDelegate(_windowProcedure), Instance = GetModuleHandle(null), ClassName = className }; RegisterClassEx(ref windowClass); _windowHandle = CreateWindowEx(0, className, className, 0, 0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, windowClass.Instance, IntPtr.Zero); _iconHandle = LoadImage(IntPtr.Zero, Path.Combine(AppContext.BaseDirectory, "Assets", "AppIcon.ico"), ImageIcon, 0, 0, LrLoadFromFile | LrDefaultSize); _iconData = CreateIconData(NifMessage | NifIcon | NifTip); ShellNotifyIcon(NimAdd, ref _iconData); } public void ShowRunningBalloon() { _iconData.Flags = NifInfo; _iconData.InfoTitle = "Krona Staff"; _iconData.Info = "Терминал продолжает работать в трее."; _iconData.InfoFlags = 1; ShellNotifyIcon(NimModify, ref _iconData); } private NotifyIconData CreateIconData(uint flags) => new() { Size = (uint)Marshal.SizeOf(), WindowHandle = _windowHandle, Id = 1, Flags = flags, CallbackMessage = CallbackMessage, IconHandle = _iconHandle, Tip = "Krona Staff — терминал" }; private IntPtr HandleWindowMessage(IntPtr window, uint message, IntPtr wParam, IntPtr lParam) { if (message == CallbackMessage) { var mouseMessage = unchecked((uint)lParam.ToInt64()); if (mouseMessage == WmLButtonDblClk) { Dispatch(_showWindow); return IntPtr.Zero; } if (mouseMessage == WmRButtonUp) { ShowContextMenu(); return IntPtr.Zero; } } return DefWindowProc(window, message, wParam, lParam); } private void ShowContextMenu() { var menu = CreatePopupMenu(); AppendMenu(menu, MfString, OpenCommand, "Открыть Krona Staff"); AppendMenu(menu, MfSeparator, 0, null); AppendMenu(menu, MfString, ExitCommand, "Выход администратора…"); GetCursorPos(out var point); SetForegroundWindow(_windowHandle); var command = TrackPopupMenu(menu, TpmReturnCmd, point.X, point.Y, 0, _windowHandle, IntPtr.Zero); DestroyMenu(menu); if (command == OpenCommand) Dispatch(_showWindow); if (command == ExitCommand) Dispatch(_requestExit); } private static void Dispatch(Action action) => App.DispatcherQueue.TryEnqueue(() => action()); public void Dispose() { if (_disposed) return; _disposed = true; ShellNotifyIcon(NimDelete, ref _iconData); if (_iconHandle != IntPtr.Zero) DestroyIcon(_iconHandle); if (_windowHandle != IntPtr.Zero) DestroyWindow(_windowHandle); } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct WindowClass { public uint Size; public uint Style; public IntPtr WindowProcedure; public int ClassExtra; public int WindowExtra; public IntPtr Instance; public IntPtr Icon; public IntPtr Cursor; public IntPtr Background; public string? MenuName; public string ClassName; public IntPtr SmallIcon; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct NotifyIconData { public uint Size; public IntPtr WindowHandle; public uint Id; public uint Flags; public uint CallbackMessage; public IntPtr IconHandle; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string Tip; public uint State; public uint StateMask; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string Info; public uint TimeoutOrVersion; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] public string InfoTitle; public uint InfoFlags; public Guid GuidItem; public IntPtr BalloonIcon; } [StructLayout(LayoutKind.Sequential)] private struct Point { public int X; public int Y; } private delegate IntPtr WindowProcedure(IntPtr window, uint message, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern ushort RegisterClassEx(ref WindowClass windowClass); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr CreateWindowEx(uint extendedStyle, string className, string windowName, uint style, int x, int y, int width, int height, IntPtr parent, IntPtr menu, IntPtr instance, IntPtr parameter); [DllImport("user32.dll")] private static extern IntPtr DefWindowProc(IntPtr window, uint message, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr LoadImage(IntPtr instance, string name, uint type, int width, int height, uint load); [DllImport("user32.dll")] private static extern bool DestroyIcon(IntPtr icon); [DllImport("user32.dll")] private static extern bool DestroyWindow(IntPtr window); [DllImport("user32.dll")] private static extern IntPtr CreatePopupMenu(); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern bool AppendMenu(IntPtr menu, uint flags, uint id, string? text); [DllImport("user32.dll")] private static extern uint TrackPopupMenu(IntPtr menu, uint flags, int x, int y, int reserved, IntPtr window, IntPtr rectangle); [DllImport("user32.dll")] private static extern bool DestroyMenu(IntPtr menu); [DllImport("user32.dll")] private static extern bool GetCursorPos(out Point point); [DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr window); [DllImport("shell32.dll", CharSet = CharSet.Unicode)] private static extern bool Shell_NotifyIcon(uint message, ref NotifyIconData data); [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr GetModuleHandle(string? moduleName); private static bool ShellNotifyIcon(uint message, ref NotifyIconData data) => Shell_NotifyIcon(message, ref data); }