[Modification le 3/7/2015: Python v3.4 et PyQt5 v5.5]
Par exemple sous Windows:
Cela permet d'avoir un programme Python/PyQt disponible en permanence, sans pour autant que cela prenne de la place sur la barre des tâches.
J'ai ajouté un petit menu contextuel (clic droit) pour quitter effectivement le programme, ce qui ferme la fenêtre du programme, et fait disparaitre l'icône de la zone de notification.
On crée un code basé sur l'objet QtWidgets.QSystemTrayIcon.
Entre ce code et le programme à lancer, il y a 2 solutions:
On va traiter ici le cas 2, parce qu'il a un grand intérêt: il ne nécessite pas de modifier le code du programme pour qu'il soit lancé par le code tray.
On va prendre un programme test tout simple: une fenêtre avec un QLineEdit au milieu:
Fichier fenetre.pyw:
#! /usr/bin/python # -*- coding: utf-8 -*- # Python v3 PyQt5 __programme__ = "Programme test" __logiciel__ = "fenetre.pyw" __version__ = "2.00" __date__ = "03/08/2015" import sys from PyQt5 import QtCore, QtWidgets ############################################################################# class Fenetre(QtWidgets.QWidget): #======================================================================== def __init__(self, parent=None): super().__init__(parent) self.resize(400, 300) # créer le lineEdit self.lineEdit = QtWidgets.QLineEdit(self) # positionnement des widgets dans la fenêtre posit = QtWidgets.QGridLayout() posit.addWidget(self.lineEdit, 0, 0) self.setLayout(posit) ############################################################################# if __name__ == '__main__': app = QtWidgets.QApplication(sys.argv) fen = Fenetre() fen.show() sys.exit(app.exec_())
Et voilà le code tray. Pour l'exemple, il s'appellera fenetre_tray.pyw:
Il faudra lui ajouter:
#! /usr/bin/python # -*- coding: utf-8 -*- # Python 3, PyQt5 """ lancement d'une icone dans le tray. clic gauche sur l'icone => affiche la fenêtre programme clic droit sur l'icone => menu contextuel: -> quitte avec confirmation oui/non """ import sys from PyQt5 import (QtWidgets, QtGui, QtCore) ############################################################################# ############################################################################# # adaptation au programme à lancer # ================================ #============================================================================ # renommer le fichier tray.pyw qui lance "programme" => programme_tray.pyw #============================================================================ # importation de la fenêtre à lancer from fenetre import Fenetre #============================================================================ # nom du programme programme = "Fenêtre pour test" # bulle bulle = programme # icone de la fenêtre qui sera aussi l'icone du tray icone = "icone.png" #============================================================================ # fonction qui lance la fenêtre sans l'afficher def lancementfen(): """lance la fenêtre sans l'afficher """ # retourne la variable d'instance de la fenêtre à lancer return Fenetre() #============================================================================ def affichefenetre(fen): """complément d'affichage de la fenêtre du programme (ex: setFocus) """ # met le focus sur la ligne de saisie de la fenêtre fen.lineEdit.setFocus() ############################################################################# ############################################################################# class SystemTrayIcon(QtWidgets.QSystemTrayIcon): #======================================================================== def __init__(self, app, qicone, bul="", parent=None): super().__init__(qicone, parent) self.app = app self.parent = parent #-------------------------------------------------------------------- # un clic gauche affichera la fenêtre self.activated.connect(self.affichefenetre) #-------------------------------------------------------------------- # ajoute une bulle d'information quand la souris est sur l'icône tray if bul != "": self.setToolTip(bul) #-------------------------------------------------------------------- # lance la fenêtre sans affichage (=> fonction adaptée "lancementfen") self.fen = lancementfen() # initialise le menu self.initmenu() #======================================================================== def initmenu(self): """initialise le popupmenu qui apparaitra au clic droit sur l'icône on peut, bien sûr, ajouter d'autres items, par exemple le "à propos" ou des items de configuration """ # crée le menu menu = QtWidgets.QMenu(self.parent) # ferme la fenêtre et quitte le programme: # l'icône de l'item "Quitter" du menu sera une simple croix rouge quitterIcon = QtGui.QIcon.fromTheme("application-exit", QtGui.QIcon("icone_quitter.png")) quitterAction = QtWidgets.QAction(quitterIcon, '&Quitter', self) quitterAction.triggered.connect(self.quitter) menu.addAction(quitterAction) self.setContextMenu(menu) #======================================================================== @QtCore.pyqtSlot("QObject") def affichefenetre(self, raison=None): """clic gauche sur l'icone => affiche la fenêtre au dessus des autres """ if raison == QtWidgets.QSystemTrayIcon.Trigger: self.fen.showNormal() # affiche en mode fenêtre self.fen.activateWindow() # affiche au dessus des autres fenêtres # pour d'éventuels compléments d'affichage (ex: setFocus) affichefenetre(self.fen) #======================================================================== @QtCore.pyqtSlot() def quitter(self): """permet de quitter: ferme le programme ainsi que le systemtray """ reponse = QtWidgets.QMessageBox.question(None, "Confirmez!", "Voulez-vous vraiment quitter?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) if reponse == QtWidgets.QMessageBox.Yes: # ferme la fenêtre si elle est encore ouverte try: self.fen.close() except Exception: pass # ferme l'application et arrête l'exécution self.app.quit() ############################################################################# if __name__ == '__main__': #======================================================================== app = QtWidgets.QApplication(sys.argv) #======================================================================== # définition du style (à adapter selon l'OS) if sys.platform=="win32": app.setStyle(QtWidgets.QStyleFactory.create("Fusion")) elif sys.platform=="linux": app.setStyle(QtWidgets.QStyleFactory.create("gtk")) elif sys.platform=="darwin": app.setStyle(QtWidgets.QStyleFactory.create("macintosh")) # pour afficher les styles disponible sous l'OS choisi: # exemple pour Windows: ['Windows', 'WindowsXP', 'WindowsVista', 'Fusion'] # print([st for st in QtWidgets.QStyleFactory.keys()]) #======================================================================== # indispensable pour utiliser QSystemTrayIcon # sinon: arrêt complet après fermeture d'un simple messagebox app.setQuitOnLastWindowClosed(False) #======================================================================== # pour assurer la traduction automatique du conversationnel à la locale # pour que le messagebox demande "Oui"/ "Non" (et non "Yes" / "No") locale = QtCore.QLocale.system().name() translator = QtCore.QTranslator () reptrad = QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath) translator.load("qtbase_" + locale, reptrad) app.installTranslator(translator) #======================================================================== # mettre la même icone par défaut pour toutes les fenêtres de l'application # (mais le programme lancé peut avoir sa propre icône) qicone = QtGui.QIcon(icone) # icone est une variable globale app.setWindowIcon(qicone) #======================================================================== # lancement du tray trayIcon = SystemTrayIcon(app, qicone, bulle) # bulle est une variable globale trayIcon.show() #-------------------------------------------------------------------- # message d'information affiché 1 seconde si l'OS le supporte # sous Windows, l'activation nécessite dans le registre: # HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\ # Explorer\Advanced\EnableBalloonTips => dword:0x00000001 # et la désactivation: ..\EnableBalloonTips => dword:0x00000000 if trayIcon.supportsMessages(): trayIcon.showMessage(programme, # programme est une variable globale "Cliquez sur l'icône pour afficher", QtWidgets.QSystemTrayIcon.Information, 1000) # temps d'affichage en millisecondes #======================================================================== # boucle des évènements sys.exit(app.exec_())
Il faut adapter le code tray au programme à lancer, et cette adaptation a été ramenée au début du code pour faciliter cette adaptation.
Rappel de cette partie qui figure déjà dans le code précédent:
############################################################################# ############################################################################# # adaptation au programme à lancer # ================================ #============================================================================ # renommer le fichier tray.pyw qui lance "programme" => programme_tray.pyw #============================================================================ # importation de la fenêtre à lancer from fenetre import Fenetre #============================================================================ # nom du programme programme = "Fenêtre pour test" # bulle bulle = programme # icone de la fenêtre qui sera aussi l'icone du tray icone = "icone.png" #============================================================================ # fonction qui lance la fenêtre sans l'afficher def lancementfen(): """lance la fenêtre sans l'afficher """ # retourne la variable d'instance de la fenêtre à lancer return Fenetre() #============================================================================ def affichefenetre(fen): """complément d'affichage de la fenêtre du programme (ex: setFocus) """ # met le focus sur la ligne de saisie de la fenêtre fen.lineEdit.setFocus() ############################################################################# #############################################################################
Une fois l'adaptation faite, on lance le code tray (ici fenetre_tray.pyw), qui lance la fenêtre du programme (ici fenetre.pyw), et on a ce qui est décrit dans le chapitre “objectif” plus haut.
On peut en plus traiter le code Python par cx_freeze, ce qui donnera un exécutable à lancer (“exe” sous Windows), et permettra d'utiliser cela sur des PC qui n'ont pas Python ni PyQt d'installé!
Il faut, bien entendu, installer avant cx_freeze selon la version de Python que vous avez (http://cx-freeze.sourceforge.net/). Sous Windows, on peut le télécharger ici: http://www.lfd.uci.edu/~gohlke/pythonlibs/. Par exemple, pour Python 3.4 32 bits, chercher: “cx_Freeze-4.3.4-cp34-none-win32.whl”. La version whl s'installe avec: “pip install some-package.whl”.
Sous Windows, il faudra ajouter l'icône qui sera intégrée dans l'exe: “icone.ico” qui pourra être une simple conversion de “icone.png” au format .ico spécifique Windows. A voir pour Linux avec le format .xbm.
Il faut créer un setup qu'on appellera ici: fenetre_tray_setup.py:
#!/usr/bin/python # -*- coding: utf-8 -*- # Python 3 # 07/2015 """ Pas d'accent dans le setup, ni dans la description, ni dans les commentaires Icone sous Windows: il faut: => un xxx.ico pour integration dans le exe, avec "icon=xxx.ico" => un xxx.png pour integration avec PyQt4 + demander la copie avec includefiles. """ import sys, os from cx_Freeze import setup, Executable ############################################################################# # preparation des options # chemins de recherche des modules path = sys.path # options d'inclusion/exclusion des modules includes = ["atexit", "sip"] excludes = [] packages = [] # copier les fichiers et/ou repertoires et leur contenu: includefiles = ["icone.png", "icone_quitter.png", ("qt_cx.conf", "qt.conf") # <= recopie du fichier de configuration Qt avec renommage ] # copier les fichiers necessaires aux traductions des messagebox if sys.platform == "win32": # repertoire de python.exe repexe = os.path.dirname(sys.executable) # repertoire de PyQt5 reppyqt = os.path.join(repexe, r"Lib\site-packages\PyQt5") # recopie du repertoire des plugins PyQt5 (dont pilotes SQL) includefiles += [(os.path.join(reppyqt, "plugins"), "plugins")] # recopie du repertoire des fichiers de traductions includefiles += [(os.path.join(reppyqt, "translations"), "translations")] elif sys.platform == "linux2": # repertoire des plugins PyQt5: verifier / adapter a la distribution includefiles += [(r"/usr/lib/qt5/plugins", "plugins")] # repertoire pour les traductions: verifier / adapter a la distribution includefiles += [(r"/usr/share/qt5/translations", "translations")] else: pass # cas du Mac OS X non traite ici # pour que les bibliotheques binaires de /usr/lib soient recopiees aussi sous Linux binpathincludes = [] if sys.platform == "linux2": binpathincludes += ["/usr/lib"] # niveau d'optimisation pour la compilation en bytecodes optimize = 0 # si True, n'affiche que les warning et les erreurs pendant le traitement cx_freeze silent = True # construction du dictionnaire des options options = {"path": path, "includes": includes, "excludes": excludes, "packages": packages, "include_files": includefiles, "bin_path_includes": binpathincludes, "create_shared_zip": False, # <= ne pas generer de fichier zip "include_in_shared_zip": False, # <= ne pas generer de fichier zip "compressed": False, # <= ne pas generer de fichier zip "optimize": optimize, "silent": silent } # pour inclure sous Windows les dll system de Windows necessaires if sys.platform == "win32": options["include_msvcr"] = True ############################################################################# # preparation des cibles base = None if sys.platform == "win32": base = "Win32GUI" #base = "Console" icone = None if sys.platform == "win32": icone = "icone.ico" cible_1 = Executable( script = "fenetre_tray.pyw", base = base, compress=False, # <= ne pas generer de fichier zip copyDependentFiles=True, appendScriptToExe=True, appendScriptToLibrary=False, # <= ne pas generer de fichier zip icon=icone ) cible_2 = Executable( script = "fenetre.pyw", base = base, compress=False, # <= ne pas generer de fichier zip copyDependentFiles=True, appendScriptToExe=True, appendScriptToLibrary=False, # <= ne pas generer de fichier zip icon=icone ) ############################################################################# # creation du setup setup( name = "fenetre_tray", version = "2.00", description = "fenetre_tray", author = "NomDuProgrammeur", options = {"build_exe": options}, executables = [cible_1, cible_2] )
Pour que PyQt trouve les adresses correctes pour les répertoires plugins et translations, il faut créer un petit fichier texte appelé ici “qt_cx.conf” et qui contiendra:
[Paths] Prefix = . Binaries = . Libraries = . Plugins = plugins Translations = translations
Ce fichier “qt_cx.conf” qui se trouvera dans le répertoire des sources Python (fenetre_tray.pyw) sera recopié par le traitement par cx_freeze et renommé automatiquement en “qt.conf” pour être utilisé par l'exe.
Si tout se passe bien, vous récuperez dans le sous-répertoire build\\exe.win32-2.7 plusieurs répertoires et une vingtaine de fichiers, et en particulier 2 fichiers “exe”: fenetre.exe et fenetre_tray.exe. Le 1er lance la console toute seule, et le 2e lance l'icône dans la zone de notification, qui affiche la fenêtre quand on clique.
Sous Windows 8.1, on suppose que vous travaillez en mode “bureau” similaire à Windows 7. Comme je déteste le mode “tuile”, il ne sera pas traité ici.
Pour que le programme se lance au boot du PC: sous Windows, on ajoute le raccourci qui pointe sur le programme exe “fenetre_tray.exe” dans le répertoire “Démarrage”. Voir à quel endroit il est selon la version de Windows: sous Windows 8.1, le répertoire de démarrage est ici:
C:\Users\utilisateur\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
Enfin, en jouant sur les propriétés de la zone de notification, on peut faire en sorte que l'icone soit toujours visible et non dans le petit menu. Dans ce cas, un simple clic gauche de la souris sur l'icone fait afficher la fenêtre du programme.
Amusez-vous bien!