266 lines
10 KiB
C#
266 lines
10 KiB
C#
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);
|
||
}
|