QML 和 PySide6“TypeError:无法读取 null 的属性‘x’”

问题描述 投票:0回答:1

我正在开发一个用 PySide6 编写的应用程序前端,它在大部分 GUI 中使用 Qt Widgets,但我正在尝试添加一些使用从单独后端发送的数据生成的 QML 对话框。

尝试启动 QML 文件时,遇到以下错误:

file:///C:/Users/user/dev/qml_dialogs/date_range_dialog/DateRangeDialog.qml:28: TypeError: Cannot read property 'width' of null
file:///C:/Users/user/dev/qml_dialogs/date_range_dialog/DateRangeDialog.qml:71: TypeError: Cannot read property 'endDateModel' of null
file:///C:/Users/user/dev/qml_dialogs/date_range_dialog/DateRangeDialog.qml:52: TypeError: Cannot read property 'startDateModel' of null
file:///C:/Users/user/dev/qml_dialogs/date_range_dialog/DateRangeDialog.qml:31: TypeError: Cannot read property 'height' of null

我的 QML

ApplicationWindow
中的组合框仅显示存储在
QStringListModel
中对应于
dateRangeDialog.startDateModel
的第一个字符串项,在 QML 中引用为
dialog.startDateModel

遵循 https://doc.qt.io/qtforpython-6/PySide6/QtCore/Property.html https://doc.qt.io/qt-6/qtqml-cppintegration- 中的一些文档后hidecppstate.html 为了获得类和属性的帮助,我创建了一个

DateRangeDialog
类以将属性公开给 QML。

我尝试使用

DialogManager
来封装
QQmlApplicationEngine
,设置根上下文,并加载QML文件,因为前端的架构不允许在主函数中发生这种情况。

下面是一个最小的可重现示例,使用

DialogManager
DateRangeDialog
和 DateRangeDialog.qml 文件(全部位于同一目录中)以及 style.qrc 文件、__init__.py 等。

关于如何重构这些类,以便在执行时可以从 QML 文件访问

height
width
和日期范围属性,有什么建议吗?

main.py

import sys

from PySide6.QtWidgets import QApplication

from dialog_manager import DialogManager

import style_rc


def main() -> None:
    app = QApplication(sys.argv)
    dialogManager = DialogManager()
    dialogManager.loadDialog(
            name="DateRangeDialog",
            width=800, 
            height=180, 
            startDates=["14/08/23", "15/08/23"], 
            endDates=["15/08/23"]
    )

    sys.exit(app.exec())


if __name__ == "__main__":
    main()

dialog_manager.py

from pathlib import Path

from PySide6.QtCore import QObject, QStringListModel
from PySide6.QtQml import QQmlApplicationEngine

from date_range_dialog import DateRangeDialog


class DialogManager(QObject):
    _engine: QQmlApplicationEngine

    def __init__(self):
        QObject.__init__(self)
        self._engine = QQmlApplicationEngine()

    def loadDialog(
            self,
            name: str,
            width: float,
            height: float,
            startDates: list[str],
            endDates: list[str]
    ):
        dialog = DateRangeDialog()
        dialog.width = width
        dialog.height = height
        dialog.startDateModel = QStringListModel(startDates)
        dialog.endDateModel = QStringListModel(endDates)

        self._setRootContext(dialog)
        self._loadQmlFile(name)

        root = self._getQQmlEngineRoot()
        try: 
            root.outputDateRange.connect(dialog.outputDateRange)
        except AttributeError as e:
            print(e)

    def _setRootContext(self, dialog: DateRangeDialog):
        self._engine.rootContext().setContextProperty("dialog", dialog)

    def _loadQmlFile(self, qmlName: str) -> None:
        qmlFile = Path(__file__).parent / f"{qmlName}.qml"
        self._engine.load(qmlFile)

    def _getQQmlEngineRoot(self) -> QObject:
        if not self._engine.rootObjects():
            raise AttributeError("Root objects for QQmlApplicationEngine not found")

        root = QObject()
        for rootObject in self._engine.rootObjects():
            if rootObject.inherits("QWindow"): 
                root = rootObject
                break

        return root
       

日期范围对话框.py

rom PySide6.QtCore import Property, QObject, QStringListModel, Signal


class DateRangeDialog(QObject):
    widthChanged = Signal(float)
    heightChanged = Signal(float)
    startDateModelChanged = Signal(QObject)
    endDateModelChanged = Signal(QObject)

    def __init__(self):
        QObject.__init__(self)
        self._width = 800 
        self._height = 180
        self._startDateModel = QStringListModel()
        self._endDateModel = QStringListModel()

    def outputDateRange(self, start_date: str, end_date: str) -> None:
        print(f"Start Date: {start_date}, End Date: {end_date}")
    
    @Property(float, notify=widthChanged)
    def width(self) -> float:
        return self._width

    @width.setter
    def width(self, width: float) -> None:
        if self._width != width:
            self._width = width
            self.widthChanged.emit(width)

    @Property(float, notify=heightChanged)
    def height(self) -> float:
        return self._height

    @height.setter
    def height(self, height: float) -> None:
        if self._height != height:
            self._height = height
            self.heightChanged.emit(height)

    @Property(QObject, notify=startDateModelChanged)
    def startDateModel(self):
        return self._startDateModel

    @startDateModel.setter
    def startDateModel(self, startDateModel: QStringListModel) -> None:
        if self._startDateModel != startDateModel:
            self._startDateModel = startDateModel
            self.startDateModelChanged.emit(startDateModel)

    @Property(QObject, notify=endDateModelChanged)
    def endDateModel(self):
        return self._endDateModel

    @endDateModel.setter
    def endDateModel(self, endDateModel: QStringListModel) -> None:
        if self._endDateModel != endDateModel:
            self._endDateModel = endDateModel
            self.endDateModelChanged.emit(endDateModel)

DateRangeDialog.qml

import QtQuick
import QtQuick.Controls
import QtQuick.Window

ApplicationWindow {
    id: root

    signal buttonClicked(string buttonText);
    signal outputDateRange(string startDate, string endDate);

    Component.onCompleted: { 
        console.log("Initialised DateRangeDialog")
        root.buttonClicked.connect(closeDialog); 
    }
    function closeDialog(buttonText) {
        console.log(buttonText + " clicked");

        if (buttonText === "OK") {
            var startDate = startDateComboBox.currentText;
            var endDate = endDateComboBox.currentText;
            root.outputDateRange(startDate, endDate);
        }

        close();
    }

    visible: true
    width: dialog.width
    minimumWidth: width
    maximumWidth: width
    height: dialog.height
    minimumHeight: height
    maximumHeight: height
    flags: Qt.Dialog
    title: qsTr("Enter Report Range")

    Label {
        id: startDateLabel

        anchors.right: startDateComboBox.left
        anchors.rightMargin: parent.width / 20
        anchors.verticalCenter: startDateComboBox.verticalCenter
        text: qsTr("Start Date: ")
    }
    ComboBox {
        id: startDateComboBox

        anchors.right: parent.horizontalCenter
        anchors.rightMargin: parent.width / 40
        anchors.bottom: parent.verticalCenter
        anchors.bottomMargin: parent.height / 8
        model: dialog.startDateModel
        textRole: "display"
    }

    Label {
        id: endDateLabel

        anchors.left: parent.horizontalCenter
        anchors.leftMargin: parent.width / 40
        anchors.verticalCenter: endDateComboBox.verticalCenter
        text: qsTr("End Date: ")
    }
    ComboBox {
        id: endDateComboBox

        anchors.left: endDateLabel.right
        anchors.leftMargin: parent.width / 20
        anchors.bottom: parent.verticalCenter
        anchors.bottomMargin: parent.height / 8
        model: dialog.endDateModel
        textRole: "display"
    }

    Button {
        id: okButton

        onClicked: root.buttonClicked(text);

        anchors.right: parent.horizontalCenter
        anchors.rightMargin: parent.width / 40
        anchors.top: parent.verticalCenter
        anchors.topMargin: parent.height / 6
        text: qsTr("OK")
    }

    Button {
        id: cancelButton

        onClicked: root.buttonClicked(text);

        anchors.left: parent.horizontalCenter
        anchors.leftMargin: parent.width / 40
        anchors.top: parent.verticalCenter
        anchors.topMargin: parent.height / 6
        text: qsTr("Cancel")
    }
}
python-3.x qml pyside6 qt6 qqmlapplicationengine
1个回答
0
投票

请参阅 @musicamente 的评论以获得正确答案。将

dialog
实例分配为
DialogManager
的属性可防止
dialog
实例被垃圾回收,并且它仍然作为 QML 中的可访问组件。

请参阅下面更正的

DialogManager
课程。

dialog_manager.py

from pathlib import Path

from PySide6.QtCore import QObject, QStringListModel
from PySide6.QtQml import QQmlApplicationEngine

from date_range_dialog import DateRangeDialog


class DialogManager(QObject):
    _engine: QQmlApplicationEngine
    _dialog: DateRangeDialog

    def __init__(self):
        QObject.__init__(self)
        self._engine = QQmlApplicationEngine()

    def loadDialog(
            self,
            name: str,
            width: float,
            height: float,
            title: str,
            startDates: list[str],
            endDates: list[str]
    ):
        self.dialog = DateRangeDialog()
        self.dialog.width = width
        self.dialog.height = height
        self.dialog.title = title
        self.dialog.startDateModel = QStringListModel(startDates)
        self.dialog.endDateModel = QStringListModel(endDates)

        self._setRootContext(self.dialog)
        self._loadQmlFile(name)

        root = self._getQQmlEngineRoot()
        try: 
            root.outputDateRange.connect(self.dialog.outputDateRange)
        except AttributeError as e:
            print(e)

    def _setRootContext(self, dialog: DateRangeDialog):
        self._engine.rootContext().setContextProperty("dialog", dialog)

    def _loadQmlFile(self, qmlName: str) -> None:
        qmlFile = Path(__file__).parent / f"{qmlName}.qml"
        self._engine.load(qmlFile)

    def _getQQmlEngineRoot(self) -> QObject:
        if not self._engine.rootObjects():
            raise AttributeError("Root objects for QQmlApplicationEngine not found")

        root = QObject()
        for rootObject in self._engine.rootObjects():
            if rootObject.inherits("QWindow"): 
                root = rootObject
                break

        return root

© www.soinside.com 2019 - 2024. All rights reserved.