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.
- layouts : QHBoxLayout, QVBoxLayout, QGridLayout, QFormLayout…
- widgets : QLabel, QLineEdit, QPushButton…
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.