系统托盘图示 (system tray icon) 通常用来在桌面应用程序最小化后,不希望在任务列(task
bar)出现时,提供一个简单跟使用者交互的接口。 透过这样的用户接口,应用程序可以在有重要事件发生时,即时通知用户。因此,系统托盘图示常被诸如“邮件检查”、“股票报价”等不需要复杂接口的桌面应用所使用。本文山姆锅说明 Python 如何使用 PySide 来实现一个跨平台 (cross-platform) 的系统托盘图示应用程序。
目前三种主要的桌面操作系统,也就是 Windows, Mac OSX, 以及 Linux,都有支持托盘图示接口,但是名称跟支持程度稍有不同。 PySide (QT) 提供一个跨平台的方案,对大部分的 Python
桌面应用来说,这是适合的方案。如果真的不需要其他接口组件, 可以针对各个平台分别来实现系统托盘程序,对于这样的情况,山姆锅建议使用下列组合:
Windows: PyWin32
Ubuntu(Linux): PyGObject
注意:其中 PyGObject 只适合 GTK3 桌面环境。
执行环境
山姆锅假设以下的执行环境:
Python 2.7.x
PySide 1.2.2
QT 4
范例程序
本文使用 PySide 完成一个单纯的桌面程序 AvaShell,可以做到下列功能:
在系统托盘显示一个图示。
用户点选图示后,会弹跳 (pop-up) 一个菜单。
用户可以从菜单选择离开程序。
是的,目前就只能完成上述功能。
为了封装代码, 先定义一个 Shell 抽象类作为后续实现的基础,这样以后可以依照不同环境选用不同实现:
shell_base.pyview raw1
2
3
4
5
6
7
8
9
10
11
12
13from __future__ import absolute_import, division, print_function, unicode_literals
from abc import abstractmethod
class (object):
def (self):
"""Starts up the shell.
"""
pass
抽象方法(method) run 必须由继承的子类来实现。底下是采用 PySide 的实现类:
shell_pyside.pyview raw1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81from __future__ import absolute_import, print_function, unicode_literals
import sys
import logging
from PySide.QtGui import *
from avashell.shell_base import ShellBase
from avashell.utils import resource_path
_logger = logging.getLogger(__name__)
class MainWnd(QMainWindow):
def __init__(self, shell, icon):
super(MainWnd, self).__init__()
self._shell = shell
self.icon = icon
self.context_menu = None
self.tray_icon = None
if not QSystemTrayIcon.isSystemTrayAvailable():
msg = "I couldn't detect any system tray on this system."
_logger.error(msg)
QMessageBox.critical(None, "AvaShell", msg)
sys.exit(1)
self.init_ui()
def init_ui(self):
self.setWindowIcon(self.icon)
self.setWindowTitle('AvaShell')
self.create_tray_icon(self.icon)
self.tray_icon.show()
def on_tray_activated(self, reason=None):
_logger.debug("Tray icon activated.")
def on_quit(self):
self._shell.quit_app()
def create_context_menu(self):
self.quit_action = QAction("&Quit AvaShell", self, triggered=self.on_quit)
menu = QMenu(self)
menu.addAction(self.quit_action)
return menu
def create_tray_icon(self, icon):
self.context_menu = self.create_context_menu()
self.tray_icon = QSystemTrayIcon(self)
self.tray_icon.setContextMenu(self.context_menu)
self.tray_icon.setIcon(icon)
self.tray_icon.activated.connect(self.on_tray_activated)
class Shell(ShellBase):
""" Shell implementation using PySide
"""
def __init__(self):
super(Shell, self).__init__()
self.app = QApplication(sys.argv)
self.app.setQuitOnLastWindowClosed(False)
self.icon = QIcon(resource_path('res/icon.png'))
self.menu = None
self.wnd = MainWnd(self, self.icon)
def quit_app(self):
self.app.quit()
def run(self):
_logger.info("Shell is running...")
self.app.exec_()
if __name__ == '__main__':
shell = Shell()
shell.run()
Shell 为 ShellBase 的子类,负责建构 QApplication 以及主窗口(MainWnd); 主窗口负责建立 QSystemTrayIcon 这个代表系统托盘的对象,以及它使用的弹出式菜单,同时注册相关事件处理器。由于主窗口并不显示,且不希望主窗口被关闭的时候, 程序自动离开,标号 1 的叙述就是通知 PySide 不要自动离开程序。
为了找到图档资源(resource),另外定义了一个公用函数 resource_path,以便之后在使用 PyInstaller 打包后能正常运行。此公用函数定义在 utils.py 这个文件中。
utils.pyview raw1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18from __future__ import absolute_import, division, print_function, unicode_literals
import os
import sys
def resource_path(relative):
"""Gets the resource's absolute path.
:param relative: the relative path to the resource file.
:return: the absolute path to the resource file.
"""
if hasattr(sys, "_MEIPASS"): # 1
return os.path.join(sys._MEIPASS, relative)
abspath = os.path.abspath(os.path.join(__file__, "..")) # 2
abspath = os.path.dirname(abspath)
return os.path.join(abspath, relative)
标号 1 的叙述判断是否在经过 PyInstaller 打包好的环境执行,如果是则直接使用 sys._MEIPASS 这个特殊属性值作为资源路径; 如果不是, resource_path 则假设资源文件放在它的上层目录。 因此,标号 2 的叙述需要根据 utils.py 与资源的相对位置作调整。
参考数据
结语
使用 PySide 或者 PyQT 可以很方便地实现跨平台的图形接口,对于大部分的桌面应用来说,这是好事。但是对于只需要一个简单的系统托盘图示的应用来说,使用 PySide 意味着额外需要十几 MB 的空间来散布所开发的应用程序。 即使如此,相对于针对各个平台开发所需的时间来说,使用 PySide 通常是比较合理的作法。