Intégrer MatPlotLib dans vos interfaces graphiques¶

L'intégration de Matplotlib (MPL) dans une interface graphique (GUI) offre une flexibilité et une interactivité considérablement améliorées pour les utilisateurs. Alors que mpl seul permet de créer des graphiques statiques, l'incorporation dans une GUI permet aux utilisateurs d'interagir directement avec les données visuelles par le biais d'outils de zoom, de panning, de sélection et d'autres options personnalisables. Cela rend l'analyse des données plus intuitive et efficace, car les utilisateurs peuvent explorer les tendances et les détails spécifiques sans avoir à générer de nombreux graphiques statiques pour chaque vue.

En outre, une interface utilisateur bien conçue peut simplifier la manipulation de vastes ensembles de données et rendre les visualisations plus accessibles, même à ceux qui ne sont pas familiers avec la programmation.

On parle de backend pour identifier la solution technique utilisée pour la mise en oeuvre d'interfaces graphiques. Matplotlib offre une grande variété de backends et notamment :

  • Backend Qt (Qt5Agg) : Qt est un framework de développement d'applications multiplateforme largement utilisé, qui permet de créer des interfaces graphiques, des applications de bureau, des applications mobiles et même des interfaces pour des systèmes embarqués. Il est réputé pour sa flexibilité, sa performance et son ensemble complet de widgets et d'outils de développement. MPL est compatible avec les deux binding Qt existants : PySide et PyQt.

  • Backend Tkinter (TkAgg) : Tk est une bibliothèque de création d'interfaces graphiques (GUI) qui est utilisée dans divers langages de programmation (initialement développée pour TCL), et qui est particulièrement connue pour sa simplicité et sa légèreté. Tkinter est l'interface Python (on dit aussi le binding) pour Tk, et est souvent considérée comme la bibliothèque de GUI standard pour Python. Moins riche que Qt, Tk permet néanmoins de développer des interfaces graphiques complexes et fonctionnelles.

  • Backend GTK (GTK3Agg) : GTK, acronyme de GIMP Toolkit, est une bibliothèque de création d'interfaces graphiques (GUI) multiplateforme largement utilisée, en particulier dans les environnements de bureau Linux tels que GNOME. GTK est apprécié pour sa flexibilité, son look moderne et sa capacité à créer des interfaces hautement personnalisables. Bien entendu, GTK et utilisable en Python.

  • Il existe encore d'autres backends...

Intégration de MPL dans une application Tkinter¶

Nous allons développer une application graphique en Python qui permet aux utilisateurs de visualiser des courbes de fonctions sinus et cosinus. La fenêtre de l'application contient deux boutons, "Sinus" et "Cosinus", placés côte à côte en haut, ainsi qu'une zone de tracé en dessous. En cliquant sur l'un des boutons, la courbe correspondante de la fonction sinus ou cosinus est tracée sur la zone de dessin. De plus, lorsque l'utilisateur clique sur la zone de tracé, un marqueur est ajouté à l'endroit cliqué et des informations sur l'événement de clic sont affichées dans la console. L'application utilise la bibliothèque Tkinter pour l'interface graphique et Matplotlib pour les graphiques.

Voici le visuel de l'application à développer.

image.png

Et voici maintenant le code Python/Tkinter :

In [1]:
import sys
import numpy as np
import tkinter as tk

from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg


class MyWindow(tk.Tk):

    def __init__(self):
        super().__init__()
        self.title("Curve Tracer V1.0")

        # On définit le Frame qui va contenir nos boutons.
        button_frame = tk.Frame(self)
        button_frame.pack(side=tk.TOP, fill=tk.X)

        # On place les deux boutons dans le frame.
        self.__btn_sinus = tk.Button(button_frame, text="Sinus", command=self.btn_sinus_clicked)
        self.__btn_sinus.grid(row=0, column=0, sticky="nsew")

        self.__btn_cosinus = tk.Button(button_frame, text="Cosinus", command=self.btn_cosinus_clicked)
        self.__btn_cosinus.grid(row=0, column=1, sticky="nsew")

        # On configure les boutons pour se partager la place disponible.
        button_frame.grid_columnconfigure(0, weight=1)
        button_frame.grid_columnconfigure(1, weight=1)

        # On instancie le Canvas MPL.
        self.__fig = Figure(figsize=(4, 3))
        self.__canvas = FigureCanvasTkAgg(self.__fig, master=self)
        self.__canvas.get_tk_widget().pack(fill=tk.BOTH, expand=1)
        self.__canvas.mpl_connect("button_press_event", self.canvas_clicked)
        self.__plt = self.__fig.add_subplot(111)

        self.btn_sinus_clicked()

    def canvas_clicked(self, event):
        self.__plt.scatter(event.xdata, event.ydata, marker="+", s=100)
        self.__canvas.draw()
        print(dir(event))

    def btn_sinus_clicked(self):
        self.__plt.clear()
        x = np.linspace(-10, 10, 1000)
        y = np.sin(x)
        self.__plt.plot(x, y, "r")
        self.__canvas.draw()

    def btn_cosinus_clicked(self):
        self.__plt.clear()
        x = np.linspace(-10, 10, 1000)
        y = np.cos(x)
        self.__plt.plot(x, y, "b--")
        self.__canvas.draw()


if __name__ == "__main__":
    app = MyWindow()
    app.mainloop()

Comme vous le constater, le look Tk n'est pas forcément le plus sympa.

Heureusement, le projet CustomTkinter permet de relooker un peu les choses. Deux thèmes graphiques peuvent utilisés : Light et Dark.

Light theme

Dark theme

Et voici les modifications à apporter au code pour utiliser l'extension CustomTkinter.

Note : pensez à l'installer (!pip install CustomTkinter).

In [2]:
import sys
import numpy as np
import customtkinter as ctk

from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg


class MyWindow(ctk.CTk):

    def __init__(self):
        super().__init__()
        self.title("Curve Tracer V1.0")

        # On définit le Frame qui va contenir nos boutons.
        button_frame = ctk.CTkFrame(self)
        button_frame.pack(side='top', fill='x')

        # On place les deux boutons dans le frame.
        self.__btn_sinus = ctk.CTkButton(button_frame, text="Sinus", command=self.btn_sinus_clicked)
        self.__btn_sinus.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)

        self.__btn_cosinus = ctk.CTkButton(button_frame, text="Cosinus", command=self.btn_cosinus_clicked)
        self.__btn_cosinus.grid(row=0, column=1, sticky="nsew", padx=10, pady=10)

        # On configure les boutons pour se partager la place disponible.
        button_frame.grid_columnconfigure(0, weight=1)
        button_frame.grid_columnconfigure(1, weight=1)

        # On instancie le Canvas MPL.
        self.__fig = Figure(figsize=(4, 3))
        self.__canvas = FigureCanvasTkAgg(self.__fig, master=self)
        self.__canvas.get_tk_widget().pack(fill='both', expand=1)
        self.__canvas.mpl_connect("button_press_event", self.canvas_clicked)
        self.__plt = self.__fig.add_subplot(111)

        self.btn_sinus_clicked()

    def canvas_clicked(self, event):
        self.__plt.scatter(event.xdata, event.ydata, marker="+", s=100)
        self.__canvas.draw()
        print(dir(event))

    def btn_sinus_clicked(self):
        self.__plt.clear()
        x = np.linspace(-10, 10, 1000)
        y = np.sin(x)
        self.__plt.plot(x, y, "r")
        self.__canvas.draw()

    def btn_cosinus_clicked(self):
        self.__plt.clear()
        x = np.linspace(-10, 10, 1000)
        y = np.cos(x)
        self.__plt.plot(x, y, "b--")
        self.__canvas.draw()


if __name__ == "__main__":
    app = MyWindow()
    app.mainloop()

Intégration de MPL dans une application Qt¶

PyQt et PySide sont deux bindings populaires qui permettent aux développeurs Python d'intégrer la bibliothèque Qt dans leurs applications.

  • PyQt, développé par Riverbank Computing, est l’un des bindings les plus anciens et propose des liaisons pour Qt sous plusieurs licences, y compris GPL et une licence commerciale.
  • PySide, également connu sous le nom de PySide2 ou PySide6 pour les versions liées à Qt 5 et Qt 6 respectivement, est le binding officiel développé par The Qt Company et est disponible sous la licence LGPL, ce qui le rend plus permissif pour les projets commerciaux.

Les deux bindings sont très similaires en termes de fonctionnalités, et le choix entre eux dépend souvent de considérations de licence et de préférences personnelles. Personnellement, j'utilise plutôt PySide (en version 6) pour sa licence LGPL.

L'exemple de code suivant est similaire à l'exemple pour Tkinter vu précédemment.

In [ ]:
import sys

from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QPushButton, QHBoxLayout, QApplication

import numpy as np
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvas


class MyWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        self.setWindowTitle("Curve Tracer V1.0")

        mainWidget = QWidget()
        self.setCentralWidget(mainWidget)
        vbox = QVBoxLayout(mainWidget)
        # vbox = QVBoxLayout()
        # mainWidget.setLayout(vbox)

        self.__btnSinus = QPushButton("Sinus")
        self.__btnSinus.clicked.connect(self.btnSinusClicked)
        self.__btnCosinus = QPushButton("Cosinus")
        self.__btnCosinus.clicked.connect(self.btnCosinusClicked)

        hbox = QHBoxLayout()
        hbox.addWidget( self.__btnSinus)
        hbox.addWidget( self.__btnCosinus)
        vbox.addLayout(hbox)

        self.__canvas = FigureCanvas(Figure(figsize=(4, 3)))
        self.__canvas.mpl_connect("button_press_event", self.canvasClicked)
        vbox.addWidget(self.__canvas)
        self.__plt = self.__canvas.figure.subplots()

        self.btnSinusClicked()

    def canvasClicked(self, event):
        self.__plt.scatter(event.xdata, event.ydata, marker="+", s=100)
        self.__canvas.draw()
        print(dir(event))

    def btnSinusClicked(self):
        self.__plt.clear()
        x = np.linspace(-10, 10, 1000)
        y = np.sin(x)
        self.__plt.plot(x, y, "r")
        self.__canvas.draw()

    def btnCosinusClicked(self):
        self.__plt.clear()
        x = np.linspace(-10, 10, 1000)
        y = np.cos(x)
        self.__plt.plot(x, y, "b--")
        self.__canvas.draw()


if __name__ == "__main__":
    app = QApplication(sys.argv)

    myWindow = MyWindow()
    myWindow.show()

    sys.exit(app.exec())

Pour ceux qui sont intéressés par l'utilisation de Qt pour y intégrer un composant MatPlotLib, je vous renvoie vers mon tuto Qt : vous y trouverez quelques exemples complémentaires.