krona-staff/apps/KronaStaffTerminal/Services/TrayService.cs
Ochenstarik 3fbeb1fe1b
Some checks are pending
ci / verify (push) Waiting to run
feat: secure terminal lifecycle and attendance scans
2026-09-17 21:13:42 +07:00

176 lines
7.6 KiB
C#

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<WindowClass>(),
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<NotifyIconData>(),
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);
}