FMXTrayIcon.pas 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. {
  2. TrayIcon for FMX v0.5
  3. by gale 2025-02-13
  4. https://github.com/higale
  5. }
  6. unit FMXTrayIcon;
  7. interface
  8. uses
  9. {$IFDEF MSWINDOWS}
  10. Vcl.Graphics, WinAPI.Messages, WinAPI.Windows, WinAPI.ShellAPI, FMX.Platform.Win,
  11. System.Messaging, System.IOUtils,
  12. {$ELSE}
  13. Macapi.ObjectiveC, Macapi.CocoaTypes, Macapi.Foundation, Macapi.AppKit, FMX.Dialogs,
  14. Macapi.Helpers, Macapi.ObjcRuntime, System.TypInfo, FMX.Platform, FMX.Platform.Mac,
  15. Posix.Unistd, // 添加这个单元以使用 getpid
  16. {$ENDIF}
  17. System.hash, System.SysUtils, System.Types, System.Classes, FMX.Types, FMX.Forms,
  18. FMX.Menus;
  19. const
  20. {$IFDEF MSWINDOWS}
  21. WM_ICONTRAY = WM_USER + 1;
  22. WM_RUNONLYONCE = WM_USER + 2;
  23. {$ELSE}
  24. MACOS_TAG = 1;
  25. {$ENDIF}
  26. type
  27. TNotifyEventRef = reference to procedure(sender: TObject);
  28. TOnRunOnlyOnce = reference to procedure(sender: TObject; AParam: TArray<String>);
  29. TBalloonIconType = (None, Info, Warning, Error, User, BigWarning, BigError);
  30. {$IFDEF MACOS}
  31. IClickHandler = interface(NSObject)
  32. ['{B3B9C05D-8909-4419-BC8C-ACE5CFE6388D}']
  33. procedure click(sender: id); cdecl;
  34. procedure menuItemClick(sender: id); cdecl;
  35. end;
  36. TTrayIcon = class;
  37. TClickHandler = class(TOCLocal)
  38. private
  39. FOwner: TTrayIcon;
  40. public
  41. procedure click(sender: id); cdecl;
  42. procedure menuItemClick(sender: id); cdecl;
  43. function GetObjectiveCClass: PTypeInfo; override;
  44. end;
  45. {$ENDIF}
  46. TTrayIcon = class(TComponent)
  47. {$IFDEF MSWINDOWS}
  48. private
  49. class var FRunOnlyOnceTag: string;
  50. class var FHasOtherRunning: Boolean;
  51. class procedure CheckRunOnce;
  52. public
  53. class property HasOtherRunning: Boolean read FHasOtherRunning;
  54. {$ENDIF}
  55. private
  56. {$IFDEF MSWINDOWS}
  57. FTrayWnd: HWND;
  58. FTrayIconData: TNotifyIconData;
  59. FIcon: TIcon;
  60. FShowingPopup: Boolean;
  61. {$ELSE}
  62. FStatItem: NSStatusItem;
  63. FClickHandler: TClickHandler;
  64. {$ENDIF}
  65. FIconFile: string;
  66. FPopupMenu: TPopupMenu;
  67. FHint: string;
  68. FVisible: Boolean;
  69. FRunOnlyOnce: Boolean;
  70. FOnRunOnlyOnce: TOnRunOnlyOnce;
  71. FOnClick: TNotifyEventRef;
  72. FOnDblClick: TNotifyEventRef;
  73. function GetShowAppOnTaskbar: Boolean;
  74. procedure SetShowAppOnTaskbar(AValue: Boolean);
  75. procedure SetHint(Value: string);
  76. procedure SetIconFile(Value: string);
  77. procedure SetPopupMenu(Value: TPopupMenu);
  78. {$IFDEF MSWINDOWS}
  79. procedure TrayWndProc(var Message: WinAPI.Messages.TMessage);
  80. procedure FOnPopupForm(const sender: TObject; const M: TMessage);
  81. {$ELSE}
  82. {$ENDIF}
  83. procedure SetVisible(Value: Boolean);
  84. procedure SetRunOnlyOnce(Value: Boolean);
  85. public
  86. property IconFile: string read FIconFile write SetIconFile;
  87. property PopupMenu: TPopupMenu read FPopupMenu write SetPopupMenu;
  88. property Hint: string read FHint write SetHint;
  89. property Visible: Boolean read FVisible write SetVisible default False;
  90. property RunOnlyOnce: Boolean read FRunOnlyOnce write SetRunOnlyOnce default False;
  91. property ShowAppOnTaskbar: Boolean read GetShowAppOnTaskbar write SetShowAppOnTaskbar;
  92. property OnRunOnlyOnce: TOnRunOnlyOnce read FOnRunOnlyOnce write FOnRunOnlyOnce;
  93. property OnClick: TNotifyEventRef read FOnClick write FOnClick;
  94. property OnDblClick: TNotifyEventRef read FOnDblClick write FOnDblClick;
  95. public
  96. procedure ShowBalloonHint(Title, Text: string; BalloonIcon: TBalloonIconType);
  97. public
  98. constructor Create(AOwner: TComponent); override;
  99. destructor Destroy; override;
  100. end;
  101. implementation
  102. {$IFDEF MSWINDOWS}
  103. class procedure TTrayIcon.CheckRunOnce;
  104. var
  105. hRunOnce: HWND;
  106. strs: TStrings;
  107. strParmFile: string;
  108. begin
  109. FRunOnlyOnceTag := System.hash.THashSHA1.GetHashString(ParamStr(0).ToLower);
  110. FHasOtherRunning := False;
  111. hRunOnce := WinAPI.Windows.FindWindow('TPUtilWindow', PChar(TTrayIcon.FRunOnlyOnceTag));
  112. if IsWindow(hRunOnce) then
  113. begin
  114. strParmFile := TPath.GetTempPath + TTrayIcon.FRunOnlyOnceTag + '_run_only_once.txt';
  115. strs := TStringList.Create;
  116. Try
  117. for var i := 0 to ParamCount do
  118. strs.Add(ParamStr(i));
  119. strs.SaveToFile(strParmFile);
  120. Finally
  121. strs.Free;
  122. End;
  123. PostMessage(hRunOnce, WM_RUNONLYONCE, 0, 0);
  124. TTrayIcon.FHasOtherRunning := true;
  125. Application.Terminate;
  126. Application.Terminated := true;
  127. end;
  128. end;
  129. {$ENDIF}
  130. constructor TTrayIcon.Create(AOwner: TComponent);
  131. {$IFDEF MACOS}
  132. var
  133. LStatBar: NSStatusBar;
  134. LImg: NSImage;
  135. {$ENDIF}
  136. begin
  137. inherited Create(AOwner);
  138. FRunOnlyOnce := False;
  139. {$IFDEF MSWINDOWS}
  140. FShowingPopup := False;
  141. FIcon := nil;
  142. TMessageManager.DefaultManager.SubscribeToMessage(TFormBeforeShownMessage, FOnPopupForm);
  143. FTrayWnd := AllocateHWnd(TrayWndProc);
  144. FTrayIconData.cbSize := System.SizeOf(FTrayIconData);
  145. FTrayIconData.Wnd := FTrayWnd;
  146. FTrayIconData.uID := 1;
  147. FTrayIconData.uFlags := NIF_MESSAGE or NIF_ICON or NIF_TIP;
  148. FTrayIconData.uCallbackMessage := WM_ICONTRAY;
  149. FTrayIconData.hIcon := GetClassLong(FmxHandleToHWND((AOwner as TForm).Handle), GCL_HICONSM);
  150. {$ELSE}
  151. FClickHandler := TClickHandler.Create;
  152. FClickHandler.FOwner := Self;
  153. LStatBar := TNSStatusBar.Wrap(TNSStatusBar.OCClass.systemStatusBar);
  154. FStatItem := LStatBar.statusItemWithLength(NSVariableStatusItemLength);
  155. FIconFile := NSStrToStr(TNSBundle.Wrap(TNSBundle.OCClass.mainBundle).resourcePath.stringByAppendingString(StrToNSStr('/' + ExtractFileName(ParamStr(0)) + '.icns')));
  156. LImg := TNSImage.Wrap(TNSImage.Alloc.initWithContentsOfFile(StrToNSStr(FIconFile)));
  157. LImg.setSize(NSSize(TSizeF.Create(16, 16)));
  158. FStatItem.setImage(LImg);
  159. LImg.release;
  160. FStatItem.setTarget(FClickHandler.GetObjectID);
  161. FStatItem.setAction(sel_getUid(PAnsiChar('click:')));
  162. {$ENDIF}
  163. end;
  164. destructor TTrayIcon.Destroy;
  165. begin
  166. {$IFDEF MSWINDOWS}
  167. TMessageManager.DefaultManager.Unsubscribe(TFormBeforeShownMessage, FOnPopupForm);
  168. if FVisible then
  169. begin
  170. Shell_NotifyIcon(NIM_DELETE, @FTrayIconData);
  171. end;
  172. FIcon.Free;
  173. {$ELSE}
  174. FClickHandler.Free;
  175. {$ENDIF}
  176. inherited;
  177. end;
  178. procedure TTrayIcon.ShowBalloonHint(Title, Text: string; BalloonIcon: TBalloonIconType);
  179. {$IFDEF MSWINDOWS}
  180. begin
  181. with FTrayIconData do
  182. begin
  183. StrLCopy(szInfo, PChar(Text), High(szInfo));
  184. StrLCopy(szInfoTitle, PChar(Title), High(szInfoTitle));
  185. dwInfoFlags := Ord(BalloonIcon);
  186. uFlags := NIF_INFO;
  187. end;
  188. Shell_NotifyIcon(NIM_MODIFY, @FTrayIconData);
  189. end;
  190. {$ELSE}
  191. var
  192. Notification: NSUserNotification;
  193. Center: NSUserNotificationCenter;
  194. begin
  195. // 使用 NSUserNotification 模拟气泡提示
  196. Center := TNSUserNotificationCenter.Wrap(TNSUserNotificationCenter.OCClass.defaultUserNotificationCenter);
  197. Notification := TNSUserNotification.Create;
  198. Notification.setTitle(StrToNSStr(Title));
  199. Notification.setInformativeText(StrToNSStr(Text));
  200. // 发送通知
  201. Center.deliverNotification(Notification);
  202. end;
  203. {$ENDIF}
  204. function TTrayIcon.GetShowAppOnTaskbar: Boolean;
  205. begin
  206. {$IFDEF MSWINDOWS}
  207. Result := (GetWindowLong(ApplicationHWND, GWL_EXSTYLE) and WS_EX_TOOLWINDOW) = 0;
  208. {$ELSE}
  209. Result := TNSApplication.Wrap(TNSApplication.OCClass.sharedApplication).activationPolicy = NSApplicationActivationPolicyRegular;
  210. {$ENDIF}
  211. end;
  212. procedure TTrayIcon.SetShowAppOnTaskbar(AValue: Boolean);
  213. begin
  214. {$IFDEF MSWINDOWS}
  215. if AValue then
  216. begin
  217. SetWindowLong(ApplicationHWND, GWL_EXSTYLE, GetWindowLong(ApplicationHWND, GWL_EXSTYLE) and (not WS_EX_APPWINDOW) and not WS_EX_TOOLWINDOW);
  218. end
  219. else
  220. begin
  221. SetWindowLong(ApplicationHWND, GWL_EXSTYLE, GetWindowLong(ApplicationHWND, GWL_EXSTYLE) and (not WS_EX_APPWINDOW) or WS_EX_TOOLWINDOW);
  222. end;
  223. {$ELSE}
  224. if AValue then
  225. TNSApplication.Wrap(TNSApplication.OCClass.sharedApplication).setActivationPolicy(NSApplicationActivationPolicyRegular)
  226. else
  227. TNSApplication.Wrap(TNSApplication.OCClass.sharedApplication).setActivationPolicy(NSApplicationActivationPolicyAccessory);
  228. {$ENDIF}
  229. end;
  230. procedure TTrayIcon.SetHint(Value: string);
  231. begin
  232. {$IFDEF MSWINDOWS}
  233. if Value <> FHint then
  234. begin
  235. FHint := Value;
  236. StrLCopy(FTrayIconData.szTip, PChar(FHint), High(FTrayIconData.szTip));
  237. end;
  238. Shell_NotifyIcon(NIM_MODIFY, @FTrayIconData);
  239. {$ELSE}
  240. FStatItem.setToolTip(StrToNSStr(Value));
  241. {$ENDIF}
  242. end;
  243. procedure TTrayIcon.SetIconFile(Value: string);
  244. begin
  245. if FIconFile = Value then
  246. Exit;
  247. {$IFDEF MSWINDOWS}
  248. if Value = FIconFile then
  249. Exit;
  250. FreeAndNil(FIcon);
  251. if Value = '' then
  252. begin
  253. FTrayIconData.hIcon := GetClassLong(FmxHandleToHWND((Owner as TForm).Handle), GCL_HICONSM);
  254. end
  255. else
  256. begin
  257. FIcon := TIcon.Create;
  258. FIcon.LoadFromFile(Value);
  259. FTrayIconData.hIcon := FIcon.Handle;
  260. end;
  261. if FVisible then
  262. begin
  263. FTrayIconData.uFlags := NIF_ICON;
  264. Shell_NotifyIcon(NIM_MODIFY, @FTrayIconData);
  265. end;
  266. {$ELSE}
  267. // 设置图标
  268. FIconFile := Value;
  269. var
  270. LImg := TNSImage.Wrap(TNSImage.Alloc.initWithContentsOfFile(StrToNSStr(Value)));
  271. LImg.setSize(NSSize(TSizeF.Create(16, 16)));
  272. FStatItem.setImage(LImg);
  273. LImg.release;
  274. {$ENDIF}
  275. end;
  276. procedure TTrayIcon.SetPopupMenu(Value: TPopupMenu);
  277. {$IFDEF MACOS}
  278. var
  279. LNSMenu: NSMenu;
  280. NSContItem: NSMenuItem;
  281. {$ENDIF}
  282. begin
  283. if FPopupMenu = Value then
  284. Exit;
  285. FPopupMenu := Value;
  286. {$IFDEF MACOS}
  287. FStatItem.setMenu(nil);
  288. LNSMenu := TNSMenu.Create;
  289. FStatItem.setMenu(LNSMenu);
  290. for var i := 0 to Value.ItemsCount - 1 do
  291. begin
  292. if Value.Items[i].Text = '-' then // 添加分隔线
  293. begin
  294. FStatItem.menu.addItem(TNSMenuItem.Wrap(TNSMenuItem.OCClass.separatorItem));
  295. end
  296. else
  297. begin
  298. NSContItem := TNSMenuItem.Create;
  299. NSContItem.initWithTitle(StrToNSStr(Value.Items[i].Text), sel_getUid(PAnsiChar('menuItemClick:')), StrToNSStr(''));
  300. NSContItem.setTag(i);
  301. NSContItem.setTarget(FClickHandler.GetObjectID);
  302. FStatItem.menu.addItem(NSContItem);
  303. NSContItem.release;
  304. end;
  305. end;
  306. {$ENDIF}
  307. end;
  308. procedure TTrayIcon.SetVisible(Value: Boolean);
  309. begin
  310. {$IFDEF MSWINDOWS}
  311. if Value <> FVisible then
  312. begin
  313. if Value then
  314. begin
  315. FVisible := Shell_NotifyIcon(NIM_ADD, @FTrayIconData);
  316. end
  317. else
  318. begin
  319. Shell_NotifyIcon(NIM_DELETE, @FTrayIconData);
  320. FVisible := False;
  321. end;
  322. end;
  323. {$ELSE}
  324. {$ENDIF}
  325. end;
  326. procedure TTrayIcon.SetRunOnlyOnce(Value: Boolean);
  327. {$IFDEF MACOS}
  328. {var
  329. Workspace: NSWorkspace;
  330. RunningApps: NSArray;
  331. i: Integer;
  332. CurrentApp: NSRunningApplication;
  333. BundleID: NSString;
  334. CurrentPID: Integer;}
  335. {$ENDIF}
  336. begin
  337. if FRunOnlyOnce = Value then
  338. Exit;
  339. FRunOnlyOnce := Value;
  340. {$IFDEF MSWINDOWS}
  341. if FRunOnlyOnce then
  342. begin
  343. WinAPI.Windows.SetWindowText(FTrayWnd, FRunOnlyOnceTag);
  344. end
  345. else
  346. begin
  347. WinAPI.Windows.SetWindowText(FTrayWnd, '');
  348. end;
  349. {$ELSE}
  350. {if FRunOnlyOnce then
  351. begin
  352. Workspace := TNSWorkspace.Wrap(TNSWorkspace.OCClass.sharedWorkspace);
  353. RunningApps := Workspace.runningApplications;
  354. BundleID := TNSBundle.Wrap(TNSBundle.OCClass.mainBundle).bundleIdentifier;
  355. CurrentPID := getpid; // 获取当前进程的 PID
  356. for i := 0 to RunningApps.count - 1 do
  357. begin
  358. CurrentApp := TNSRunningApplication.Wrap(RunningApps.objectAtIndex(i));
  359. if (CurrentApp.bundleIdentifier.isEqualToString(BundleID)) and
  360. (CurrentApp.processIdentifier <> CurrentPID) then
  361. begin
  362. ShowMessage('Another instance is already running.');
  363. Application.Terminate;
  364. Exit;
  365. end;
  366. end;
  367. end;}
  368. {$ENDIF}
  369. end;
  370. {$IFDEF MSWINDOWS}
  371. procedure TTrayIcon.TrayWndProc(var Message: WinAPI.Messages.TMessage);
  372. var
  373. strParmFile: string;
  374. strs: TStrings;
  375. begin
  376. if Message.MSG = WM_ICONTRAY then
  377. begin
  378. case Message.LParam of
  379. WM_LBUTTONUP:
  380. begin
  381. if Assigned(FOnClick) then
  382. begin
  383. FOnClick(Self)
  384. end
  385. else if Assigned(FPopupMenu) then
  386. begin
  387. SetForegroundWindow(ApplicationHWND);
  388. FShowingPopup := true;
  389. try
  390. FPopupMenu.Popup(Screen.MousePos.X, Screen.MousePos.Y);
  391. finally
  392. FShowingPopup := False;
  393. end;
  394. end;
  395. end;
  396. WM_LBUTTONDBLCLK:
  397. if Assigned(FOnDblClick) then
  398. FOnDblClick(Self);
  399. WM_RBUTTONDOWN:
  400. begin
  401. if Assigned(FPopupMenu) then
  402. begin
  403. SetForegroundWindow(ApplicationHWND);
  404. FShowingPopup := true;
  405. try
  406. FPopupMenu.Popup(Screen.MousePos.X, Screen.MousePos.Y);
  407. finally
  408. FShowingPopup := False;
  409. end;
  410. end;
  411. end;
  412. end;
  413. end
  414. else if Message.MSG = WM_RUNONLYONCE then
  415. begin
  416. if Assigned(FOnRunOnlyOnce) then
  417. begin
  418. strParmFile := TPath.GetTempPath + TTrayIcon.FRunOnlyOnceTag + '_run_only_once.txt';
  419. if FileExists(strParmFile) then
  420. begin
  421. strs := TStringList.Create;
  422. try
  423. strs.LoadFromFile(strParmFile);
  424. try
  425. DeleteFile(strParmFile);
  426. except
  427. end;
  428. FOnRunOnlyOnce(Self, strs.ToStringArray);
  429. finally
  430. strs.Free;
  431. end;
  432. end
  433. else
  434. begin
  435. FOnRunOnlyOnce(Self, []);
  436. end;
  437. end;
  438. end
  439. else
  440. begin
  441. Message.Result := DefWindowProc(FTrayWnd, Message.MSG, Message.WParam, Message.LParam);
  442. end;
  443. end;
  444. procedure TTrayIcon.FOnPopupForm(const sender: TObject; const M: TMessage);
  445. var
  446. MSG: TFormBeforeShownMessage absolute M;
  447. begin
  448. if FShowingPopup and (MSG.Value is TCustomPopupForm) then
  449. begin
  450. SetWindowPos(FormToHWND(MSG.Value), HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE or SWP_NOMOVE or SWP_NOACTIVATE);
  451. end;
  452. end;
  453. {$ELSE}
  454. procedure TClickHandler.click(sender: id);
  455. cdecl;
  456. begin
  457. if Assigned(FOwner.FOnClick) then
  458. begin
  459. FOwner.FOnClick(FOwner);
  460. end;
  461. end;
  462. procedure TClickHandler.menuItemClick(sender: id); cdecl;
  463. var
  464. MenuItem: NSMenuItem;
  465. begin
  466. MenuItem := TNSMenuItem.Wrap(sender);
  467. if Assigned(FOwner.FPopupMenu.Items[MenuItem.tag].OnClick) then
  468. begin
  469. FOwner.FPopupMenu.Items[MenuItem.tag].OnClick(FOwner.FPopupMenu.Items[MenuItem.tag]);
  470. end;
  471. end;
  472. function TClickHandler.GetObjectiveCClass: PTypeInfo;
  473. begin
  474. Result := TypeInfo(IClickHandler);
  475. end;
  476. {$ENDIF}
  477. initialization
  478. {$IFDEF MSWINDOWS}
  479. TTrayIcon.CheckRunOnce;
  480. {$ENDIF}
  481. end.