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.

366 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()
  68. {
  69. var exePath = RuntimeUtility.ExecutablePath;
  70. var exeIcon = System.Drawing.Icon.ExtractAssociatedIcon(exePath);
  71. return exeIcon;
  72. }
  73. /// <summary>获取 EXE 文件的图标。</summary>
  74. /// <exception cref="ArgumentNullException" />
  75. /// <exception cref="FileNotFoundException" />
  76. public static Icon GetExeIcon(string filePath)
  77. {
  78. if (filePath.IsEmpty()) throw new ArgumentNullException(nameof(filePath));
  79. if (!File.Exists(filePath)) throw new FileNotFoundException($"文件【{filePath}】不存在。");
  80. var icon = System.Drawing.Icon.ExtractAssociatedIcon(filePath);
  81. return icon;
  82. }
  83. #endregion
  84. #region tip
  85. /// <summary>鼠标点击弹出提示时执行的程序。</summary>
  86. public Action OnTipClick { get; set; }
  87. /// <summary>显示悬浮通知。</summary>
  88. /// <exception cref="ObjectDisposedException"></exception>
  89. public void ShowTip(string text, string title = null, ToolTipIcon icon = ToolTipIcon.None)
  90. {
  91. if (_disposed) throw new ObjectDisposedException(ToString());
  92. if (text.IsEmpty()) return;
  93. _origin?.ShowBalloonTip(5000, title, text, icon);
  94. }
  95. #endregion
  96. #region menu
  97. /// <summary>清除右键菜单。</summary>
  98. public void ClearMenu()
  99. {
  100. if (_disposed) throw new ObjectDisposedException(ToString());
  101. SyncInvoke(() =>
  102. {
  103. #if NETFRAMEWORK
  104. if (_origin.ContextMenu != null)
  105. {
  106. var old = _origin.ContextMenu;
  107. _origin.ContextMenu = null;
  108. RuntimeUtility.Dispose(old);
  109. }
  110. #endif
  111. if (_origin.ContextMenuStrip != null)
  112. {
  113. var old = _origin.ContextMenuStrip;
  114. _origin.ContextMenuStrip = null;
  115. RuntimeUtility.Dispose(old);
  116. }
  117. });
  118. }
  119. /// <summary>设置右键菜单。</summary>
  120. public void SetMenu(params MenuItem[] menu)
  121. {
  122. ClearMenu();
  123. if (menu == null) return;
  124. #if NETFRAMEWORK
  125. SetMenu(menu.ContextMenu());
  126. #else
  127. SetMenu(menu.ContextMenuStrip());
  128. #endif
  129. }
  130. #if NETFRAMEWORK
  131. /// <summary>设置右键菜单。</summary>
  132. public void SetMenu(ContextMenu menu)
  133. {
  134. if (_disposed) throw new ObjectDisposedException(ToString());
  135. ClearMenu();
  136. SyncInvoke(() =>
  137. {
  138. if (menu != null) _origin.ContextMenu = menu;
  139. });
  140. }
  141. #endif
  142. /// <summary>设置右键菜单。</summary>
  143. public void SetMenu(ContextMenuStrip menu)
  144. {
  145. if (_disposed) throw new ObjectDisposedException(ToString());
  146. ClearMenu();
  147. SyncInvoke(() =>
  148. {
  149. if (menu != null) _origin.ContextMenuStrip = menu;
  150. });
  151. }
  152. #endregion
  153. #region instance
  154. bool _disposed = false;
  155. SynchronizationContext _sync = null;
  156. Form _form = null;
  157. Action<Tray> _action = null;
  158. /// <summary>创建新的托盘实例。</summary>
  159. /// <param name="action">创建成功后,执行的回调。</param>
  160. /// <exception cref="InvalidOperationException"></exception>
  161. public Tray(Action<Tray> action)
  162. {
  163. _sync = SynchronizationContext.Current;
  164. if (_sync == null) throw new InvalidOperationException($"当前线程无法启动 {nameof(Tray)} 实例。");
  165. _origin = new NotifyIcon();
  166. _origin.Click += (s, e) => OnClick?.Invoke();
  167. _origin.DoubleClick += (s, e) => OnDoubleClick?.Invoke();
  168. _origin.BalloonTipClicked += (s, e) => OnTipClick?.Invoke();
  169. _form = new Form();
  170. _form.FormBorderStyle = FormBorderStyle.None;
  171. _form.Opacity = 0;
  172. _form.ShowInTaskbar = false;
  173. _form.Size = new Size(0, 0);
  174. _form.StartPosition = FormStartPosition.CenterScreen;
  175. _form.TopMost = true;
  176. _action = action;
  177. _form.Load += Load;
  178. _form.Show();
  179. _form.Visible = false;
  180. }
  181. /// <summary></summary>
  182. public void Dispose()
  183. {
  184. if (_disposed) throw new ObjectDisposedException(ToString());
  185. ClearMenu();
  186. ClearIcon();
  187. _origin.Dispose();
  188. _origin = null;
  189. _form.Dispose();
  190. _form = null;
  191. _disposed = true;
  192. }
  193. void Load(object sender, EventArgs e)
  194. {
  195. RuntimeUtility.InBackground(() =>
  196. {
  197. _action?.Invoke(this);
  198. });
  199. }
  200. /// <summary></summary>
  201. public void SyncInvoke(Action action)
  202. {
  203. if (action == null) return;
  204. if (_sync == null) action.Invoke();
  205. else _sync.Send(state => action.Invoke(), null);
  206. }
  207. /// <summary></summary>
  208. public void FormInvoke(Action action)
  209. {
  210. if (action == null) return;
  211. if (_form == null) return;
  212. if (_form.InvokeRequired)
  213. {
  214. _form.Invoke(action);
  215. return;
  216. }
  217. action.Invoke();
  218. }
  219. /// <summary></summary>
  220. public void FormBeginInvoke(Action action)
  221. {
  222. if (action == null) return;
  223. if (_form == null) return;
  224. _form.BeginInvoke(action);
  225. }
  226. /// <summary>显示对话框,并获取结果。</summary>
  227. /// <exception cref="ArgumentNullException" />
  228. public DialogResult ShowDialog(CommonDialog dialog)
  229. {
  230. if (_disposed) throw new ObjectDisposedException(ToString());
  231. if (dialog == null) throw new ArgumentNullException(nameof(dialog));
  232. if (_form.InvokeRequired)
  233. {
  234. var result = default(DialogResult);
  235. _form.Invoke(() =>
  236. {
  237. _form.Visible = true;
  238. result = dialog.ShowDialog();
  239. _form.Visible = false;
  240. });
  241. return result;
  242. }
  243. return dialog.ShowDialog();
  244. }
  245. /// <summary></summary>
  246. public void Exit()
  247. {
  248. if (!_disposed) Dispose();
  249. Application.Exit();
  250. }
  251. /// <summary></summary>
  252. public void Exit(Action customExit)
  253. {
  254. if (!_disposed) Dispose();
  255. customExit.Invoke();
  256. }
  257. #endregion
  258. #region run
  259. /// <summary>唯一实例。</summary>
  260. public static Tray Instance { get; private set; }
  261. /// <summary>已启动的服务名称。</summary>
  262. public static string ServiceName { get; private set; }
  263. /// <summary>在当前线程运行托盘程序,并启动消息循环。此方法应在主线程中调用,并且此方法将阻塞当前线程。</summary>
  264. /// <remarks>托盘启动后,将在后台线程执行参数中指定的程序。</remarks>
  265. /// <param name="action">(在后台线程执行)启动托盘后执行的程序。</param>
  266. /// <exception cref="ArgumentNullException" />
  267. /// <exception cref="InvalidOperationException" />
  268. [STAThread]
  269. public static void Run(Action<Tray> action)
  270. {
  271. if (action == null) throw new ArgumentNullException(nameof(action));
  272. if (Instance != null) throw new InvalidOperationException($"已存在实例,无法再次启动。");
  273. Control.CheckForIllegalCrossThreadCalls = false;
  274. Application.EnableVisualStyles();
  275. Application.SetCompatibleTextRenderingDefault(false);
  276. if (SynchronizationContext.Current == null)
  277. {
  278. var sc = AsyncOperationManager.SynchronizationContext;
  279. if (sc == null || sc.GetType() == typeof(SynchronizationContext))
  280. {
  281. new PermissionSet(PermissionState.Unrestricted).Assert();
  282. try { AsyncOperationManager.SynchronizationContext = new WindowsFormsSynchronizationContext(); }
  283. finally { CodeAccessPermission.RevertAssert(); }
  284. }
  285. }
  286. Instance = new Tray(action);
  287. // action.Invoke(instance);
  288. Application.Run();
  289. }
  290. /// <summary>启动服务。</summary>
  291. /// <param name="onStart">(在后台线程执行)服务启动后执行的程序。</param>
  292. /// <param name="onStop">(同步执行)停止服务时执行的程序。</param>
  293. public static void Service(Action onStart, Action onStop = null)
  294. {
  295. var processName = Process.GetCurrentProcess().ProcessName;
  296. var service = new WindowsService(processName, onStart, onStop);
  297. ServiceBase.Run(service);
  298. }
  299. #endregion
  300. }
  301. }