Различные статьи по Qt и c++

Работа с моделями данных в PySide6: QAbstractListModel на Python

Введение

Статья 489 QML-цикла разобрала QAbstractListModel на C++ — в этой статье повторим тот же паттерн на Python, что особенно важно для приложений, желающих использовать богатый, виртуализированный ListView/GridView QML (статья 488 QML-цикла) для отображения данных, обрабатываемых Python-кодом.

Концепция

Реализация QAbstractListModel на Python требует переопределения тех же обязательных методов, что и в C++ версии (rowCount(), data(), roleNames()), но с питоническим синтаксисом — roleNames() в Python обычно возвращает обычный словарь (dict), преобразуемый PySide6 в ожидаемый Qt-типа QHash<int, QByteArray> автоматически, без необходимости явно создавать соответствующий C++ тип вручную.

Пример кода: Работа с моделями данных в PySide6

# Python-реализация QAbstractListModel — прямой аналог C++ версии из статьи 489 QML-цикла
from PySide6.QtCore import QAbstractListModel, Qt, QModelIndex

class TaskListModel(QAbstractListModel):
    TitleRole = Qt.UserRole + 1
    CompletedRole = Qt.UserRole + 2

    def __init__(self, parent=None):
        super().__init__(parent)
        self._tasks = []  # каждый элемент — словарь {"title": str, "completed": bool}

    def rowCount(self, parent=QModelIndex()) -> int:
        return len(self._tasks)

    def data(self, index: QModelIndex, role: int):
        if not index.isValid() or index.row() >= len(self._tasks):
            return None

        task = self._tasks[index.row()]
        if role == self.TitleRole:
            return task["title"]
        if role == self.CompletedRole:
            return task["completed"]
        return None

    def roleNames(self) -> dict:
        return {
            self.TitleRole: b"title",       # имена ролей передаются как bytes, а не обычная str
            self.CompletedRole: b"completed",
        }

    def addTask(self, title: str):
        self.beginInsertRows(QModelIndex(), len(self._tasks), len(self._tasks))
        self._tasks.append({"title": title, "completed": False})
        self.endInsertRows()  # обязательное уведомление видов QML, как и в C++ версии
# Использование модели — передача в QML через контекстное свойство (статья 503)
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
import sys

app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()

task_model = TaskListModel()
task_model.addTask("Купить продукты")
task_model.addTask("Написать статью про PySide6")

engine.rootContext().setContextProperty("taskListModel", task_model)
engine.load("main.qml")

sys.exit(app.exec())

Пояснения к коду

TitleRole = Qt.UserRole + 1/CompletedRole = Qt.UserRole + 2 показывают тот же паттерн смещения от базового значения Qt.UserRole, что и в C++ версии (статья 489 QML-цикла) — каждая пользовательская роль должна иметь собственное, уникальное числовое значение, и явное смещение предотвращает случайное совпадение значений разных ролей. roleNames(), возвращающий обычный Python dict с ключами-числами и значениями типа bytes (b"title", а не обычная строка "title") — важная деталь синтаксиса, отражающая то, что соответствующий C++ тип QByteArray ожидает именно байтовую строку, а не текстовую Python-строку Unicode, и PySide6 не выполняет автоматического преобразования str в bytes в этом конкретном контексте. beginInsertRows()/endInsertRows() в addTask() показывают, что обязательность этого паттерна уведомления подключённых видов сохраняется идентично C++ версии — Python не меняет фундаментальные требования протокола модели/представления Qt, лишь синтаксис их выражения.

Подводные камни

  • Использование обычной Python-строки ("title") вместо bytes (b"title") в качестве значения, возвращаемого roleNames() — это частая, легко допускаемая ошибка при переносе паттерна с C++ на Python, поскольку интуитивно строка кажется более естественным, «питоническим» выбором, но конкретный ожидаемый тип (QByteArray, соответствующий Python bytes) требует именно байтовой строки, и использование обычной str может привести к ошибке или неожидаемому поведению при попытке использования этой роли в QML.
  • Хранение данных модели в структуре, неэффективной для частых операций, выполняемых моделью (например, использование Python list для структуры данных, где более частыми операциями являются не добавление в конец, а вставка/удаление в произвольной позиции или частый поиск по ключу) — выбор внутренней структуры хранения данных модели (статья 456 алгоритмического цикла, применимая и к Python) должен учитывать реальный паттерн использования модели, а не выбираться автоматически как «очевидный» список без анализа.
  • Игнорирование производительности Python-реализации data() при очень частых вызовах (статья 494 QML-цикла про производительность, применимая и здесь) — поскольку Python в целом медленнее эквивалентного C++ кода (статья 507 этого цикла рассмотрит эту тему подробнее), производительность data(), вызываемого очень часто для каждого видимого делегата ListView, может стать заметным узким местом для моделей с очень большим количеством элементов или очень частым обновлением, и для таких случаев стоит явно профилировать и, при необходимости, рассмотреть оптимизацию (или, в крайнем случае, перенос самой модели в C++ при сохранении остальной логики на Python).
  • Несогласованность типов данных, возвращаемых data() для одной и той же роли при разных вызовах (например, иногда возвращается int, а иногда строка для одной и той же роли, в зависимости от пути выполнения кода) — в отличие от строго типизированного C++ кода, где такая несогласованность была бы обнаружена компилятором, динамическая типизация Python позволяет такой код «случайно скомпилироваться» без ошибки, но привести к неожидаемому поведению или ошибке уже во время выполнения именно в QML-стороне, ожидающей согласованный тип для конкретной роли во всех элементах модели.