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")
}
}
See the comment from @musicamente for correct answer. Assigning the dialog
instance as an attribute of DialogManager
prevented the dialog
instance from being garbage collected, and it remains as an accessible component in the QML.
See corrected DialogManager
class below.
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