123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528 |
- {
- TrayIcon for FMX v0.5
- by gale 2025-02-13
- https://github.com/higale
- }
- unit FMXTrayIcon;
- interface
- uses
- {$IFDEF MSWINDOWS}
- Vcl.Graphics, WinAPI.Messages, WinAPI.Windows, WinAPI.ShellAPI, FMX.Platform.Win,
- System.Messaging, System.IOUtils,
- {$ELSE}
- Macapi.ObjectiveC, Macapi.CocoaTypes, Macapi.Foundation, Macapi.AppKit, FMX.Dialogs,
- Macapi.Helpers, Macapi.ObjcRuntime, System.TypInfo, FMX.Platform, FMX.Platform.Mac,
- Posix.Unistd, // 添加这个单元以使用 getpid
- {$ENDIF}
- System.hash, System.SysUtils, System.Types, System.Classes, FMX.Types, FMX.Forms,
- FMX.Menus;
- const
- {$IFDEF MSWINDOWS}
- WM_ICONTRAY = WM_USER + 1;
- WM_RUNONLYONCE = WM_USER + 2;
- {$ELSE}
- MACOS_TAG = 1;
- {$ENDIF}
- type
- TNotifyEventRef = reference to procedure(sender: TObject);
- TOnRunOnlyOnce = reference to procedure(sender: TObject; AParam: TArray<String>);
- TBalloonIconType = (None, Info, Warning, Error, User, BigWarning, BigError);
- {$IFDEF MACOS}
- IClickHandler = interface(NSObject)
- ['{B3B9C05D-8909-4419-BC8C-ACE5CFE6388D}']
- procedure click(sender: id); cdecl;
- procedure menuItemClick(sender: id); cdecl;
- end;
- TTrayIcon = class;
- TClickHandler = class(TOCLocal)
- private
- FOwner: TTrayIcon;
- public
- procedure click(sender: id); cdecl;
- procedure menuItemClick(sender: id); cdecl;
- function GetObjectiveCClass: PTypeInfo; override;
- end;
- {$ENDIF}
- TTrayIcon = class(TComponent)
- {$IFDEF MSWINDOWS}
- private
- class var FRunOnlyOnceTag: string;
- class var FHasOtherRunning: Boolean;
- class procedure CheckRunOnce;
- public
- class property HasOtherRunning: Boolean read FHasOtherRunning;
- {$ENDIF}
- private
- {$IFDEF MSWINDOWS}
- FTrayWnd: HWND;
- FTrayIconData: TNotifyIconData;
- FIcon: TIcon;
- FShowingPopup: Boolean;
- {$ELSE}
- FStatItem: NSStatusItem;
- FClickHandler: TClickHandler;
- {$ENDIF}
- FIconFile: string;
- FPopupMenu: TPopupMenu;
- FHint: string;
- FVisible: Boolean;
- FRunOnlyOnce: Boolean;
- FOnRunOnlyOnce: TOnRunOnlyOnce;
- FOnClick: TNotifyEventRef;
- FOnDblClick: TNotifyEventRef;
- function GetShowAppOnTaskbar: Boolean;
- procedure SetShowAppOnTaskbar(AValue: Boolean);
- procedure SetHint(Value: string);
- procedure SetIconFile(Value: string);
- procedure SetPopupMenu(Value: TPopupMenu);
- {$IFDEF MSWINDOWS}
- procedure TrayWndProc(var Message: WinAPI.Messages.TMessage);
- procedure FOnPopupForm(const sender: TObject; const M: TMessage);
- {$ELSE}
- {$ENDIF}
- procedure SetVisible(Value: Boolean);
- procedure SetRunOnlyOnce(Value: Boolean);
- public
- property IconFile: string read FIconFile write SetIconFile;
- property PopupMenu: TPopupMenu read FPopupMenu write SetPopupMenu;
- property Hint: string read FHint write SetHint;
- property Visible: Boolean read FVisible write SetVisible default False;
- property RunOnlyOnce: Boolean read FRunOnlyOnce write SetRunOnlyOnce default False;
- property ShowAppOnTaskbar: Boolean read GetShowAppOnTaskbar write SetShowAppOnTaskbar;
- property OnRunOnlyOnce: TOnRunOnlyOnce read FOnRunOnlyOnce write FOnRunOnlyOnce;
- property OnClick: TNotifyEventRef read FOnClick write FOnClick;
- property OnDblClick: TNotifyEventRef read FOnDblClick write FOnDblClick;
- public
- procedure ShowBalloonHint(Title, Text: string; BalloonIcon: TBalloonIconType);
- public
- constructor Create(AOwner: TComponent); override;
- destructor Destroy; override;
- end;
- implementation
- {$IFDEF MSWINDOWS}
- class procedure TTrayIcon.CheckRunOnce;
- var
- hRunOnce: HWND;
- strs: TStrings;
- strParmFile: string;
- begin
- FRunOnlyOnceTag := System.hash.THashSHA1.GetHashString(ParamStr(0).ToLower);
- FHasOtherRunning := False;
- hRunOnce := WinAPI.Windows.FindWindow('TPUtilWindow', PChar(TTrayIcon.FRunOnlyOnceTag));
- if IsWindow(hRunOnce) then
- begin
- strParmFile := TPath.GetTempPath + TTrayIcon.FRunOnlyOnceTag + '_run_only_once.txt';
- strs := TStringList.Create;
- Try
- for var i := 0 to ParamCount do
- strs.Add(ParamStr(i));
- strs.SaveToFile(strParmFile);
- Finally
- strs.Free;
- End;
- PostMessage(hRunOnce, WM_RUNONLYONCE, 0, 0);
- TTrayIcon.FHasOtherRunning := true;
- Application.Terminate;
- Application.Terminated := true;
- end;
- end;
- {$ENDIF}
- constructor TTrayIcon.Create(AOwner: TComponent);
- {$IFDEF MACOS}
- var
- LStatBar: NSStatusBar;
- LImg: NSImage;
- {$ENDIF}
- begin
- inherited Create(AOwner);
- FRunOnlyOnce := False;
- {$IFDEF MSWINDOWS}
- FShowingPopup := False;
- FIcon := nil;
- TMessageManager.DefaultManager.SubscribeToMessage(TFormBeforeShownMessage, FOnPopupForm);
- FTrayWnd := AllocateHWnd(TrayWndProc);
- FTrayIconData.cbSize := System.SizeOf(FTrayIconData);
- FTrayIconData.Wnd := FTrayWnd;
- FTrayIconData.uID := 1;
- FTrayIconData.uFlags := NIF_MESSAGE or NIF_ICON or NIF_TIP;
- FTrayIconData.uCallbackMessage := WM_ICONTRAY;
- FTrayIconData.hIcon := GetClassLong(FmxHandleToHWND((AOwner as TForm).Handle), GCL_HICONSM);
- {$ELSE}
- FClickHandler := TClickHandler.Create;
- FClickHandler.FOwner := Self;
- LStatBar := TNSStatusBar.Wrap(TNSStatusBar.OCClass.systemStatusBar);
- FStatItem := LStatBar.statusItemWithLength(NSVariableStatusItemLength);
- FIconFile := NSStrToStr(TNSBundle.Wrap(TNSBundle.OCClass.mainBundle).resourcePath.stringByAppendingString(StrToNSStr('/' + ExtractFileName(ParamStr(0)) + '.icns')));
- LImg := TNSImage.Wrap(TNSImage.Alloc.initWithContentsOfFile(StrToNSStr(FIconFile)));
- LImg.setSize(NSSize(TSizeF.Create(16, 16)));
- FStatItem.setImage(LImg);
- LImg.release;
- FStatItem.setTarget(FClickHandler.GetObjectID);
- FStatItem.setAction(sel_getUid(PAnsiChar('click:')));
- {$ENDIF}
- end;
- destructor TTrayIcon.Destroy;
- begin
- {$IFDEF MSWINDOWS}
- TMessageManager.DefaultManager.Unsubscribe(TFormBeforeShownMessage, FOnPopupForm);
- if FVisible then
- begin
- Shell_NotifyIcon(NIM_DELETE, @FTrayIconData);
- end;
- FIcon.Free;
- {$ELSE}
- FClickHandler.Free;
- {$ENDIF}
- inherited;
- end;
- procedure TTrayIcon.ShowBalloonHint(Title, Text: string; BalloonIcon: TBalloonIconType);
- {$IFDEF MSWINDOWS}
- begin
- with FTrayIconData do
- begin
- StrLCopy(szInfo, PChar(Text), High(szInfo));
- StrLCopy(szInfoTitle, PChar(Title), High(szInfoTitle));
- dwInfoFlags := Ord(BalloonIcon);
- uFlags := NIF_INFO;
- end;
- Shell_NotifyIcon(NIM_MODIFY, @FTrayIconData);
- end;
- {$ELSE}
- var
- Notification: NSUserNotification;
- Center: NSUserNotificationCenter;
- begin
- // 使用 NSUserNotification 模拟气泡提示
- Center := TNSUserNotificationCenter.Wrap(TNSUserNotificationCenter.OCClass.defaultUserNotificationCenter);
- Notification := TNSUserNotification.Create;
- Notification.setTitle(StrToNSStr(Title));
- Notification.setInformativeText(StrToNSStr(Text));
- // 发送通知
- Center.deliverNotification(Notification);
- end;
- {$ENDIF}
- function TTrayIcon.GetShowAppOnTaskbar: Boolean;
- begin
- {$IFDEF MSWINDOWS}
- Result := (GetWindowLong(ApplicationHWND, GWL_EXSTYLE) and WS_EX_TOOLWINDOW) = 0;
- {$ELSE}
- Result := TNSApplication.Wrap(TNSApplication.OCClass.sharedApplication).activationPolicy = NSApplicationActivationPolicyRegular;
- {$ENDIF}
- end;
- procedure TTrayIcon.SetShowAppOnTaskbar(AValue: Boolean);
- begin
- {$IFDEF MSWINDOWS}
- if AValue then
- begin
- SetWindowLong(ApplicationHWND, GWL_EXSTYLE, GetWindowLong(ApplicationHWND, GWL_EXSTYLE) and (not WS_EX_APPWINDOW) and not WS_EX_TOOLWINDOW);
- end
- else
- begin
- SetWindowLong(ApplicationHWND, GWL_EXSTYLE, GetWindowLong(ApplicationHWND, GWL_EXSTYLE) and (not WS_EX_APPWINDOW) or WS_EX_TOOLWINDOW);
- end;
- {$ELSE}
- if AValue then
- TNSApplication.Wrap(TNSApplication.OCClass.sharedApplication).setActivationPolicy(NSApplicationActivationPolicyRegular)
- else
- TNSApplication.Wrap(TNSApplication.OCClass.sharedApplication).setActivationPolicy(NSApplicationActivationPolicyAccessory);
- {$ENDIF}
- end;
- procedure TTrayIcon.SetHint(Value: string);
- begin
- {$IFDEF MSWINDOWS}
- if Value <> FHint then
- begin
- FHint := Value;
- StrLCopy(FTrayIconData.szTip, PChar(FHint), High(FTrayIconData.szTip));
- end;
- Shell_NotifyIcon(NIM_MODIFY, @FTrayIconData);
- {$ELSE}
- FStatItem.setToolTip(StrToNSStr(Value));
- {$ENDIF}
- end;
- procedure TTrayIcon.SetIconFile(Value: string);
- begin
- if FIconFile = Value then
- Exit;
- {$IFDEF MSWINDOWS}
- if Value = FIconFile then
- Exit;
- FreeAndNil(FIcon);
- if Value = '' then
- begin
- FTrayIconData.hIcon := GetClassLong(FmxHandleToHWND((Owner as TForm).Handle), GCL_HICONSM);
- end
- else
- begin
- FIcon := TIcon.Create;
- FIcon.LoadFromFile(Value);
- FTrayIconData.hIcon := FIcon.Handle;
- end;
- if FVisible then
- begin
- FTrayIconData.uFlags := NIF_ICON;
- Shell_NotifyIcon(NIM_MODIFY, @FTrayIconData);
- end;
- {$ELSE}
- // 设置图标
- FIconFile := Value;
- var
- LImg := TNSImage.Wrap(TNSImage.Alloc.initWithContentsOfFile(StrToNSStr(Value)));
- LImg.setSize(NSSize(TSizeF.Create(16, 16)));
- FStatItem.setImage(LImg);
- LImg.release;
- {$ENDIF}
- end;
- procedure TTrayIcon.SetPopupMenu(Value: TPopupMenu);
- {$IFDEF MACOS}
- var
- LNSMenu: NSMenu;
- NSContItem: NSMenuItem;
- {$ENDIF}
- begin
- if FPopupMenu = Value then
- Exit;
- FPopupMenu := Value;
- {$IFDEF MACOS}
- FStatItem.setMenu(nil);
- LNSMenu := TNSMenu.Create;
- FStatItem.setMenu(LNSMenu);
- for var i := 0 to Value.ItemsCount - 1 do
- begin
- if Value.Items[i].Text = '-' then // 添加分隔线
- begin
- FStatItem.menu.addItem(TNSMenuItem.Wrap(TNSMenuItem.OCClass.separatorItem));
- end
- else
- begin
- NSContItem := TNSMenuItem.Create;
- NSContItem.initWithTitle(StrToNSStr(Value.Items[i].Text), sel_getUid(PAnsiChar('menuItemClick:')), StrToNSStr(''));
- NSContItem.setTag(i);
- NSContItem.setTarget(FClickHandler.GetObjectID);
- FStatItem.menu.addItem(NSContItem);
- NSContItem.release;
- end;
- end;
- {$ENDIF}
- end;
- procedure TTrayIcon.SetVisible(Value: Boolean);
- begin
- {$IFDEF MSWINDOWS}
- if Value <> FVisible then
- begin
- if Value then
- begin
- FVisible := Shell_NotifyIcon(NIM_ADD, @FTrayIconData);
- end
- else
- begin
- Shell_NotifyIcon(NIM_DELETE, @FTrayIconData);
- FVisible := False;
- end;
- end;
- {$ELSE}
- {$ENDIF}
- end;
- procedure TTrayIcon.SetRunOnlyOnce(Value: Boolean);
- {$IFDEF MACOS}
- {var
- Workspace: NSWorkspace;
- RunningApps: NSArray;
- i: Integer;
- CurrentApp: NSRunningApplication;
- BundleID: NSString;
- CurrentPID: Integer;}
- {$ENDIF}
- begin
- if FRunOnlyOnce = Value then
- Exit;
- FRunOnlyOnce := Value;
- {$IFDEF MSWINDOWS}
- if FRunOnlyOnce then
- begin
- WinAPI.Windows.SetWindowText(FTrayWnd, FRunOnlyOnceTag);
- end
- else
- begin
- WinAPI.Windows.SetWindowText(FTrayWnd, '');
- end;
- {$ELSE}
- {if FRunOnlyOnce then
- begin
- Workspace := TNSWorkspace.Wrap(TNSWorkspace.OCClass.sharedWorkspace);
- RunningApps := Workspace.runningApplications;
- BundleID := TNSBundle.Wrap(TNSBundle.OCClass.mainBundle).bundleIdentifier;
- CurrentPID := getpid; // 获取当前进程的 PID
- for i := 0 to RunningApps.count - 1 do
- begin
- CurrentApp := TNSRunningApplication.Wrap(RunningApps.objectAtIndex(i));
- if (CurrentApp.bundleIdentifier.isEqualToString(BundleID)) and
- (CurrentApp.processIdentifier <> CurrentPID) then
- begin
- ShowMessage('Another instance is already running.');
- Application.Terminate;
- Exit;
- end;
- end;
- end;}
- {$ENDIF}
- end;
- {$IFDEF MSWINDOWS}
- procedure TTrayIcon.TrayWndProc(var Message: WinAPI.Messages.TMessage);
- var
- strParmFile: string;
- strs: TStrings;
- begin
- if Message.MSG = WM_ICONTRAY then
- begin
- case Message.LParam of
- WM_LBUTTONUP:
- begin
- if Assigned(FOnClick) then
- begin
- FOnClick(Self)
- end
- else if Assigned(FPopupMenu) then
- begin
- SetForegroundWindow(ApplicationHWND);
- FShowingPopup := true;
- try
- FPopupMenu.Popup(Screen.MousePos.X, Screen.MousePos.Y);
- finally
- FShowingPopup := False;
- end;
- end;
- end;
- WM_LBUTTONDBLCLK:
- if Assigned(FOnDblClick) then
- FOnDblClick(Self);
- WM_RBUTTONDOWN:
- begin
- if Assigned(FPopupMenu) then
- begin
- SetForegroundWindow(ApplicationHWND);
- FShowingPopup := true;
- try
- FPopupMenu.Popup(Screen.MousePos.X, Screen.MousePos.Y);
- finally
- FShowingPopup := False;
- end;
- end;
- end;
- end;
- end
- else if Message.MSG = WM_RUNONLYONCE then
- begin
- if Assigned(FOnRunOnlyOnce) then
- begin
- strParmFile := TPath.GetTempPath + TTrayIcon.FRunOnlyOnceTag + '_run_only_once.txt';
- if FileExists(strParmFile) then
- begin
- strs := TStringList.Create;
- try
- strs.LoadFromFile(strParmFile);
- try
- DeleteFile(strParmFile);
- except
- end;
- FOnRunOnlyOnce(Self, strs.ToStringArray);
- finally
- strs.Free;
- end;
- end
- else
- begin
- FOnRunOnlyOnce(Self, []);
- end;
- end;
- end
- else
- begin
- Message.Result := DefWindowProc(FTrayWnd, Message.MSG, Message.WParam, Message.LParam);
- end;
- end;
- procedure TTrayIcon.FOnPopupForm(const sender: TObject; const M: TMessage);
- var
- MSG: TFormBeforeShownMessage absolute M;
- begin
- if FShowingPopup and (MSG.Value is TCustomPopupForm) then
- begin
- SetWindowPos(FormToHWND(MSG.Value), HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE or SWP_NOMOVE or SWP_NOACTIVATE);
- end;
- end;
- {$ELSE}
- procedure TClickHandler.click(sender: id);
- cdecl;
- begin
- if Assigned(FOwner.FOnClick) then
- begin
- FOwner.FOnClick(FOwner);
- end;
- end;
- procedure TClickHandler.menuItemClick(sender: id); cdecl;
- var
- MenuItem: NSMenuItem;
- begin
- MenuItem := TNSMenuItem.Wrap(sender);
- if Assigned(FOwner.FPopupMenu.Items[MenuItem.tag].OnClick) then
- begin
- FOwner.FPopupMenu.Items[MenuItem.tag].OnClick(FOwner.FPopupMenu.Items[MenuItem.tag]);
- end;
- end;
- function TClickHandler.GetObjectiveCClass: PTypeInfo;
- begin
- Result := TypeInfo(IClickHandler);
- end;
- {$ENDIF}
- initialization
- {$IFDEF MSWINDOWS}
- TTrayIcon.CheckRunOnce;
- {$ENDIF}
- end.
|