编辑(2020 年 6 月 27 日):我创建了一个包,它具有在 Mac OS here 上显示通知的功能。它将使用 PyObjC 创建和显示通知。如果由于某种原因它不起作用,它将回退到带有osascript 的 AppleScript 通知。我做了一些测试,发现 PyObjC 通知在某些设备上有效,但在某些设备上无效。
答案:
我也一直在寻找这个答案,所以我想分享一下我的发现:
您会注意到的第一件事是函数notify() 定义了一个类,然后返回它的一个实例。您可能想知道为什么不能直接致电Notification.send(params)。我试过了,但是 PyObjC 出现错误,很遗憾我无法修复:
# Error
class Notification(NSObject):
objc.BadPrototypeError: Objective-C expects 1 arguments, Python argument has 2 arguments for <unbound selector send of Notification at 0x10e410180>
现在进入代码:
# vscode may show the error: "No name '...' in module 'Foundation'; you can ignore it"
from Foundation import NSUserNotification, NSUserNotificationCenter, NSObject, NSDate
from PyObjCTools import AppHelper
def notify(
title='Notification',
subtitle=None, text=None,
delay=0,
action_button_title=None,
action_button_callback=None,
other_button_title=None,
other_button_callback=None,
reply_placeholder=None,
reply_callback=None
):
class Notification(NSObject):
def send(self):
notif = NSUserNotification.alloc().init()
if title is not None:
notif.setTitle_(title)
if subtitle is not None:
notif.setSubtitle_(subtitle)
if text is not None:
notif.setInformativeText_(text)
# notification buttons (main action button and other button)
if action_button_title:
notif.setActionButtonTitle_(action_button_title)
notif.set_showsButtons_(True)
if other_button_title:
notif.setOtherButtonTitle_(other_button_title)
notif.set_showsButtons_(True)
# reply button
if reply_callback:
notif.setHasReplyButton_(True)
if reply_placeholder:
notif.setResponsePlaceholder_(reply_placeholder)
NSUserNotificationCenter.defaultUserNotificationCenter().setDelegate_(self)
# setting delivery date as current date + delay (in seconds)
notif.setDeliveryDate_(NSDate.dateWithTimeInterval_sinceDate_(delay, NSDate.date()))
# schedule the notification send
NSUserNotificationCenter.defaultUserNotificationCenter().scheduleNotification_(notif)
# on if any of the callbacks are provided, start the event loop (this will keep the program from stopping)
if action_button_callback or other_button_callback or reply_callback:
print('started')
AppHelper.runConsoleEventLoop()
def userNotificationCenter_didDeliverNotification_(self, center, notif):
print('delivered notification')
def userNotificationCenter_didActivateNotification_(self, center, notif):
print('did activate')
response = notif.response()
if notif.activationType() == 1:
# user clicked on the notification (not on a button)
# don't stop event loop because the other buttons can still be pressed
pass
elif notif.activationType() == 2:
# user clicked on the action button
action_button_callback()
AppHelper.stopEventLoop()
elif notif.activationType() == 3:
# user clicked on the reply button
reply_text = response.string()
reply_callback(reply_text)
AppHelper.stopEventLoop()
# create the new notification
new_notif = Notification.alloc().init()
# return notification
return new_notif
def main():
n = notify(
title='Notification',
delay=0,
action_button_title='Action',
action_button_callback=lambda: print('Action'),
# other_button_title='Other',
# other_button_callback=lambda: print('Other'),
reply_placeholder='Enter your reply please',
reply_callback=lambda reply: print('Replied: ', reply),
)
n.send()
if __name__ == '__main__':
main()
说明
notify() 函数接受很多参数(它们是不言自明的)。 delay 是通知将在几秒后出现。请注意,如果您设置的延迟时间比程序的执行时间长,则通知将在程序执行后发送。
您将看到按钮参数。共有三种按钮:
- 动作按钮:主导动作
- 其他按钮:辅助操作
- 回复按钮:打开文本字段并接受用户输入的按钮。这在 iMessage 等消息应用中很常见。
所有这些if 语句都在适当地设置按钮并且不言自明。例如,如果没有提供其他按钮的参数,则不会显示其他按钮。
您会注意到,如果有按钮,我们将启动控制台事件循环:
if action_button_callback or other_button_callback or reply_callback:
print('started')
AppHelper.runConsoleEventLoop()
这是 Python Objective-C 的一部分。这不是一个很好的解释,但它基本上让程序“运行”(我希望有人能给出更好的解释)。
基本上,如果你指定你想要一个按钮,程序将继续“开启”直到AppHelper.stopEventLoop()(稍后会详细介绍)。
现在有一些“挂钩”功能:
-
userNotificationCenter_didDeliverNotification_(self, notification_center, notification):发送通知时调用
-
userNotificationCenter_didActivateNotification_(self, notification_center, notification):当用户与通知交互(点击、点击动作按钮或回复)时调用(documentation)
当然还有更多,但不幸的是,我不认为通知被忽略或忽略是一个钩子。
使用userNotificationCenter_didActivateNotification_,我们可以定义一些回调:
def userNotificationCenter_didActivateNotification_(self, center, notif):
print('did activate')
response = notif.response()
if notif.activationType() == 1:
# user clicked on the notification (not on a button)
# don't stop event loop because the other buttons can still be pressed
pass
elif notif.activationType() == 2:
# user clicked on the action button
# action button callback
action_button_callback()
AppHelper.stopEventLoop()
elif notif.activationType() == 3:
# user clicked on the reply button
reply_text = response.string()
# reply button callback
reply_callback(reply_text)
AppHelper.stopEventLoop()
动作类型有不同的激活类型。回复操作中的文本也可以如图所示检索。
您还会注意到末尾的AppHelper.stopEventLoop()。这意味着“结束”程序的执行,因为通知已被用户处理。
现在让我们解决这个解决方案的所有问题。
问题
- 如果用户不与通知交互,程序将永远不会停止。通知将滑入通知中心,可能会或可能永远不会与之交互。正如我之前所说,没有通知被忽略或通知被驳回的钩子,所以我们不能在这种时候调用
AppHelper.stopEventLoop()。
- 因为
AppHelper.stopEventLoop()是在交互后运行的,所以无法通过回调发送多个通知,因为在第一个通知交互后程序将停止执行。
- 虽然我可以显示其他按钮(并给它文本),但我找不到给它一个回调的方法。这就是为什么我没有在上面的代码块中解决它。我可以给它文本,但它本质上是一个虚拟按钮,因为它不能做任何事情。
我还应该使用这个解决方案吗?
如果您想要带有回调的通知,您可能不应该这样做,因为我已经解决了这些问题。
如果您只想显示通知以提醒用户某事,可以。
其他解决方案
PYNC 是terminal-notifier 的包装。但是,两者都在 2018 年收到了最后一次提交。Alerter 似乎是终端通知程序的继任者,但没有 Python 包装器。
你也可以尝试运行applescript发送通知,但是不能设置回调,也不能改变图标。
我希望这个答案对你有所帮助。我还试图找出如何在 Mac OS 上可靠地发送带有回调的通知。我已经弄清楚如何发送通知,但问题是回调。