Python : les interfaces graphiques

nora.nckm.eu

Illustration

Les principales bibliothèques destinées à concevoir des interfaces graphiques sous Linux sont Qt et GTK.

Qt

La documentation contient notamment les informations sur Qt for Python et l'API PySide6.

Après avoir installé le module pyside6, exemple d'application :

from functools import partial
from random import randrange
from PySide6.QtWidgets import QApplication, QWidget, QHBoxLayout, QVBoxLayout, QPushButton, QLineEdit, QLabel, QListWidget, QSizePolicy

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("GUI Sandbox")

        main_layout = QHBoxLayout(self)
        left_layout = QVBoxLayout()
        right_layout = QVBoxLayout()

        main_layout.addLayout(left_layout)
        main_layout.addLayout(right_layout)

        for i in range(5):
            button = QPushButton(f"Button {i}")
            left_layout.addWidget(button)
            button.clicked.connect(partial(self.button_clicked, i))

        self.le_text = QLineEdit()
        self.lbl_text = QLabel("…")
        self.btn_clear = QPushButton("Clear")
        self.btn_todo = QPushButton("Todo")
        self.btn_custom = MyButton("Custom", flat=True)

        self.le_text.textChanged.connect(self.lbl_text.setText)
        self.btn_clear.clicked.connect(self.le_text.clear)
        self.btn_todo.clicked.connect(self.create_todo_window)

        right_layout.addWidget(self.le_text)
        right_layout.addWidget(self.lbl_text)
        right_layout.addWidget(self.btn_clear)
        right_layout.addWidget(self.btn_todo)
        right_layout.addWidget(self.btn_custom)

        self.lbl_text.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        main_layout.setStretch(0, 2)
        main_layout.setStretch(1, 10)

    def button_clicked(self, msg):
        print(f"bouton {msg} cliqué")

    def create_todo_window(self):
        self.todo_win = TodoWindow()
        self.todo_win.show()

class TodoWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("My ToDo")

        main_layout = QVBoxLayout(self)

        self.lw_tasks = QListWidget()
        self.le_task = QLineEdit()
        self.le_task.setPlaceholderText("Tâche à effectuer…")
        self.btn_delete = QPushButton("Tout supprimer")

        self.le_task.returnPressed.connect(self.add_task)
        self.lw_tasks.itemDoubleClicked.connect(self.delete_task)
        self.btn_delete.clicked.connect(self.lw_tasks.clear)

        main_layout.addWidget(self.lw_tasks)
        main_layout.addWidget(self.le_task)
        main_layout.addWidget(self.btn_delete)

    def add_task(self):
        self.lw_tasks.addItem(self.le_task.text())
        self.le_task.clear()

    def delete_task(self, item):
        self.lw_tasks.takeItem(self.lw_tasks.row(item))

class MyButton(QPushButton):
    def __init__(self, text, size=48, flat=False):
        super().__init__(text)
        self.setMinimumSize(size, size)
        self.setFlat(flat)
        self.clicked.connect(self.random_color)

    def random_color(self):
        self.setStyleSheet(f"color: rgb({randrange(255)}, {randrange(255)}, {randrange(255)});")

app = QApplication()
win = MainWindow()
win.show()
app.exec()

Une autre manière de concevoir les interfaces consiste à les décrire dans un fichier XML (.ui) que l'on importe grâce à QUiLoader (cf doc).

Qt Designer permet d'éditer visuellement le fichier :

.env/bin/pyside6-designer

Qt User Interface Compiler permet de compiler le fichier en Python :

.env/bin/pyside6-uic window.ui -o window_ui.py -g python

Astuces

Les sources sont disponibles sur sourcehut ainsi que d'autres projets d'exemple.

Gérer la taille des widgets :

# Taille recommandée
widget.sizeHint()
# Politique de taille
widget.setSizePolicy(horizontal, vertical)
# Facteur d'étirement
layout.setStretch(index, stretch)

Modifier le style d'un widget :

self.lw_tasks = QtWidgets.QListWidget()
stylesheet = f"""
    QListView::item:selected {{
        background: rgb({color_str});
        color: rgb(0, 0, 0);
    }}
"""
self.lw_tasks.setStyleSheet(stylesheet)

Créer un raccourci clavier :

QtGui.QShortcut(QtGui.QKeySequence("Backspace"), self.widget, self.function)

Ajouter une action dans la barre d'outils :

self.toolbar = QtWidgets.QToolBar()

# Avec une icône standard
icon = self.style().standardIcon(QtWidgets.QStyle.SP_MediaPlay)
# Avec une icône personnalisée
icon = QtGui.QIcon("play.svg")

action = self.toolbar.addAction(icon, "Lire")
action.triggered.connect(self.play_function)

Exécuter une tâche en arrière-plan avec barre de progression :

class Worker(QtCore.QObject):
    image_converted = QtCore.Signal(object, bool)
    finished = QtCore.Signal()

    def __init__(self, images, size):
        super().__init__()
        self.images = images
        self.size = size
        self.run = True

    def convert_images(self):
        for item in self.images:
            if self.run and not item.processed:
                success = reduce(path=item.text(), size=self.size)
                self.image_converted.emit(item, success)

        self.finished.emit()


class MainWindow(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.lw_files = QtWidgets.QListWidget()
        self.btn_convert = QtWidgets.QPushButton("Convertir")
        self.btn_convert.clicked.connect(self.convert_images)

    def convert_images(self):
        lw_items = [self.lw_files.item(i) for i in range(self.lw_files.count())]
        img_to_convert = [1 for i in lw_items if not i.processed]

        # Launch a task in background with Thread and Worker
        self.thread = QtCore.QThread(self)
        self.worker = Worker(images=lw_items, size=size)
        self.worker.moveToThread(self.thread)
        self.worker.image_converted.connect(self.image_converted)
        self.worker.finished.connect(self.thread.quit)
        self.thread.started.connect(self.worker.convert_images)
        self.thread.start()

        self.prg_dialog = QtWidgets.QProgressDialog("Conversion des images", "Annuler", 1, len(img_to_convert))
        self.prg_dialog.canceled.connect(self.cancel_convert_images)
        self.prg_dialog.show()

        def cancel_convert_images(self):
            self.worker.run = False
            self.thread.quit()

        def image_converted(self, lw_item, success):
            if success:
                lw_item.setIcon(self.ico_checked)
                lw_item.processed = True
                self.prg_dialog.setValue(self.prg_dialog.value() + 1)

GTK

La documentation contient notamment les informations sur Python GTK et l'API PyGObject ainsi qu'un tutoriel complet (GTK4).

Après avoir installé les packages python-gobject et gtk4 sous Arch Linux (cf installation), exemple d'application :

import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk

class MainWindow(Gtk.ApplicationWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_title("GUI Sandbox")

        self.main_layout = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        self.left_layout = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.right_layout = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

        self.set_child(self.main_layout)
        self.main_layout.append(self.left_layout)
        self.main_layout.append(self.right_layout)

        for i in range(5):
            button = Gtk.Button(label=f"Button {i}")
            self.left_layout.append(button)
            button.connect('clicked', self.button_clicked, i)

        self.le_text = Gtk.Entry()
        self.lbl_text = Gtk.Label(label="…")
        self.btn_clear = Gtk.Button(label="Clear")
        self.btn_todo = Gtk.Button(label="Todo")

        self.le_text.connect('changed', self.le_text_changed)
        self.btn_clear.connect('clicked', self.btn_clear_clicked)
        self.btn_todo.connect('clicked', self.create_todo_window)

        self.right_layout.append(self.le_text)
        self.right_layout.append(self.lbl_text)
        self.right_layout.append(self.btn_clear)
        self.right_layout.append(self.btn_todo)

        # HeaderBar
        self.header = Gtk.HeaderBar()
        self.set_titlebar(self.header)
        self.open_button = Gtk.Button(label="Open")
        self.open_button.set_icon_name("document-open-symbolic")
        self.header.pack_start(self.open_button)

    def button_clicked(self, widget, msg):
        print(f"bouton {msg} cliqué")

    def btn_clear_clicked(self, widget):
        self.le_text.set_text("")

    def le_text_changed(self, widget):
        self.lbl_text.set_text(widget.get_text())

    def create_todo_window(self, widget):
        self.todo_win = TodoWindow()
        self.todo_win.present()

class TodoWindow(Gtk.Window):
    def __init__(self):
        super().__init__()
        self.set_title("My ToDo")

        self.main_layout = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.set_child(self.main_layout)

        # TreeView
        self.liststore = Gtk.ListStore(str)
        treeview = Gtk.TreeView(model=self.liststore)
        renderer = Gtk.CellRendererText()
        renderer.set_property("editable", True)
        column = Gtk.TreeViewColumn("Tâches", renderer, text=0)
        treeview.append_column(column)

        self.le_task = Gtk.Entry(placeholder_text="Tâche à effectuer…")
        self.btn_delete = Gtk.Button(label="Tout supprimer")

        self.le_task.connect('activate', self.add_task)
        renderer.connect('edited', self.edit_task)
        self.btn_delete.connect('clicked', self.delete_tasks)

        self.main_layout.append(treeview)
        self.main_layout.append(self.le_task)
        self.main_layout.append(self.btn_delete)

    def add_task(self, widget):
        self.liststore.append([self.le_task.get_text()])
        self.le_task.set_text("")

    def edit_task(self, widget, path, text):
        self.liststore[path][0] = text

    def delete_tasks(self, widget):
        self.liststore.clear()

class MyApp(Gtk.Application):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.connect('activate', self.on_activate)

    def on_activate(self, app):
        self.win = MainWindow(application=app)
        self.win.present()

app = MyApp(application_id="com.example.MyApp")
app.run()

Une autre manière de concevoir les interfaces consiste à les décrire dans un fichier XML (.ui) que l'on importe grâce à GtkBuilder. Il est également possible de décrire les interfaces avec le langage Blueprint (.blp). Le site Python et GTK4 présente de nombreux exemples.

Emojis

Un commentaire sur un de mes articles ? Commencez une discussion sur ma liste de diffusion en envoyant un email à ~nora/public-inbox@lists.sr.ht [règles]