You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

360 lines
11 KiB

  1. using System;
  2. using System.ComponentModel;
  3. using System.Diagnostics;
  4. using System.Drawing;
  5. using System.IO;
  6. using System.Security;
  7. using System.Security.Permissions;
  8. using System.ServiceProcess;
  9. using System.Threading;
  10. using System.Windows.Forms;
  11. namespace Apewer.WinForm
  12. {
  13. /// <summary>通用托盘。</summary>
  14. public sealed class Tray : IDisposable
  15. {
  16. #region icon
  17. NotifyIcon _origin = null;
  18. /// <summary>通知图标。</summary>
  19. NotifyIcon Icon { get => _origin; }
  20. /// <summary>鼠标点击通知图标时执行的程序。</summary>
  21. public Action OnClick { get; set; }
  22. /// <summary>鼠标点击通知图标时执行的程序。</summary>
  23. public Action OnDoubleClick { get; set; }
  24. /// <summary>设置图标。</summary>
  25. public void SetIcon(Icon icon)
  26. {
  27. if (_disposed) throw new ObjectDisposedException(ToString());
  28. ClearIcon();
  29. SyncInvoke(() =>
  30. {
  31. if (icon == null)
  32. {
  33. _origin.Visible = false;
  34. }
  35. else
  36. {
  37. _origin.Icon = icon;
  38. _origin.Visible = true;
  39. }
  40. });
  41. }
  42. /// <summary>设置标题。</summary>
  43. public void SetTitle(string title)
  44. {
  45. if (_disposed) throw new ObjectDisposedException(ToString());
  46. if (_origin != null)
  47. {
  48. SyncInvoke(() =>
  49. {
  50. _origin.Text = title;
  51. });
  52. }
  53. }
  54. void ClearIcon()
  55. {
  56. SyncInvoke(() =>
  57. {
  58. if (_origin != null)
  59. {
  60. var old = _origin.Icon;
  61. _origin.Icon = null;
  62. RuntimeUtility.Dispose(old);
  63. }
  64. });
  65. }
  66. /// <summary>获取 EXE 文件的图标。</summary>
  67. public static Icon GetExeIcon() => System.Drawing.Icon.ExtractAssociatedIcon(RuntimeUtility.ExecutablePath);
  68. /// <summary>获取 EXE 文件的图标。</summary>
  69. /// <exception cref="ArgumentNullException" />
  70. /// <exception cref="FileNotFoundException" />
  71. public static Icon GetExeIcon(string filePath)
  72. {
  73. if (filePath.IsEmpty()) throw new ArgumentNullException(nameof(filePath));
  74. if (!File.Exists(filePath)) throw new FileNotFoundException($"文件【{filePath}】不存在。");
  75. var icon = System.Drawing.Icon.ExtractAssociatedIcon(filePath);
  76. return icon;
  77. }
  78. #endregion
  79. #region tip
  80. /// <summary>鼠标点击弹出提示时执行的程序。</summary>
  81. public Action OnTipClick { get; set; }
  82. /// <summary>显示悬浮通知。</summary>
  83. /// <exception cref="ObjectDisposedException"></exception>
  84. public void ShowTip(string text, string title = null, ToolTipIcon icon = ToolTipIcon.None)
  85. {
  86. if (_disposed) throw new ObjectDisposedException(ToString());
  87. if (text.IsEmpty()) return;
  88. _origin?.ShowBalloonTip(5000, title, text, icon);
  89. }
  90. #endregion
  91. #region menu
  92. /// <summary>清除右键菜单。</summary>
  93. public void ClearMenu()
  94. {
  95. if (_disposed) throw new ObjectDisposedException(ToString());
  96. SyncInvoke(() =>
  97. {
  98. #if NETFRAMEWORK
  99. if (_origin.ContextMenu != null)
  100. {
  101. var old = _origin.ContextMenu;
  102. _origin.ContextMenu = null;
  103. RuntimeUtility.Dispose(old);
  104. }
  105. #endif
  106. if (_origin.ContextMenuStrip != null)
  107. {
  108. var old = _origin.ContextMenuStrip;
  109. _origin.ContextMenuStrip = null;
  110. RuntimeUtility.Dispose(old);
  111. }
  112. });
  113. }
  114. /// <summary>设置右键菜单。</summary>
  115. public void SetMenu(MenuItem[] menu)
  116. {
  117. ClearMenu();
  118. if (menu == null) return;
  119. #if NETFRAMEWORK
  120. SetMenu(menu.ContextMenu());
  121. #else
  122. SetMenu(menu.ContextMenuStrip());
  123. #endif
  124. }
  125. #if NETFRAMEWORK
  126. /// <summary>设置右键菜单。</summary>
  127. public void SetMenu(ContextMenu menu)
  128. {
  129. if (_disposed) throw new ObjectDisposedException(ToString());
  130. ClearMenu();
  131. SyncInvoke(() =>
  132. {
  133. if (menu != null) _origin.ContextMenu = menu;
  134. });
  135. }
  136. #endif
  137. /// <summary>设置右键菜单。</summary>
  138. public void SetMenu(ContextMenuStrip menu)
  139. {
  140. if (_disposed) throw new ObjectDisposedException(ToString());
  141. ClearMenu();
  142. SyncInvoke(() =>
  143. {
  144. if (menu != null) _origin.ContextMenuStrip = menu;
  145. });
  146. }
  147. #endregion
  148. #region instance
  149. bool _disposed = false;
  150. SynchronizationContext _sync = null;
  151. Form _form = null;
  152. Action<Tray> _action = null;
  153. /// <summary></summary>
  154. /// <exception cref="InvalidOperationException"></exception>
  155. public Tray(Action<Tray> action)
  156. {
  157. _sync = SynchronizationContext.Current;
  158. if (_sync == null) throw new InvalidOperationException($"当前线程无法启动 {nameof(Tray)} 实例。");
  159. _origin = new NotifyIcon();
  160. _origin.Click += (s, e) => OnClick?.Invoke();
  161. _origin.DoubleClick += (s, e) => OnDoubleClick?.Invoke();
  162. _origin.BalloonTipClicked += (s, e) => OnTipClick?.Invoke();
  163. _form = new Form();
  164. _form.FormBorderStyle = FormBorderStyle.None;
  165. _form.Opacity = 0;
  166. _form.ShowInTaskbar = false;
  167. _form.Size = new Size(0, 0);
  168. _form.StartPosition = FormStartPosition.CenterScreen;
  169. _form.TopMost = true;
  170. _action = action;
  171. _form.Load += Load;
  172. _form.Show();
  173. _form.Visible = false;
  174. }
  175. /// <summary></summary>
  176. public void Dispose()
  177. {
  178. if (_disposed) throw new ObjectDisposedException(ToString());
  179. ClearMenu();
  180. ClearIcon();
  181. _origin.Dispose();
  182. _origin = null;
  183. _form.Dispose();
  184. _form = null;
  185. _disposed = true;
  186. }
  187. void Load(object sender, EventArgs e)
  188. {
  189. RuntimeUtility.InBackground(() =>
  190. {
  191. _action?.Invoke(this);
  192. });
  193. }
  194. /// <summary></summary>
  195. public void SyncInvoke(Action action)
  196. {
  197. if (action == null) return;
  198. if (_sync == null) action.Invoke();
  199. else _sync.Send(state => action.Invoke(), null);
  200. }
  201. /// <summary></summary>
  202. public void FormInvoke(Action action)
  203. {
  204. if (action == null) return;
  205. if (_form == null) return;
  206. if (_form.InvokeRequired)
  207. {
  208. _form.Invoke(action);
  209. return;
  210. }
  211. action.Invoke();
  212. }
  213. /// <summary>显示对话框,并获取结果。</summary>
  214. /// <exception cref="ArgumentNullException" />
  215. public DialogResult ShowDialog(CommonDialog dialog)
  216. {
  217. if (_disposed) throw new ObjectDisposedException(ToString());
  218. if (dialog == null) throw new ArgumentNullException(nameof(dialog));
  219. if (_form.InvokeRequired)
  220. {
  221. var result = default(DialogResult);
  222. _form.Invoke(() =>
  223. {
  224. _form.Visible = true;
  225. result = dialog.ShowDialog();
  226. _form.Visible = false;
  227. });
  228. return result;
  229. }
  230. return dialog.ShowDialog();
  231. }
  232. /// <summary></summary>
  233. public void Exit()
  234. {
  235. if (!_disposed) Dispose();
  236. Application.Exit();
  237. }
  238. #endregion
  239. #region run
  240. /// <summary>已启动的服务名称。</summary>
  241. public static string ServiceName { get; private set; }
  242. /// <summary>在当前线程运行托盘程序,并启动消息循环。</summary>
  243. /// <param name="action">启动托盘后执行的程序。</param>
  244. /// <exception cref="ArgumentNullException" />
  245. [STAThread]
  246. public static void Run(Action<Tray> action)
  247. {
  248. if (action == null) throw new ArgumentNullException(nameof(action));
  249. Control.CheckForIllegalCrossThreadCalls = false;
  250. Application.EnableVisualStyles();
  251. Application.SetCompatibleTextRenderingDefault(false);
  252. if (SynchronizationContext.Current == null)
  253. {
  254. var sc = AsyncOperationManager.SynchronizationContext;
  255. if (sc == null || sc.GetType() == typeof(SynchronizationContext))
  256. {
  257. new PermissionSet(PermissionState.Unrestricted).Assert();
  258. try { AsyncOperationManager.SynchronizationContext = new WindowsFormsSynchronizationContext(); }
  259. finally { CodeAccessPermission.RevertAssert(); }
  260. }
  261. }
  262. var instance = new Tray(action);
  263. // action.Invoke(instance);
  264. Application.Run();
  265. }
  266. /// <summary>启动服务。</summary>
  267. /// <param name="onStart">服务启动后执行的程序。</param>
  268. /// <param name="onStop">停止服务时执行的程序。</param>
  269. public static void Service(Action onStart, Action onStop = null)
  270. {
  271. var processName = Process.GetCurrentProcess().ProcessName;
  272. var service = new TrayService(processName, onStart, onStop);
  273. ServiceBase.Run(service);
  274. }
  275. class TrayService : ServiceBase
  276. {
  277. Action on_start;
  278. Action on_stop;
  279. public TrayService(string serviceName, Action onStart, Action onStop)
  280. {
  281. if (serviceName.IsEmpty()) throw new ArgumentNullException(nameof(serviceName));
  282. if (onStart == null) throw new ArgumentNullException(nameof(onStart));
  283. ServiceName = serviceName;
  284. on_start = onStart;
  285. on_stop = onStop;
  286. }
  287. protected override void OnStart(string[] args) => RuntimeUtility.InBackground(on_start);
  288. protected override void OnStop() => on_stop?.Invoke();
  289. }
  290. #endregion
  291. }
  292. }