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.

380 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(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. /// <exception cref="InvalidOperationException"></exception>
  160. public Tray(Action<Tray> action)
  161. {
  162. _sync = SynchronizationContext.Current;
  163. if (_sync == null) throw new InvalidOperationException($"当前线程无法启动 {nameof(Tray)} 实例。");
  164. _origin = new NotifyIcon();
  165. _origin.Click += (s, e) => OnClick?.Invoke();
  166. _origin.DoubleClick += (s, e) => OnDoubleClick?.Invoke();
  167. _origin.BalloonTipClicked += (s, e) => OnTipClick?.Invoke();
  168. _form = new Form();
  169. _form.FormBorderStyle = FormBorderStyle.None;
  170. _form.Opacity = 0;
  171. _form.ShowInTaskbar = false;
  172. _form.Size = new Size(0, 0);
  173. _form.StartPosition = FormStartPosition.CenterScreen;
  174. _form.TopMost = true;
  175. _action = action;
  176. _form.Load += Load;
  177. _form.Show();
  178. _form.Visible = false;
  179. }
  180. /// <summary></summary>
  181. public void Dispose()
  182. {
  183. if (_disposed) throw new ObjectDisposedException(ToString());
  184. ClearMenu();
  185. ClearIcon();
  186. _origin.Dispose();
  187. _origin = null;
  188. _form.Dispose();
  189. _form = null;
  190. _disposed = true;
  191. }
  192. void Load(object sender, EventArgs e)
  193. {
  194. RuntimeUtility.InBackground(() =>
  195. {
  196. _action?.Invoke(this);
  197. });
  198. }
  199. /// <summary></summary>
  200. public void SyncInvoke(Action action)
  201. {
  202. if (action == null) return;
  203. if (_sync == null) action.Invoke();
  204. else _sync.Send(state => action.Invoke(), null);
  205. }
  206. /// <summary></summary>
  207. public void FormInvoke(Action action)
  208. {
  209. if (action == null) return;
  210. if (_form == null) return;
  211. if (_form.InvokeRequired)
  212. {
  213. _form.Invoke(action);
  214. return;
  215. }
  216. action.Invoke();
  217. }
  218. /// <summary></summary>
  219. public void FormBeginInvoke(Action action)
  220. {
  221. if (action == null) return;
  222. if (_form == null) return;
  223. _form.BeginInvoke(action);
  224. }
  225. /// <summary>显示对话框,并获取结果。</summary>
  226. /// <exception cref="ArgumentNullException" />
  227. public DialogResult ShowDialog(CommonDialog dialog)
  228. {
  229. if (_disposed) throw new ObjectDisposedException(ToString());
  230. if (dialog == null) throw new ArgumentNullException(nameof(dialog));
  231. if (_form.InvokeRequired)
  232. {
  233. var result = default(DialogResult);
  234. _form.Invoke(() =>
  235. {
  236. _form.Visible = true;
  237. result = dialog.ShowDialog();
  238. _form.Visible = false;
  239. });
  240. return result;
  241. }
  242. return dialog.ShowDialog();
  243. }
  244. /// <summary></summary>
  245. public void Exit()
  246. {
  247. if (!_disposed) Dispose();
  248. Application.Exit();
  249. }
  250. /// <summary></summary>
  251. public void Exit(Action customExit)
  252. {
  253. if (!_disposed) Dispose();
  254. customExit.Invoke();
  255. }
  256. #endregion
  257. #region run
  258. /// <summary>已启动的服务名称。</summary>
  259. public static string ServiceName { get; private set; }
  260. /// <summary>在当前线程运行托盘程序,并启动消息循环。</summary>
  261. /// <param name="action">启动托盘后执行的程序。</param>
  262. /// <exception cref="ArgumentNullException" />
  263. [STAThread]
  264. public static void Run(Action<Tray> action)
  265. {
  266. if (action == null) throw new ArgumentNullException(nameof(action));
  267. Control.CheckForIllegalCrossThreadCalls = false;
  268. Application.EnableVisualStyles();
  269. Application.SetCompatibleTextRenderingDefault(false);
  270. if (SynchronizationContext.Current == null)
  271. {
  272. var sc = AsyncOperationManager.SynchronizationContext;
  273. if (sc == null || sc.GetType() == typeof(SynchronizationContext))
  274. {
  275. new PermissionSet(PermissionState.Unrestricted).Assert();
  276. try { AsyncOperationManager.SynchronizationContext = new WindowsFormsSynchronizationContext(); }
  277. finally { CodeAccessPermission.RevertAssert(); }
  278. }
  279. }
  280. var instance = new Tray(action);
  281. // action.Invoke(instance);
  282. Application.Run();
  283. }
  284. /// <summary>启动服务。</summary>
  285. /// <param name="onStart">服务启动后执行的程序。</param>
  286. /// <param name="onStop">停止服务时执行的程序。</param>
  287. public static void Service(Action onStart, Action onStop = null)
  288. {
  289. var processName = Process.GetCurrentProcess().ProcessName;
  290. var service = new TrayService(processName, onStart, onStop);
  291. ServiceBase.Run(service);
  292. }
  293. class TrayService : ServiceBase
  294. {
  295. Action on_start;
  296. Action on_stop;
  297. public TrayService(string serviceName, Action onStart, Action onStop)
  298. {
  299. if (serviceName.IsEmpty()) throw new ArgumentNullException(nameof(serviceName));
  300. if (onStart == null) throw new ArgumentNullException(nameof(onStart));
  301. ServiceName = serviceName;
  302. on_start = onStart;
  303. on_stop = onStop;
  304. }
  305. protected override void OnStart(string[] args) => RuntimeUtility.InBackground(on_start);
  306. protected override void OnStop() => on_stop?.Invoke();
  307. }
  308. #endregion
  309. }
  310. }