系統托盤圖示 (system tray icon) 通常用來在桌面應用程式最小化後,不希望在任務列 (task
bar) 出現時,提供一個簡單跟使用者互動的介面。 透過這樣的用戶介面,應用程式可以在有重要事件發生時,即時通知用戶。因此,系統托盤圖示常被諸如「郵件檢查」、「股票報價」等不需要複雜介面的桌面應用所使用。本文山姆鍋說明 Python 如何使用 PySide 來實現一個跨平台 (cross-platform) 的系統托盤圖示應用程式。
目前三種主要的桌面作業系統,也就是 Windows, Mac OSX, 以及 Linux,都有支援托盤圖示介面,但是名稱跟支援程度稍有不同。 PySide (QT) 提供一個跨平台的方案,對大部份的 Python
桌面應用來說,這是適合的方案。如果真的不需要其他介面元件, 可以針對各個平台分別來實現系統托盤程式,對於這樣的情況,山姆鍋建議使用下列組合:
注意:其中 PyGObject 只適合 GTK3 桌面環境。
執行環境
山姆鍋假設以下的執行環境:
Python 2.7.x
PySide 1.2.2
QT 4
其中,Python 建議使用 Anaconda Scientific Python Distribution 。
範例程式
本文使用 PySide 完成一個單純的桌面程式 AvaShell
,可以做到下列功能:
在系統托盤顯示一個圖示。
用戶點選圖示後,會彈跳 (pop-up) 一個選單。
用戶可以從選單選擇離開程式。
是的,目前就只能完成上述功能。
為了封裝程式碼,先定義一個 Shell
抽象類別作為後續實作的基礎,這樣以後可以依照不同環境選用不同實作:
shell_base.py view raw 1 2 3 4 5 6 7 8 9 10 11 12 13 from __future__ import absolute_import, division, print_function, unicode_literalsfrom abc import abstractmethodclass ShellBase (object) : @abstractmethod def run (self) : """ Starts up the shell. """ pass
抽象方法 (method) run
必須由繼承的子類別來實作。底下是採用 PySide 的實作類別:
shell_pyside.py view raw 1 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 81 from __future__ import absolute_import, print_function, unicode_literalsimport sysimport loggingfrom PySide.QtGui import *from avashell.shell_base import ShellBasefrom 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.py view raw 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from __future__ import absolute_import, division, print_function, unicode_literalsimport osimport sysdef 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" ): return os.path.join(sys._MEIPASS, relative) abspath = os.path.abspath(os.path.join(__file__, ".." )) abspath = os.path.dirname(abspath) return os.path.join(abspath, relative)
標號 1 的敘述判斷是否在經過 PyInstaller 打包好的環境執行,如果是則直接使用 sys._MEIPASS
這個特殊屬性值作為資源路徑;如果不是, resource_path
則假設資源檔案放在它的上層目錄。 因此,標號 2
的敘述需要根據 utils.py
與資源的相對位置作調整。
參考資料
Anaconda Scientific Python Distribution :
https://store.continuum.io/cshop/anaconda/
PyWin32 : http://sourceforge.net/projects/pywin32/
PyObjC : https://pythonhosted.org/pyobjc/index.html
PyGObject :
http://python-gtk-3-tutorial.readthedocs.org/en/latest/index.html
PyInstaller : https://github.com/pyinstaller/pyinstaller
PySide : https://pypi.python.org/pypi/PySide/1.2.2
結語
使用 PySide 或者 PyQT 可以很方便地實現跨平台的圖形介面,對於大部份的桌面應用來說,這是好事。但是對於只需要一個簡單的系統托盤圖示的應用來說,使用 PySide 意味著額外需要十幾 MB 的空間來散佈所開發的應用程式。 即使如此,相對於針對各個平台開發所需的時間來說,使用 PySide 通常是比較合理的作法。