using System.Runtime.InteropServices; using LumaTunnel.Client.Interface; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; using XFEExtension.NetCore.WinUIHelper.Implements.Services; namespace LumaTunnel.Client.Implements; public sealed class TrayService : GlobalServiceBase, ITrayService { private const uint CallbackMessage = 0x8001; private const uint LeftDoubleClick = 0x0203; private readonly ManualResetEventSlim _ready = new(); private Thread? _thread; private IntPtr _window; private IntPtr _icon; private WndProc? _wndProc; private DispatcherQueue? _dispatcher; private Action? _showWindow; public void Initialize(Window window, Action showWindow) { if (_thread is not null) return; _dispatcher = window.DispatcherQueue; _showWindow = showWindow; _thread = new Thread(MessageLoop) { IsBackground = true, Name = "LumaTunnel.Tray" }; _thread.Start(); _ready.Wait(TimeSpan.FromSeconds(3)); } public void Dispose() { if (_window != IntPtr.Zero) PostMessage(_window, 0x0010, IntPtr.Zero, IntPtr.Zero); _thread?.Join(TimeSpan.FromSeconds(2)); _ready.Dispose(); GC.SuppressFinalize(this); } private void MessageLoop() { _wndProc = WindowProcedure; var className = "LumaTunnelTrayWindow." + Environment.ProcessId; var windowClass = new WindowClass { ClassName = className, Instance = GetModuleHandle(null), WindowProcedure = Marshal.GetFunctionPointerForDelegate(_wndProc) }; RegisterClass(ref windowClass); _window = CreateWindowEx(0, className, className, 0, 0, 0, 0, 0, new IntPtr(-3), IntPtr.Zero, windowClass.Instance, IntPtr.Zero); _icon = LoadImage(IntPtr.Zero, Path.Combine(AppContext.BaseDirectory, "Assets", "LumaTunnel.ico"), 1, 0, 0, 0x0010 | 0x0040); var data = CreateNotifyData(0); ShellNotifyIcon(0, ref data); _ready.Set(); while (GetMessage(out var message, IntPtr.Zero, 0, 0) > 0) { TranslateMessage(ref message); DispatchMessage(ref message); } data = CreateNotifyData(0); ShellNotifyIcon(2, ref data); if (_icon != IntPtr.Zero) DestroyIcon(_icon); _window = IntPtr.Zero; } private IntPtr WindowProcedure(IntPtr window, uint message, IntPtr wParam, IntPtr lParam) { if (message == CallbackMessage && unchecked((uint)lParam.ToInt64()) == LeftDoubleClick) _dispatcher?.TryEnqueue(() => _showWindow?.Invoke()); if (message == 0x0002) PostQuitMessage(0); return DefWindowProc(window, message, wParam, lParam); } private NotifyIconData CreateNotifyData(uint flags) => new() { Size = Marshal.SizeOf(), Window = _window, Id = 1, Flags = flags == 0 ? 0x1u | 0x2u | 0x4u : flags, CallbackMessage = CallbackMessage, Icon = _icon, Tip = "LumaTunnel 光隧", Info = string.Empty, InfoTitle = string.Empty }; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct WindowClass { public uint Style; public IntPtr WindowProcedure; public int ClassExtra; public int WindowExtra; public IntPtr Instance; public IntPtr Icon; public IntPtr Cursor; public IntPtr Background; [MarshalAs(UnmanagedType.LPWStr)] public string? MenuName; [MarshalAs(UnmanagedType.LPWStr)] public string ClassName; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct NotifyIconData { public int Size; public IntPtr Window; public uint Id; public uint Flags; public uint CallbackMessage; public IntPtr Icon; [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 Item; public IntPtr BalloonIcon; } [StructLayout(LayoutKind.Sequential)] private struct Message { public IntPtr Window; public uint Value; public UIntPtr WParam; public IntPtr LParam; public uint Time; public int X; public int Y; } private delegate IntPtr WndProc(IntPtr window, uint message, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern ushort RegisterClass(ref WindowClass windowClass); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr CreateWindowEx(uint exStyle, 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")] private static extern int GetMessage(out Message message, IntPtr window, uint min, uint max); [DllImport("user32.dll")] private static extern bool TranslateMessage(ref Message message); [DllImport("user32.dll")] private static extern IntPtr DispatchMessage(ref Message message); [DllImport("user32.dll")] private static extern void PostQuitMessage(int exitCode); [DllImport("user32.dll")] private static extern bool PostMessage(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("kernel32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr GetModuleHandle(string? moduleName); [DllImport("shell32.dll", CharSet = CharSet.Unicode)] private static extern bool ShellNotifyIcon(uint message, ref NotifyIconData data); }