Введение
Статья 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, соответствующий Pythonbytes) требует именно байтовой строки, и использование обычнойstrможет привести к ошибке или неожидаемому поведению при попытке использования этой роли в QML. - Хранение данных модели в структуре, неэффективной для частых операций, выполняемых моделью (например, использование Python
listдля структуры данных, где более частыми операциями являются не добавление в конец, а вставка/удаление в произвольной позиции или частый поиск по ключу) — выбор внутренней структуры хранения данных модели (статья 456 алгоритмического цикла, применимая и к Python) должен учитывать реальный паттерн использования модели, а не выбираться автоматически как «очевидный» список без анализа. - Игнорирование производительности Python-реализации
data()при очень частых вызовах (статья 494 QML-цикла про производительность, применимая и здесь) — поскольку Python в целом медленнее эквивалентного C++ кода (статья 507 этого цикла рассмотрит эту тему подробнее), производительностьdata(), вызываемого очень часто для каждого видимого делегатаListView, может стать заметным узким местом для моделей с очень большим количеством элементов или очень частым обновлением, и для таких случаев стоит явно профилировать и, при необходимости, рассмотреть оптимизацию (или, в крайнем случае, перенос самой модели в C++ при сохранении остальной логики на Python). - Несогласованность типов данных, возвращаемых
data()для одной и той же роли при разных вызовах (например, иногда возвращаетсяint, а иногда строка для одной и той же роли, в зависимости от пути выполнения кода) — в отличие от строго типизированного C++ кода, где такая несогласованность была бы обнаружена компилятором, динамическая типизация Python позволяет такой код «случайно скомпилироваться» без ошибки, но привести к неожидаемому поведению или ошибке уже во время выполнения именно в QML-стороне, ожидающей согласованный тип для конкретной роли во всех элементах модели.