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

QML из Python: QQmlApplicationEngine, передача объектов в QML

Введение

QML-цикл (статьи 479-498) разбирал интеграцию C++ и QML — в этой статье разберём тот же набор концепций (контекстные свойства, регистрация типов) применительно к Python, что особенно ценно для разработчиков, предпочитающих современный, декларативный Qt Quick UI, но желающих писать логику приложения на Python, а не на C++.

Концепция

QQmlApplicationEngine используется из Python синтаксически почти идентично C++ версии (статья 490 QML-цикла) — загрузка QML-файла через engine.load(), передача Python-объектов в QML через engine.rootContext().setContextProperty(), и регистрация Python-классов как QML-типов через декоратор @QmlElement (более современный, декларативный подход) или функцию qmlRegisterType() (аналогичную одноимённой C++ функции).

Пример кода: QML из Python

# Базовая загрузка QML из Python
import sys
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine

app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.load("main.qml")

if not engine.rootObjects():
    sys.exit(-1)  # явная проверка, что QML действительно успешно загрузился

sys.exit(app.exec())
# Передача Python-объекта в QML через контекстное свойство (аналогично статье 490 QML-цикла)
from PySide6.QtCore import QObject, Property, Signal

class AppSettings(QObject):
    usernameChanged = Signal(str)

    def __init__(self):
        super().__init__()
        self._username = "Гость"

    def getUsername(self) -> str:
        return self._username

    def setUsername(self, value: str):
        if self._username != value:
            self._username = value
            self.usernameChanged.emit(value)

    username = Property(str, getUsername, setUsername, notify=usernameChanged)


settings = AppSettings()
engine.rootContext().setContextProperty("appSettings", settings)
# В QML теперь доступно: Text { text: appSettings.username }
# Регистрация Python-класса как QML-типа через декоратор QmlElement (современный, декларативный подход)
from PySide6.QtQml import QmlElement, QmlSingleton

QML_IMPORT_NAME = "MyApp.Models"
QML_IMPORT_MAJOR_VERSION = 1

@QmlElement
class TaskItem(QObject):
    # ... объявление свойств через Property, аналогично примеру AppSettings выше ...
    pass

# После регистрации в QML: import MyApp.Models; TaskItem { ... }

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

Базовый пример загрузки QML показывает, что структура Python-кода для запуска QML-приложения почти полностью идентична C++ версии — основное синтаксическое отличие лишь в обычных питонических деталях (явный import, отсутствие точки с запятой), а сама логика (создание engine, загрузка файла, проверка rootObjects(), запуск exec()) совпадает по смыслу буквально построчно. Передача settings через setContextProperty("appSettings", settings) показывает практически идентичный паттерн контекстного свойства из статьи 490 QML-цикла — AppSettings, реализованный через Property/Signal (статья 502), становится доступен в QML точно так же, как и его C++ эквивалент, демонстрируя реальную взаимозаменяемость C++ и Python для этой части архитектуры Qt-приложения. Декоратор @QmlElement с предварительным объявлением QML_IMPORT_NAME/QML_IMPORT_MAJOR_VERSION показывает более современный, декларативный способ регистрации типа по сравнению с явным вызовом функции qmlRegisterType(), концептуально аналогичный системе QML_ELEMENT-макросов для C++-кода в современных версиях Qt.

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

  • Забытая проверка engine.rootObjects() после load(), что может скрыть факт неудачной загрузки QML-файла (синтаксическая ошибка в QML, отсутствующий файл) — без этой явной проверки приложение может просто завершиться без какого-либо видимого окна и без понятного сообщения об ошибке, что усложняет диагностику проблемы по сравнению с явной, информативной обработкой этого случая, как показано в базовом примере.
  • Преждевременное удаление Python-объекта, переданного как контекстное свойство, сборщиком мусора Python, аналогично подводному камню статьи 500 про виджеты — если единственная ссылка на settings была локальной переменной функции, вышедшей из области видимости после вызова setContextProperty, сборщик мусора Python может удалить этот объект, оставив QML-сцену с «висячей» ссылкой на уже несуществующий объект; объект должен сохраняться в переменной с достаточно долгим временем жизни (например, атрибутом самого приложения или класса-обёртки) на протяжении всего времени, пока QML-сцена может к нему обращаться.
  • Путаница между @QmlElement (более современный подход, требующий определённой минимальной версии PySide6) и более старым qmlRegisterType() — для проектов, использующих более старые версии PySide6, декоратор @QmlElement может быть недоступен, и в таком случае нужно использовать явный вызов qmlRegisterType(); смешивание обоих подходов без понимания их функциональной эквивалентности может создать путаницу при чтении и поддержке кода.
  • Игнорирование того, что Python и QML работают в рамках одного, общего событийного цикла Qt, и долгая, блокирующая операция в Python-коде, вызванном из QML (через Slot, статья 501), полностью блокирует также и весь QML-интерфейс на время этой операции — аналогично подводным камням блокирующих операций в основном цикле статей про многопоточность, длительные операции должны выполняться в отдельном потоке (статья 505) или асинхронно, а не блокировать основной поток, общий для и Python-логики, и отображения QML-интерфейса.