Outils pour utilisateurs

Outils du site


pyqt4_system_tray

Faire qu'un programme PyQt soit piloté par une icône dans la zone de notification

[Modification le 3/7/2015: Python v3.4 et PyQt5 v5.5]

Objectif

Par exemple sous Windows:

  • au démarrage du PC, le programme se charge automatiquement et se lance, mais seule l'icône apparait dans la zone de notification.
  • Si on clique sur l'icône, la fenêtre du programme s'affiche.
  • On utilise le programme.
  • A un moment, on clique sur la petite croix pour fermer la fenêtre du programme. Mais en fait, elle ne se ferme pas: elle se cache (hide) et elle disparait de la barre des tâches.
  • En recliquant sur l'icône, elle s'affiche de nouveau (show) en ayant conservé sa configuration: on peut donc poursuivre l'utilisation du programme déjà commencée précédemment.

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.


Principe

On crée un code basé sur l'objet QtWidgets.QSystemTrayIcon.

Entre ce code et le programme à lancer, il y a 2 solutions:

  • Cas 1 ⇒ on lance le programme qui lance le code tray
  • Cas 2 ⇒ on lance le code tray qui lance le programme

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.


Code à adapter au programme en question

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:

  • indispensable: une icône pour la zone de notification appelée ici “icone.png”
  • optionnel: une icône pour l'item du menu “Quitter” appelée ici “icone_quitter.png”
#! /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.


Rendre ce programme autonome avec cx_freeze

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!

pyqt4_system_tray.txt · Dernière modification: 2015/08/03 09:15 de tyrtamos

Outils de la page