I am working on an application front-end written in PySide6 that uses Qt Widgets for most of the GUI, but I am trying to add some QML dialogs generated using data sent from a separate back-end.
When trying to launch a QML file, I encounter the following errors:
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
The combo boxes in my QML ApplicationWindow
only show the first of the string items stored in the QStringListModel
corresponding to dateRangeDialog.startDateModel
, which is referenced in QML as dialog.startDateModel
.
After following some of the documentation from https://doc.qt.io/qtforpython-6/PySide6/QtCore/Property.html https://doc.qt.io/qt-6/qtqml-cppintegration-exposecppstate.html for help with the classes and properties, I created a DateRangeDialog
class to expose the properties to the QML.
I am trying to use the DialogManager
to encapsulate the QQmlApplicationEngine
, set the root context, and load the QML file, since the architecture of the front-end doesn't allow for this to happen in the main function.
Below is a minimal reproducible example using the DialogManager
, DateRangeDialog
, and a DateRangeDialog.qml file, all in the same directory, along with the style.qrc file, __init__.py, etc.
Any advice on how to refactor these classes so that the height
, width
, and date range properties are accessible from the QML file upon execution?
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
date_range_dialog.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")
}
}