krona-staff/apps/KronaStaffTerminal/Services/TrayService.cs

266 lines
10 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Runtime.InteropServices;
namespace KronaStaffTerminal.Services;
public enum TrayState
{
Normal,
NoScanner,
NoServer,
Error
}
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 const uint StatusCommand = 1003;
private const uint SyncCommand = 1004;
private readonly Action _showWindow;
private readonly Action _requestExit;
private readonly Action? _showStatus;
private readonly Action? _requestSync;
private readonly WindowProcedure _windowProcedure;
private readonly IntPtr _windowHandle;
private readonly IntPtr _iconHandle;
private NotifyIconData _iconData;
private bool _disposed;
private TrayState _currentState = TrayState.Normal;
private int _pendingSyncCount;
private string _currentStatusText = "Работает в штатном режиме";
public TrayService(
Action showWindow,
Action requestExit,
Action? showStatus = null,
Action? requestSync = null)
{
_showWindow = showWindow;
_requestExit = requestExit;
_showStatus = showStatus;
_requestSync = requestSync;
_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 UpdateState(TrayState state, int pendingSyncCount, string? statusText = null)
{
_currentState = state;
_pendingSyncCount = pendingSyncCount;
if (!string.IsNullOrWhiteSpace(statusText))
{
_currentStatusText = statusText;
}
var stateLabel = state switch
{
TrayState.Normal => "Работает",
TrayState.NoScanner => "Сканер отключён",
TrayState.NoServer => "Сервер недоступен",
TrayState.Error => "Сбой",
_ => "В работе"
};
var tip = $"Krona Staff — {stateLabel} ({pendingSyncCount} в очереди)";
if (tip.Length > 127) tip = tip[..127];
_iconData.Flags = NifTip | NifIcon;
_iconData.Tip = tip;
ShellNotifyIcon(NimModify, ref _iconData);
}
public void ShowRunningBalloon()
{
ShowNotificationBalloon("Krona Staff", "Терминал продолжает работать в трее.", 1);
}
public void ShowNotificationBalloon(string title, string message, uint infoFlags = 1)
{
_iconData.Flags = NifInfo;
_iconData.InfoTitle = title.Length > 63 ? title[..63] : title;
_iconData.Info = message.Length > 255 ? message[..255] : message;
_iconData.InfoFlags = infoFlags;
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");
var statusSummary = _currentState switch
{
TrayState.Normal => "Состояние: Работает",
TrayState.NoScanner => "Состояние: Сканер отключён",
TrayState.NoServer => "Состояние: Нет связи с сервером",
TrayState.Error => "Состояние: Ошибка",
_ => $"Состояние: {_currentStatusText}"
};
AppendMenu(menu, MfString, StatusCommand, statusSummary);
var syncSummary = _pendingSyncCount > 0
? $"Синхронизация ({_pendingSyncCount} в очереди)"
: "Синхронизация (очередь пуста)";
AppendMenu(menu, MfString, SyncCommand, syncSummary);
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 == StatusCommand)
{
if (_showStatus is not null)
{
Dispatch(_showStatus);
}
else
{
ShowNotificationBalloon("Состояние терминала", _currentStatusText);
}
}
if (command == SyncCommand)
{
if (_requestSync is not null)
{
Dispatch(_requestSync);
}
}
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);
}