How to use multiple dataclass types

In How to use item model example, every item of the model had the same dataclass type and we could only change its data value. In this guide, we will go further to modify the dataclass type as well.

When the dataclass type changes, data widget for the item must also change. DataclassDelegate and DataclassMapper can be combined with DataclassStackedWidget or DataclassTabWidget to synchronize the widget with the data type.

Basic example

In this example, we create a widget which consists of:

  • DataclassTabWidget to display and choose the dataclass type

  • Buttons to change the model index

We first define the dataclasses.

from dataclasses import dataclass

@dataclass
class DataClass1:
    x: float
    y: bool

@dataclass
class DataClass2:
    x: float
    y: int

Note that the two dataclasses partially share their fields. When the data widget changes, values of the common fields in the model data will be updated to the new widget.

Then we construct the widget with basic API, and add the dataclass types.

from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton
from PySide6.QtCore import Signal
from dawiq import DataclassTabWidget, dataclass2Widget
import sys

app = QApplication(sys.argv)

class MyWidget(QWidget):

    toPrevious = Signal()
    toNext = Signal()

    def __init__(self, parent=None):
        super().__init__(parent)

        self.setLayout(QVBoxLayout())

        self.tabWidget = DataclassTabWidget()
        self.layout().addWidget(self.tabWidget)

        self.btn1 = QPushButton("Previous")
        self.btn2 = QPushButton("Next")
        self.layout().addWidget(self.btn1)
        self.layout().addWidget(self.btn2)

        self.btn1.clicked.connect(self.toPrevious)
        self.btn2.clicked.connect(self.toNext)

    def addDataclass(self, dcls):
        self.tabWidget.addDataWidget(dataclass2Widget(dcls), dcls, dcls.__name__)

myWidget = MyWidget()

for dcls in [DataClass1, DataClass2]:
    myWidget.addDataclass(dcls)

Now we construct a model with two items, each having different data types.

from PySide6.QtGui import QStandardItemModel, QStandardItem
from dawiq import DataclassDelegate

model = QStandardItemModel()
for dcls in [DataClass1, DataClass2]:
    item = QStandardItem()
    item.setData(dcls, role=DataclassDelegate.TypeRole)
    model.appendRow(item)

Finally we construct the delegate and the mapper, and set up the system.

from dawiq import DataclassDelegate, DataclassMapper

delegate = DataclassDelegate()
mapper = DataclassMapper()
mapper.setItemDelegate(delegate)
mapper.setModel(model)
myWidget.toPrevious.connect(mapper.toPrevious)
myWidget.toNext.connect(mapper.toNext)

mapper.addMapping(myWidget.tabWidget, 0)
mapper.setCurrentIndex(0)

Now let’s set the data and display the widget.

model.setData(model.index(0, 0), dict(x=1.0), delegate.DataRole)
model.setData(model.index(1, 0), dict(y=2), delegate.DataRole)

myWidget.show()
app.exec()
app.quit()
../_images/dcls-tab-example.jpg

Widget with dataclass tab widget

Try change the dataclass type, set the data and switch the model index.

Custom widget example

In this example, we will define a custom widget to select the dataclass instead of using DataclassTabWidget. The widget consists of:

  • QComboBox to select the dataclass type

  • DataclassStackedWidget to display the dataclass data

  • Buttons to change the model index

Dataclasses are same to Basic example, so we go directly to widget construction.

from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QComboBox, QPushButton
from PySide6.QtCore import Signal
from dawiq import DataclassStackedWidget, dataclass2Widget
import sys

app = QApplication(sys.argv)

class MyWidget(QWidget):

    toPrevious = Signal()
    toNext = Signal()

    def __init__(self, parent=None):
        super().__init__(parent)

        self.setLayout(QVBoxLayout())

        self.comboBox = QComboBox()
        self.comboBox.setPlaceholderText("Select dataclass type")
        self.layout().addWidget(self.comboBox)

        self.stackedWidget = DataclassStackedWidget()
        self.layout().addWidget(self.stackedWidget)

        self.btn1 = QPushButton("Previous")
        self.btn2 = QPushButton("Next")
        self.layout().addWidget(self.btn1)
        self.layout().addWidget(self.btn2)

        self.btn1.clicked.connect(self.toPrevious)
        self.btn2.clicked.connect(self.toNext)

    def addDataclass(self, dcls):
        self.comboBox.addItem(dcls.__name__, dcls)
        self.stackedWidget.addDataWidget(dataclass2Widget(dcls), dcls)

myWidget = MyWidget()

for dcls in [DataClass1, DataClass2]:
    myWidget.addDataclass(dcls)

We need to define a delegate so that it can synchronize DataclassStackedWidget and QComboBox.

from dawiq import DataclassDelegate

class MyDelegate(DataclassDelegate):

    def setModelData(self, editor, model, index):
        if isinstance(editor, MyWidget):
            dcls = editor.comboBox.currentData()
            model.setData(index, dcls, role=self.TypeRole)
            self.setModelData(editor.stackedWidget, model, index)
        super().setModelData(editor, model, index)

    def setEditorData(self, editor, index):
        if isinstance(editor, MyWidget):
            modeldata = index.data(role=self.TypeRole)
            editor.comboBox.setCurrentIndex(editor.comboBox.findData(modeldata))
            self.setEditorData(editor.stackedWidget, index)
        super().setEditorData(editor, index)

delegate = MyDelegate()

Now we construct a model with two items, each having different data types.

from PySide6.QtGui import QStandardItemModel, QStandardItem

model = QStandardItemModel()
for dcls in [DataClass1, DataClass2]:
    item = QStandardItem()
    item.setData(dcls, role=delegate.TypeRole)
    model.appendRow(item)

We also need to define a mapper so that whenever the combo box index changes the model will get updated too.

from dawiq import DataclassMapper

class MyMapper(DataclassMapper):
    def addMapping(self, widget, section, propertyname=b""):
        if isinstance(widget, MyWidget):
            widget.comboBox.activated.connect(self.submit)
            widget.stackedWidget.currentDataEdited.connect(self.submit)
        super().addMapping(widget, section, propertyname)

mapper = MyMapper()

Finally we set up the system.

mapper.setItemDelegate(delegate)
mapper.setModel(model)
myWidget.toPrevious.connect(mapper.toPrevious)
myWidget.toNext.connect(mapper.toNext)

mapper.addMapping(myWidget, 0)
mapper.setCurrentIndex(0)

Now let’s set the data and display the widget.

model.setData(model.index(0, 0), dict(x=1.0), delegate.DataRole)
model.setData(model.index(1, 0), dict(y=2), delegate.DataRole)

myWidget.show()
app.exec()
app.quit()
../_images/dcls-custom-example.jpg

Custom widget with dataclass selection