Which model apply to GUI app

Hello :grinning:,
I got GUI application dcspy and I get to point I need serious refactor and move to MVC. I get some reading and research but I’m a bit lost and I do not what go to dead-end street.
I’m try find-out which model to use, so my application have bunch of settings like here:

This straightforward just use some dict() to save and load value in/from GUI.

But on another tab:

I need read 10-15MB of JSON and parse as Pydantic models to fill-up
Spec and Aircraft dropdowns
https:// ibb .co/hdzpZpk

Then values from Spec can be selected and assign to keys in table, like:
https:// ibb .co/3RH9GTr

However every selected item in table dropdown can be further tune and configure:
https:// ibb .co/1ryrr1J
and those settings will be store in disk as YAML file like:

G9_M3: HOT_MIC_SW DEC
G22_M1: IFF_M4_CODE_SW CYCLE 1 2
G23_M2: IFF_M4_REPLY_SW CYCLE 1 2

And of course load during app start

So whole app model (in terms of MVC) need store app settings and all Pydantic data/models read from JSON files.

I just thinking try to implement some QTreeView but not sure is good idea, or maybe stor everything in local sqlite db and use QSqlTableModel

Any guidance or tips would be appreciated.

Hi @Michal_Plichta welcome to the forum & congratulations on reaching that point in your project where you realize you should have started with MVC all along. It happens to us all :wink:

As you probably know, Qt comes with a bunch of built in “model views” which can be use to display data in lists, dropdowns and tables. But for general UI use you’re on your own. I’ll cover both cases below.

Standard UI MVC

Use a dictionary to store the actual data. You can derive this from UserDict to define some custom behaviors – for example, adding signals to fire any time a value is updated.


class DataModelSignals(QObject):
    # Emit an "updated" signal when a property changes.
    updated = pyqtSignal()


class DataModel(UserDict):

    def __init__(self, *args, **kwargs):
        self.signals = DataModelSignals()
        super().__init__(*args, **kwargs)

    def __setitem__(self, key, value):
        previous = self.get(key) # Get the existing value.
        super().__setitem__(key, value) # Set the value.
        if value != previous: # There is a change.
            self.signals.updated.emit() # Emit the signal.
            print(self) # Show the current state.

model = DataModel(
    name="Johnina Smith",
    age=10,
    favorite_icecream='Vanilla',
    disable_details=False,
)

Then in your UI you receive signals from widgets that indicate they are updated & and use model['name_of_value'] to set the new value. You can do clever things here, like using the objectName to link to the key in the dictionary if you’re so inclined.

The model will only emit the updated signal if the value has changed. You connect this .updated signal to your UI, through an def update_ui(self) method. This reads the values from the model & applies them to the UI – all of them, because setting the same value that is already shown has no cost.

That way you can hold all your UI update logic in a single place, making it easy to add complex logic (like disabling/enabling bits of hte UI depending on config state).

The more complicated stuff

To show the table I would go with a QTableView and use a combobox delegate as the editor component for the cell. That allows you to customize what is shown in the dropdown and still benefit from using a model to store the data.

Something like the following

import sys

from PySide6 import QtCore, QtWidgets
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QComboBox, QItemDelegate


class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, data):
        super(TableModel, self).__init__()
        self._data = data

    def data(self, index, role):
        if role == Qt.ItemDataRole.DisplayRole:
            value = self._data[index.row()][index.column()]
            return str(value)

        if role == Qt.ItemDataRole.EditRole:
            value = self._data[index.row()][index.column()]
            return value

    def setData(self, index, value, role):
        if role == Qt.ItemDataRole.EditRole:
            self._data[index.row()][index.column()] = value
            return True
        return False

    def flags(self, index):
        flags = (
            Qt.ItemFlag.ItemIsEnabled
            | Qt.ItemFlag.ItemIsSelectable
            | Qt.ItemFlag.ItemIsEditable
        )
        return flags

    def rowCount(self, index):
        return len(self._data)

    def columnCount(self, index):
        return len(self._data[0])


class ComboDelegate(QItemDelegate):
    """
    Add a QComboBox in every cell of the table.
    """

    def __init__(self, parent, values):
        super(ComboDelegate, self).__init__(parent)
        self.model = None
        self.values = list(set(values))  # Unique values.

    def createEditor(self, parent, option, index):
        combobox = QComboBox(parent)
        combobox.addItems(self.values)
        return combobox

    def setEditorData(self, editor, index):
        text = index.model().data(index, Qt.EditRole)
        print("Update the editor from the model:", text)
        if index:
            editor.setCurrentText(text)

    def setModelData(self, editor, model, index):
        print("Updating the model model:", editor, model, index, editor.currentText())
        model.setData(index, editor.currentText(), Qt.ItemDataRole.EditRole)


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        combovalues = ["", "HOT_MIC_SW", "IFF_M4_CODE_SW", "IFF_M4_REPLY_SW"]

        self.table = QtWidgets.QTableView()

        self.setCentralWidget(self.table)

        combodelegate = ComboDelegate(self, combovalues)        
        self.table.setItemDelegate(combodelegate)

        self.model = TableModel(
            [
                ["", "", "", "", ""],
                ["", "", "", "", ""],
                ["", "", "", "", ""],
                ["", "", "", "", ""],
            ]
        )
        self.table.setModel(self.model)


app = QtWidgets.QApplication(sys.argv)

window = MainWindow()
window.show()
app.exec()

Which gives you a window like this, where each cell can be selected using a combo dropdown.

image

For the additional details I would add a sidebar, with the options etc. shown in it. This can react to the change in currently selected cell in the view & the current value in the dropdown.

In case it isn’t obvious you can store this information in the same model as that used for the table view — in the example above the model is a list of lists containing strings (from the selected values). But the model can contain anything, as the “values” e.g. dictionaries, objects, etc. and you can store your data however you like inside that. All you need to ensure is that the model can read/write to the value to display in the table, handled through the data/setData methods.

I hope this helps. Let me know if you have any questions, I realise it’s pretty complicated stuff.

1 Like

Thx for reply I will study your answer and back with question and comments. BTW… I kind of love your book!

1 Like

Ok Standard UI MVC is nice and clear, I will incorporate in my codebase. Pretty straightforward.

With TableModel() is quite clear. I like solution with ComboDelegate() never knew such technic in Qt. But of course got few questions/ideas:

  • to activate cell I need double click on cell, is there any way to overcome this, single click or start rolling mouse wheel to scroll options in combobox. Just curiosity this is a not big deal.
  • In my real world example some of table cells are disabled, how can I do it. See screenshoot.

Can you extend your example to:

  • add QDockWidget() with ie. QButtonGroup() with 3 QRadioButton()
# pseudo code
rb_values = {"HOT_MIC_SW": [1, 2, 3], "IFF_M4_CODE_SW": [2, 3], "IFF_M4_REPLY_SW": [1, 3]}
self.bg_rb = QButtonGroup(self)
self.rb_1 = QRadioButton()
self.rb_2 = QRadioButton()
self.rb_3 = QRadioButton()
self.bg_rb.addButton(self.rb_1)
self.bg_rb.addButton(self.rb_2)
self.bg_rb.addButton(self.rb_3)

# at start disable all radiobuttons
for b in self.bg_rb.buttons():
    b.setDisabled(True)

for combo_txt, rbs in rb_values:
    if <combo_widget> == combo_txt:
        for rb in rbs:
            getattr(self, f'rb_{rb}').setEnabled(True)
  • when current value in table combobox is changed(or enter pressed) it should enable correct radio buttons at dock
  • user can select any of enabled radio button and it should be keep (I use dict of dicts), any table cell can have some value assign (from dropdown) and this value can have (here one of 1, 2 , 3 value) assign.
  • later when this cell will be selected it should set radio buttons accordantly.

It would be a real help when you could extend your example with this user case!

Anyway do you see Aircraft dropdown list (at screenshoot) select some values it will chnage / rebuild combo_values and rb_values

And another note, I need build table based on user profile, just some snippet of code ( to get idea):

def _generate_table(self) -> None:
    ctrl_list_without_sep = [item for item in self.ctrl_list if item and CTRL_LIST_SEPARATOR not in item]
    for row in range(0, self.device.rows.total):
        for col in range(0, self.device.cols):
            self._make_combo_with_completer_at(row, col, ctrl_list_without_sep)

def _make_combo_with_completer_at(self, row: int, col: int, ctrl_list_no_sep: list[str]) -> None:
    key = self.device.get_key_at(row=row, col=col)
    if col == 0 or row < self.device.no_g_keys:
        completer = QCompleter(ctrl_list_no_sep)
        completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
        completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
        completer.setFilterMode(Qt.MatchFlag.MatchContains)
        completer.setMaxVisibleItems(self._completer_items)
        completer.setModelSorting(QCompleter.ModelSorting.CaseInsensitivelySortedModel)

        combo = QComboBox()
        combo.setEditable(True)
        combo.addItems(self.ctrl_list)
        combo.setCompleter(completer)
        self._disable_items_with(text=CTRL_LIST_SEPARATOR, widget=combo)
        self.tw_gkeys.setCellWidget(row, col, combo)
        try:
            identifier = self.input_reqs[self.current_plane][str(key)].identifier
        except KeyError:
            identifier = ''
        combo.setCurrentText(identifier)
        combo.editTextChanged.connect(partial(self._cell_ctrl_content_changed, widget=combo, row=row, col=col))
    else:
        combo = QComboBox()
        combo.setDisabled(True)
        self.tw_gkeys.setCellWidget(row, col, combo)
    combo.setStyleSheet(self._get_style_for_combobox(key=key, fg='black'))

    @staticmethod
    def _get_style_for_combobox(key: AnyButton, fg: str) -> str:
        bg = ''
        if isinstance(key, Gkey):
            bg = 'lightgreen'
        elif isinstance(key, MouseButton):
            bg = 'lightyellow'
        elif isinstance(key, LcdButton):
            bg = 'lightblue'
        return f'QComboBox{{color: {fg};background-color: {bg};}} QComboBox QAbstractItemView {{background-color: {bg};}}'

Big thanks in advanced!

PS. I would love to get even deeper knowledge of Qt beyond your book, If you consider write part 2 "Advanced Qt/Pyside6 stuff… " count on me as customer :slight_smile:

@martin If you find out a time to reply I would be appreciated.