From 16e6f3c71bbca3d5e522113203036426f10f1e39 Mon Sep 17 00:00:00 2001 From: Elivo Date: Tue, 15 Jul 2025 22:47:20 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20Tray=EF=BC=8C=E6=8F=90?= =?UTF-8?q?=E4=BE=9B=E5=9C=A8=E6=89=98=E7=9B=98=E4=B8=AD=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E7=9A=84=E6=96=B9=E5=BC=8F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Apewer.Windows/Apewer.Windows.csproj | 10 +- Apewer.Windows/Internals/Interop/User32.cs | 7 + Apewer.Windows/WinForm/Extensions.cs | 83 +++++ Apewer.Windows/WinForm/HotKey.cs | 148 +++++++++ Apewer.Windows/WinForm/MenuItem.cs | 26 ++ Apewer.Windows/WinForm/ModifierKey.cs | 124 +------ Apewer.Windows/WinForm/Tray.cs | 360 +++++++++++++++++++++ 7 files changed, 642 insertions(+), 116 deletions(-) create mode 100644 Apewer.Windows/WinForm/Extensions.cs create mode 100644 Apewer.Windows/WinForm/HotKey.cs create mode 100644 Apewer.Windows/WinForm/MenuItem.cs create mode 100644 Apewer.Windows/WinForm/Tray.cs diff --git a/Apewer.Windows/Apewer.Windows.csproj b/Apewer.Windows/Apewer.Windows.csproj index 75f1a8a..9b74ca8 100644 --- a/Apewer.Windows/Apewer.Windows.csproj +++ b/Apewer.Windows/Apewer.Windows.csproj @@ -19,6 +19,8 @@ + + @@ -33,6 +35,7 @@ + @@ -52,6 +55,7 @@ + @@ -66,14 +70,10 @@ + - - - Form - - \ No newline at end of file diff --git a/Apewer.Windows/Internals/Interop/User32.cs b/Apewer.Windows/Internals/Interop/User32.cs index d217567..c611b8d 100644 --- a/Apewer.Windows/Internals/Interop/User32.cs +++ b/Apewer.Windows/Internals/Interop/User32.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; +using System.Windows.Forms; namespace Apewer.Internals.Interop { @@ -212,6 +213,9 @@ namespace Apewer.Internals.Interop [DllImport("user32.dll ", CharSet = CharSet.Unicode)] public static extern IntPtr PostMessage(IntPtr hwnd, int wMsg, int wParam, int lParam); + [DllImport("user32.dll")] + public static extern bool RegisterHotKey(IntPtr hWnd, int id, int fsModifiers, Keys vk); + /// /// [DllImport("user32.dll", EntryPoint = "ReleaseCapture")] @@ -332,6 +336,9 @@ namespace Apewer.Internals.Interop [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] public static extern bool UnhookWindowsHookEx(int idHook); + [DllImport("user32.dll")] + public static extern bool UnregisterHotKey(IntPtr hWnd, int id); + /// /// /// diff --git a/Apewer.Windows/WinForm/Extensions.cs b/Apewer.Windows/WinForm/Extensions.cs new file mode 100644 index 0000000..5788005 --- /dev/null +++ b/Apewer.Windows/WinForm/Extensions.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Windows.Forms; + +namespace Apewer.WinForm +{ + + /// Windows 窗体工具。 + public static class Extensions + { + + static void MenuItemEventHandler(object sender, EventArgs e) + { +#if NETFRAMEWORK + var mi = sender as System.Windows.Forms.MenuItem; + if (mi != null) + { + var tag = mi.Tag as MenuItem; + if (tag != null && tag.Action != null) tag.Action.Invoke(tag); + } +#endif + + var tsmi = sender as System.Windows.Forms.ToolStripMenuItem; + if (tsmi != null) + { + var tag = tsmi.Tag as MenuItem; + if (tag != null && tag.Action != null) tag.Action.Invoke(tag); + } + } + +#if NETFRAMEWORK + + /// 生成 实例。 + /// + public static ContextMenu ContextMenu(this IEnumerable items) + { + if (items == null) throw new ArgumentNullException(nameof(items)); + + var cm = new ContextMenu(); + foreach (var item in items) + { + var isLine = item == null || item.Text == null || item.Text == "" || item.Text == "-"; + var text = isLine ? "-" : item?.Text; + var mi = new System.Windows.Forms.MenuItem(text, MenuItemEventHandler); + mi.Enabled = !isLine && item.Action != null; + mi.Tag = item; + + cm.MenuItems.Add(mi); + } + + return cm; + } + +#endif + + /// 生成 实例。 + /// + public static ContextMenuStrip ContextMenuStrip(this IEnumerable items) + { + if (items == null) throw new ArgumentNullException(nameof(items)); + + var cms = new ContextMenuStrip(); + foreach (var item in items) + { + if (item == null) continue; + + var isLine = item.Text == null || item.Text == "" || item.Text == "-"; + var text = isLine ? "-" : item.Text; + var tsmi = new ToolStripMenuItem(text, item.Image, MenuItemEventHandler); + tsmi.AutoSize = true; + tsmi.Height = isLine ? 19 : 30; + tsmi.Enabled = item.Action != null && item.Action != null; + tsmi.Tag = item; + + cms.Items.Add(tsmi); + } + + return cms; + } + + } + +} diff --git a/Apewer.Windows/WinForm/HotKey.cs b/Apewer.Windows/WinForm/HotKey.cs new file mode 100644 index 0000000..54ff349 --- /dev/null +++ b/Apewer.Windows/WinForm/HotKey.cs @@ -0,0 +1,148 @@ +using System; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace Apewer.WinForm +{ + + /// 热键。 + public sealed class HotKey : IMessageFilter + { + + #region instance + + IntPtr _pointer = IntPtr.Zero; + Keys _key = Keys.None; + ModifierKey _modifier = ModifierKey.None; + + int _id = 0; + Action _callback = null; + + /// 热键的标识符。 + public int Id { get => _id; } + + /// 修饰键。 + public ModifierKey ModifierKey { get => _modifier; } + + /// 按键。 + public Keys Key { get => _key; } + + private HotKey(IntPtr pointer, Keys key, ModifierKey modifier, Action callback) + { + if (callback == null) throw new ArgumentNullException(nameof(callback), "回调函数不能为空。"); + switch (key) + { + case Keys.KeyCode: + case Keys.Modifiers: + case Keys.None: + case Keys.Shift: + case Keys.Control: + case Keys.Alt: + throw new ArgumentException($"按键【{key}】无法注册为热键。"); + } + + // 应用程序必须在0x0000到0xBFFF的范围内指定 ID 值。 + // 共享 DLL 必须通过 0xFFFF(GlobalAddAtom 函数返回的范围)指定0xC000区域中的值。 + _id = ((int)modifier << 16) & (int)key; + + _pointer = pointer; + _callback = callback; + _key = key; + _modifier = modifier; + } + + /// + public override string ToString() + { + var alt = (ModifierKey & ModifierKey.ALT) == ModifierKey.ALT ? "ALT + " : ""; + var ctrl = (ModifierKey & ModifierKey.CTRL) == ModifierKey.CTRL ? "CTRL + " : ""; + var shift = (ModifierKey & ModifierKey.SHIFT) == ModifierKey.SHIFT ? "SHIFT + " : ""; + var win = (ModifierKey & ModifierKey.WIN) == ModifierKey.WIN ? "WIN + " : ""; + var result = $"{alt}{ctrl}{shift}{win}{Key}"; + return result; + } + + #endregion + + #region bind + + const int WM_HOTKEY = 0x0312; + + [DllImport("user32.dll")] + static extern bool RegisterHotKey(IntPtr hWnd, int id, int fsModifiers, Keys vk); + + [DllImport("user32.dll")] + static extern bool UnregisterHotKey(IntPtr hWnd, int id); + + bool IMessageFilter.PreFilterMessage(ref Message m) + { + if (m.Msg == WM_HOTKEY && (int)m.WParam == _id) + { + _callback?.Invoke(this); + return true; + } + return false; + } + + void Bind() + { + var success = RegisterHotKey(_pointer, _id, (int)_modifier, _key); + if (!success) + { + var error = Marshal.GetLastWin32Error(); + if (error == 1409) throw new Exception($"热键【{ToString()}】已被占用。"); + throw new Exception($"错误 {error}:注册热键【{ToString()}】失败。"); + } + + Application.AddMessageFilter(this); + } + + /// 释放热键。 + public void Release() + { + UnregisterHotKey(Application.OpenForms[0].Handle, _id); + } + + #endregion + + // Virtual-Key 代码 + // https://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes + + /// 绑定热键。 + /// 将接收热键生成的 WM_HOTKEY(0x0312)消息的窗口句柄。 + /// 热键的虚拟键代码。 + /// 修饰键。 + /// 回调。 + /// + /// + /// + public static HotKey Bind(Form form, Keys key, ModifierKey modifier, Action callback) + { + if (form == null) throw new ArgumentNullException(nameof(form)); + if (callback == null) throw new ArgumentNullException(nameof(callback)); + + var hotkey = new HotKey(form.Handle, key, modifier, callback); + hotkey.Bind(); + return hotkey; + } + + /// 绑定热键。 + /// WM_HOTKEY 消息将发布到调用线程的消息队列,并且必须在消息循环中进行处理。 + /// 热键的虚拟键代码。 + /// 修饰键。 + /// 回调。 + /// + /// + /// + public static HotKey Bind(Keys key, ModifierKey modifier, Action callback) + { + if (callback == null) throw new ArgumentNullException(nameof(callback)); + + var hotkey = new HotKey(IntPtr.Zero, key, modifier, callback); + hotkey.Bind(); + return hotkey; + } + + } + +} diff --git a/Apewer.Windows/WinForm/MenuItem.cs b/Apewer.Windows/WinForm/MenuItem.cs new file mode 100644 index 0000000..50db338 --- /dev/null +++ b/Apewer.Windows/WinForm/MenuItem.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Drawing; + +namespace Apewer.WinForm +{ + + /// 菜单项。 + public class MenuItem + { + + /// 显示的文本,不设置此属性时菜单将显示未为分隔线。 + public string Text { get; set; } + + /// 点击菜单时要执行的命令。 + public Action Action { get; set; } + + /// 菜单中显示的图片。 + public Image Image { get; set; } + + /// 自定义数据。 + public virtual object Data { get; set; } + + } + +} diff --git a/Apewer.Windows/WinForm/ModifierKey.cs b/Apewer.Windows/WinForm/ModifierKey.cs index 1bdc7cf..a16dece 100644 --- a/Apewer.Windows/WinForm/ModifierKey.cs +++ b/Apewer.Windows/WinForm/ModifierKey.cs @@ -1,123 +1,25 @@ -using System; - -namespace Apewer.WinForm +namespace Apewer.WinForm { /// 修饰键。 - public struct ModifierKey : IEquatable + public enum ModifierKey : int { - byte _value; - bool _alt; - bool _ctrl; - bool _shift; - bool _win; - - /// 包含 ALT 键。 - public bool WithALT { get => _alt; } - - /// 包含 CTRL 键。 - public bool WithCTRL { get => _ctrl; } - - /// 包含 SHIFT 键。 - public bool WithSHIFT { get => _shift; } - - /// 包含 WIN 键。 - public bool WithWIN { get => _win; } - - /// 修饰键组合的值。 - public byte Value { get => _value; } - - /// - private ModifierKey(byte value) - { - _alt = value << 7 >> 7 == 1; - _ctrl = value << 6 >> 7 == 1; - _shift = value << 5 >> 7 == 1; - _win = value << 4 >> 7 == 1; - _value = Convert.ToByte(value); - if (_value != value) throw new OverflowException($"参数【{value}】含有无效的键。"); - } - - private ModifierKey(bool alt, bool ctrl, bool shift, bool win) - { - _alt = alt; - _ctrl = ctrl; - _shift = shift; - _win = win; - - var value = 0; - if (alt) value += 0x01; - if (ctrl) value += 0x02; - if (shift) value += 0x04; - if (win) value += 0x05; - _value = Convert.ToByte(value); - } - - /// - public override int GetHashCode() => _value; - - /// - public override bool Equals(object obj) - { - if (obj is ModifierKey b) return _value == b._value; - return false; - } - - /// - public bool Equals(ModifierKey another) => _value == another._value; - - /// - public static implicit operator byte(ModifierKey instance) => instance._value; - - /// - /// - public static implicit operator ModifierKey(byte value) => new ModifierKey(value); - - /// - /// - public static ModifierKey operator +(ModifierKey a, ModifierKey b) - { - if (a._alt && b._alt) throw new ArgumentException("相加的两个组合键共同包含了 ALT 键。"); - if (a._ctrl && b._ctrl) throw new ArgumentException("相加的两个组合键共同包含了 CTRL 键。"); - if (a._shift && b._shift) throw new ArgumentException("相加的两个组合键共同包含了 SHIFT 键。"); - if (a._win && b._win) throw new ArgumentException("相加的两个组合键共同包含了 WIN 键。"); - - var alt = a._alt || b._alt; - var ctrl = a._ctrl || b._ctrl; - var shift = a._shift || b._shift; - var win = a._win || b._win; - return new ModifierKey(alt, ctrl, shift, win); - } - - /// - public static ModifierKey operator -(ModifierKey a, ModifierKey b) - { - var alt = a._alt; - var ctrl = a._ctrl; - var shift = a._shift; - var win = a._win; - - if (b._alt) alt = false; - if (b._ctrl) ctrl = false; - if (b._shift) shift = false; - if (b._win) win = false; - - return new ModifierKey(alt, ctrl, shift, win); - } + /// 表示未按下任何修饰键。 + None = 0, - /// 表示 ALT 修饰键。 - public static ModifierKey ALT { get; } = new ModifierKey(0x01); + /// 表示按下了 ALT 键。 + ALT = 0x0001, - /// 表示 CTRL 修饰键。 - public static ModifierKey CTRL { get; } = new ModifierKey(0x02); + /// 表示按下了 CTRL 键。 + CTRL = 0x0002, - /// 表示 SHIFT 修饰键。 - public static ModifierKey SHIFT { get; } = new ModifierKey(0x04); + /// 表示按下了 SHIFT 键。 + SHIFT = 0x0004, - /// 表示 WIN 修饰键。 - public static ModifierKey WIN { get; } = new ModifierKey(0x08); + /// 表示按下了 Windows 键。 + WIN = 0x0008 } -} +} \ No newline at end of file diff --git a/Apewer.Windows/WinForm/Tray.cs b/Apewer.Windows/WinForm/Tray.cs new file mode 100644 index 0000000..8546e43 --- /dev/null +++ b/Apewer.Windows/WinForm/Tray.cs @@ -0,0 +1,360 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Security; +using System.Security.Permissions; +using System.ServiceProcess; +using System.Threading; +using System.Windows.Forms; + +namespace Apewer.WinForm +{ + + /// 通用托盘。 + public sealed class Tray : IDisposable + { + + #region icon + + NotifyIcon _origin = null; + + /// 通知图标。 + NotifyIcon Icon { get => _origin; } + + /// 鼠标点击通知图标时执行的程序。 + public Action OnClick { get; set; } + + /// 鼠标点击通知图标时执行的程序。 + public Action OnDoubleClick { get; set; } + + /// 设置图标。 + public void SetIcon(Icon icon) + { + if (_disposed) throw new ObjectDisposedException(ToString()); + + ClearIcon(); + SyncInvoke(() => + { + if (icon == null) + { + _origin.Visible = false; + } + else + { + _origin.Icon = icon; + _origin.Visible = true; + } + }); + } + + /// 设置标题。 + public void SetTitle(string title) + { + if (_disposed) throw new ObjectDisposedException(ToString()); + if (_origin != null) + { + SyncInvoke(() => + { + _origin.Text = title; + }); + } + } + + void ClearIcon() + { + SyncInvoke(() => + { + if (_origin != null) + { + var old = _origin.Icon; + _origin.Icon = null; + RuntimeUtility.Dispose(old); + } + }); + } + + /// 获取 EXE 文件的图标。 + public static Icon GetExeIcon() => System.Drawing.Icon.ExtractAssociatedIcon(RuntimeUtility.ExecutablePath); + + /// 获取 EXE 文件的图标。 + /// + /// + public static Icon GetExeIcon(string filePath) + { + if (filePath.IsEmpty()) throw new ArgumentNullException(nameof(filePath)); + if (!File.Exists(filePath)) throw new FileNotFoundException($"文件【{filePath}】不存在。"); + var icon = System.Drawing.Icon.ExtractAssociatedIcon(filePath); + return icon; + } + + #endregion + + #region tip + + /// 鼠标点击弹出提示时执行的程序。 + public Action OnTipClick { get; set; } + + /// 显示悬浮通知。 + /// + public void ShowTip(string text, string title = null, ToolTipIcon icon = ToolTipIcon.None) + { + if (_disposed) throw new ObjectDisposedException(ToString()); + if (text.IsEmpty()) return; + _origin?.ShowBalloonTip(5000, title, text, icon); + } + + #endregion + + #region menu + + /// 清除右键菜单。 + public void ClearMenu() + { + if (_disposed) throw new ObjectDisposedException(ToString()); + + SyncInvoke(() => + { +#if NETFRAMEWORK + if (_origin.ContextMenu != null) + { + var old = _origin.ContextMenu; + _origin.ContextMenu = null; + RuntimeUtility.Dispose(old); + } +#endif + + if (_origin.ContextMenuStrip != null) + { + var old = _origin.ContextMenuStrip; + _origin.ContextMenuStrip = null; + RuntimeUtility.Dispose(old); + } + }); + } + + /// 设置右键菜单。 + public void SetMenu(MenuItem[] menu) + { + ClearMenu(); + if (menu == null) return; +#if NETFRAMEWORK + SetMenu(menu.ContextMenu()); +#else + SetMenu(menu.ContextMenuStrip()); +#endif + } + +#if NETFRAMEWORK + + /// 设置右键菜单。 + public void SetMenu(ContextMenu menu) + { + if (_disposed) throw new ObjectDisposedException(ToString()); + + ClearMenu(); + SyncInvoke(() => + { + if (menu != null) _origin.ContextMenu = menu; + }); + } + +#endif + + /// 设置右键菜单。 + public void SetMenu(ContextMenuStrip menu) + { + if (_disposed) throw new ObjectDisposedException(ToString()); + + ClearMenu(); + SyncInvoke(() => + { + if (menu != null) _origin.ContextMenuStrip = menu; + }); + } + + #endregion + + #region instance + + bool _disposed = false; + SynchronizationContext _sync = null; + Form _form = null; + Action _action = null; + + /// + /// + public Tray(Action action) + { + _sync = SynchronizationContext.Current; + if (_sync == null) throw new InvalidOperationException($"当前线程无法启动 {nameof(Tray)} 实例。"); + + _origin = new NotifyIcon(); + _origin.Click += (s, e) => OnClick?.Invoke(); + _origin.DoubleClick += (s, e) => OnDoubleClick?.Invoke(); + _origin.BalloonTipClicked += (s, e) => OnTipClick?.Invoke(); + + _form = new Form(); + _form.FormBorderStyle = FormBorderStyle.None; + _form.Opacity = 0; + _form.ShowInTaskbar = false; + _form.Size = new Size(0, 0); + _form.StartPosition = FormStartPosition.CenterScreen; + _form.TopMost = true; + + _action = action; + _form.Load += Load; + _form.Show(); + _form.Visible = false; + } + + + /// + public void Dispose() + { + if (_disposed) throw new ObjectDisposedException(ToString()); + + ClearMenu(); + ClearIcon(); + + _origin.Dispose(); + _origin = null; + + _form.Dispose(); + _form = null; + + _disposed = true; + } + + void Load(object sender, EventArgs e) + { + RuntimeUtility.InBackground(() => + { + _action?.Invoke(this); + }); + } + + /// + public void SyncInvoke(Action action) + { + if (action == null) return; + + if (_sync == null) action.Invoke(); + else _sync.Send(state => action.Invoke(), null); + } + + /// + public void FormInvoke(Action action) + { + if (action == null) return; + if (_form == null) return; + if (_form.InvokeRequired) + { + _form.Invoke(action); + return; + } + + action.Invoke(); + } + + /// 显示对话框,并获取结果。 + /// + public DialogResult ShowDialog(CommonDialog dialog) + { + if (_disposed) throw new ObjectDisposedException(ToString()); + if (dialog == null) throw new ArgumentNullException(nameof(dialog)); + + if (_form.InvokeRequired) + { + var result = default(DialogResult); + _form.Invoke(() => + { + _form.Visible = true; + result = dialog.ShowDialog(); + _form.Visible = false; + }); + return result; + } + + return dialog.ShowDialog(); + } + + /// + public void Exit() + { + if (!_disposed) Dispose(); + Application.Exit(); + } + + #endregion + + #region run + + /// 已启动的服务名称。 + public static string ServiceName { get; private set; } + + /// 在当前线程运行托盘程序,并启动消息循环。 + /// 启动托盘后执行的程序。 + /// + [STAThread] + public static void Run(Action action) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + + Control.CheckForIllegalCrossThreadCalls = false; + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + if (SynchronizationContext.Current == null) + { + var sc = AsyncOperationManager.SynchronizationContext; + if (sc == null || sc.GetType() == typeof(SynchronizationContext)) + { + new PermissionSet(PermissionState.Unrestricted).Assert(); + try { AsyncOperationManager.SynchronizationContext = new WindowsFormsSynchronizationContext(); } + finally { CodeAccessPermission.RevertAssert(); } + } + } + + var instance = new Tray(action); + // action.Invoke(instance); + Application.Run(); + } + + /// 启动服务。 + /// 服务启动后执行的程序。 + /// 停止服务时执行的程序。 + public static void Service(Action onStart, Action onStop = null) + { + var processName = Process.GetCurrentProcess().ProcessName; + var service = new TrayService(processName, onStart, onStop); + ServiceBase.Run(service); + } + + class TrayService : ServiceBase + { + + Action on_start; + Action on_stop; + + public TrayService(string serviceName, Action onStart, Action onStop) + { + if (serviceName.IsEmpty()) throw new ArgumentNullException(nameof(serviceName)); + if (onStart == null) throw new ArgumentNullException(nameof(onStart)); + ServiceName = serviceName; + on_start = onStart; + on_stop = onStop; + } + + protected override void OnStart(string[] args) => RuntimeUtility.InBackground(on_start); + + protected override void OnStop() => on_stop?.Invoke(); + + } + + #endregion + + } + +}