我正在开发一个用 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")
}
}
请参阅 @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