pystray 是一个系统托盘库,允许应用程序在后台运行并在系统托盘处添加一个图标。pystray 特别合我的需求——我要做一个轻量的定时任务程序,能方便配置不同的定时任务,但我不想写复杂的 GUI,而使用托盘的话就能够允许使用最少的 GUI 去实现功能。不过这里先不提它。
pystray 会使用主线程去同步执行关于 GUI 的逻辑,而我们如果要在背景里干其他事情,则需要其他线程去进行工作。
pystray 似乎不直接支持异步,但我可以另外起一个异步线程然后从主线程提交任务给它以避免任何形式的阻塞。
飞速过一下文档
pystray 的术语用的很奇怪——icon 指系统托盘上的那个图标,也指“MainApplication”。所以它的 Hello,World 这么写(icon.run
很 funny):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import pystray
from PIL import Image, ImageDraw
def create_image(width, height, color1, color2): image = Image.new('RGB', (width, height), color1) dc = ImageDraw.Draw(image) dc.rectangle( (width // 2, 0, width, height // 2), fill=color2) dc.rectangle( (0, height // 2, width // 2, height), fill=color2)
return image
icon = pystray.Icon( 'test name', icon=create_image(64, 64, 'black', 'white'))
icon.run()
|
这个仅创建一个 icon,但没有菜单绑定上。似乎即使没有菜单也能做某些交互,但我不处理这种情况。
菜单可以是多级的,菜单中的项,就像之前在 Qt 中学到的一样,对应一个一个 Action,而且它可以是 Checkable 的。菜单也可以包含子菜单。
下面演示 action 和 checked action,以及子菜单,以及演示 icon 自带 Notification 功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| state = False
def on_clicked(icon, item): global state state = not item.checked print(state)
menu = pystray.Menu( pystray.MenuItem('hello', action=lambda item: print('hello triggered')), pystray.MenuItem("Sub Menu", pystray.Menu( pystray.MenuItem('hello', action=lambda item: print('hello triggered')), )), pystray.MenuItem('checked', action=on_clicked, checked=lambda item: state), pystray.Menu(
) pystray.MenuItem('Exit', action=lambda item: icon.stop()) )
icon = pystray.Icon('test name', icon=create_image(64, 64, 'black', 'white'), menu=menu)
|
注意——Menu 和 MenuItem 均是不可变的,它们内部甚至是无状态的。看上去它们有 Checked 状态,但这个状态也是要通过执行外部函数得到的,Menu 只有在执行 Action 时才会重新执行 Checked,即触发 Menu 的更改。
| icon = pystray.Icon('test name', icon=create_image(64, 64, 'black', 'white'), menu=pystray.Menu( pystray.MenuItem('Add Item', action=lambda item: add_item()), ))
def add_item(): icon.menu = pystray.Menu( *icon.menu.items, pystray.MenuItem('rua', lambda: print(1)) )
|
同时,pystray 也允许 Menu 接受一个 Callable[(), Iterator[MenuItem]]
(这里的设计很奇怪,为啥不让Icon的menu接受Callable?),这时候每次需要更新时需要调用 icon.update_menu()
,此时 pystray 会重新执行这个 Callable,得到新的 Menu。
下面是新建一个异步线程去更新一个 item 为当前时间的全部代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| icon = pystray.Icon('test name', icon=create_image(64, 64, 'black', 'white'), menu=pystray.Menu( pystray.MenuItem('current time: ', action=lambda item: 1), )) import datetime async def main(): while True: await asyncio.sleep(1) icon.menu = pystray.Menu( pystray.MenuItem(f'current time: {datetime.datetime.now():%H:%M:%S}', action=lambda item: 1), )
def start_async_thread(): def go(): asyncio.run(main()) threading.Thread(target=go).start()
start_async_thread() icon.run()
|
另一种实现方式是上面说的让每次动态生成 Menu,显然这种方式对工程更为方便:
| icon = pystray.Icon('test name', icon=create_image(64, 64, 'black', 'white'), menu=pystray.Menu(lambda: [pystray.MenuItem(f'current time: {datetime.datetime.now():%H:%M:%S}', action=lambda item: 1)]))
import datetime async def main(): while True: await asyncio.sleep(1) icon.update_menu()
|
但这里有一个缺陷——托盘的 item 只有重新打开托盘时才刷新,所以无法去实现实时显示当前时间的功能,但这不怎么是一个问题。