Search code examples
pythonpyqtpyqt5qtreeview

Storing non-string value in QStandardItemModel


Here's an MRE:

import sys, datetime
from PyQt5 import QtWidgets, QtCore, QtGui

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setMinimumSize(1000, 800)
        self.main_splitter = QtWidgets.QSplitter(self)
        self.main_splitter.setOrientation(QtCore.Qt.Horizontal)
        self.setCentralWidget(self.main_splitter) 
        self.tree_view = MyTreeView(self.main_splitter)
        self.right_panel = QtWidgets.QFrame(self.main_splitter)
        self.right_panel.setStyleSheet("background-color: green");

class MyTreeView(QtWidgets.QTreeView):
    def __init__(self, *args):
        super().__init__(*args)
        self.setModel(MyTreeModel())
        self.setColumnWidth(1, 150)
        self.header().setStretchLastSection(False)
        self.header().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)

class MyTreeModel(QtGui.QStandardItemModel):
    def __init__(self, *args):
        super().__init__(*args)
        item0_0 = QtGui.QStandardItem('blip')
        item0_1 = QtGui.QStandardItem('2000-01-01')
        self.invisibleRootItem().appendRow([item0_0, item0_1])
        item1_0 = QtGui.QStandardItem('bubble')
        item1_1 = QtGui.QStandardItem('')
        self.invisibleRootItem().appendRow([item1_0, item1_1])
        
    def setData(self, index, value, role=QtCore.Qt.EditRole):
        original_value = value
        
        # if role == QtCore.Qt.EditRole:
        #     if index.column() == 1:
        #         if value.strip() == '':
        #             value = None 
        #         else:
        #             try:
        #                 value = datetime.date.fromisoformat(value)
        #             except ValueError as e:
        #                 print(f'**** value could not be parsed as date: |{value}| - value not changed')
        #                 return False
        
        return_val = super().setData(index, value, role)
        if role == QtCore.Qt.EditRole:
            item = self.itemFromIndex(index)
            assert item.text() == original_value, f'item.text() |{item.text()}| original_value |{original_value}|'
        return return_val
    
    # def data(self, index, role=QtCore.Qt.DisplayRole):
    #     value = super().data(index, role)
    #     if role == QtCore.Qt.DisplayRole:
    #         if index.column() == 1:
    #             value = '' if value == None else str(value)
    #     elif role == QtCore.Qt.EditRole:
    #         if index.column() == 1:
    #             if value == '':
    #                 value = None
    #     return value

app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()

app.exec_()

The second column here is a date column: only valid dates (or empty string) should be accepted.

If you uncomment the commented-out lines you will see what I tried to do: my idea was to store not a str but a datetime.date in the second column of the model. Modifying setData() also meant I had to modify data() (NB taking account of the fact that you may find a string initially, before any edit).

The idea is that whenever the user modifies a date (press F2 to edit in the 2nd column, enter a new valid date, or no date, press Enter), the value stored should be a datetime.date (or None). But to my surprise if I try to do this, this assert fails: the model data appears to have been updated OK by super().setData(index, value, role), but item.text(), confusingly, has not changed: although visibly the user can see that it has changed.

With these commented-out lines commented out, the assert does not fail.

What's the explanation for this? I find it difficult to believe that it is not recommended to store data other than strings in a QStandardItemModel, so I presume I am doing something wrong regarding the "updating" of the item.

I tried inserting the following (after super().setData(...)):

if role == QtCore.Qt.EditRole and index.column() == 1:
    item = self.itemFromIndex(index)
    item.setText(original_value)

... but this doesn't work: item.setText(...) triggers another call to setData() (with a string) and multiple confusing EditRole and DataRole calls to data(), which in fact deliver string values and None values.

I also surmise that the delegate may have a role to play here... by default the delegate's editor is a QLineEdit, and it appears that the delegate's method setModelData is responsible for calling the model's setData method.

However, by testing the value of item.text() both before and after calling super().setData(...) it is possible to ascertain that it is indeed that super() call which changes the text in the item. But only, apparently, if value is a string!

Until further notice, I assume that unless you completely re-implement the data storage mechanism here, you have to be content with storing only strings in a QStandardItemModel.


Solution

  • Instead of unnecessarily complicating things you could use QDate which has the same functionality as datetime.date and is handled natively by Qt.

    import sys
    from PyQt5 import QtWidgets, QtCore, QtGui
    
    
    DATE_FORMAT = "yyyy-MM-dd"
    
    
    class MyDelegate(QtWidgets.QStyledItemDelegate):
        def createEditor(self, parent, option, index):
            editor = super().createEditor(parent, option, index)
            if isinstance(editor, QtWidgets.QDateTimeEdit):
                editor.setDisplayFormat(DATE_FORMAT)
                editor.setCalendarPopup(True)
            return editor
    
    
    class MyTreeModel(QtGui.QStandardItemModel):
        def __init__(self, *args):
            super().__init__(*args)
            item0_0 = QtGui.QStandardItem("blip")
    
            dt0 = QtCore.QDate.fromString("2000-01-01", DATE_FORMAT)
            print(dt0.toPyDate(), dt0.toString(DATE_FORMAT))
            item0_1 = QtGui.QStandardItem()
            item0_1.setData(dt0, QtCore.Qt.DisplayRole)
            self.invisibleRootItem().appendRow([item0_0, item0_1])
            item1_0 = QtGui.QStandardItem("bubble")
            item1_1 = QtGui.QStandardItem()
            dt1 = QtCore.QDate()
            assert dt1.isNull()
            assert not dt1.isValid()
            item1_1.setData(dt1, QtCore.Qt.DisplayRole)
            self.invisibleRootItem().appendRow([item1_0, item1_1])
    
    
    class MyTreeView(QtWidgets.QTreeView):
        def __init__(self, *args):
            super().__init__(*args)
            model = MyTreeModel()
            self.setModel(model)
            self.setColumnWidth(1, 150)
            self.header().setStretchLastSection(False)
            self.header().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
            delegate = MyDelegate()
            self.setItemDelegate(delegate)
    
    
    class MainWindow(QtWidgets.QMainWindow):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.setMinimumSize(1000, 800)
            self.main_splitter = QtWidgets.QSplitter(self)
            self.main_splitter.setOrientation(QtCore.Qt.Horizontal)
            self.setCentralWidget(self.main_splitter)
            self.tree_view = MyTreeView(self.main_splitter)
            self.right_panel = QtWidgets.QFrame(self.main_splitter)
            self.right_panel.setStyleSheet("background-color: green")
    
    
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    
    app.exec_()