在 QStandardItemModel 中存储非字符串值

Storing non-string value in QStandardItemModel

这是一个 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_()

这里的第二列是日期列:只应接受有效日期(或空字符串)。

如果您取消对注释掉的行的注释,您将看到我尝试做的事情:我的想法是在模型的第二列中存储的不是 str,而是 datetime.date。修改 setData() 也意味着我必须修改 data()(请注意,在任何编辑之前,您最初可能会找到一个字符串)。

这个想法是,每当用户修改日期(按 F2 在第 2 列编辑,输入新的有效日期,或者没有日期,按 Enter),存储的值应该是 datetime.date (或 None)。但令我惊讶的是,如果我尝试这样做,这个 assert 会失败:模型数据似乎已被 super().setData(index, value, role) 正确更新,但令人困惑的是 item.text() 并没有改变:虽然明显用户可以看到它已经改变了。

将这些注释掉的行注释掉后,assert 不会失败。

这是怎么解释的?我发现很难相信不建议在 QStandardItemModel 中存储字符串以外的数据,所以我认为我在项目的“更新”方面做错了。

我尝试插入以下内容(在 super().setData(...) 之后):

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

...但这不起作用:item.setText(...) 触发对 setData() 的另一个调用(使用字符串)和多个令人困惑的 EditRoleDataRole 调用data(),实际上传递的是字符串值和 None 值。

我还推测委托人可能在这里发挥作用...默认情况下,委托人的编辑器是 QLineEdit,看来委托人的方法 setModelData 负责调用模型的 setData 方法。

但是,通过在 之前和调用 super().setData(...) 之后测试 item.text() 的值,可以确定确实 super() 调用更改项目中的文本。但显然,如果 value 是一个字符串!

除非另行通知,否则我假设除非您在此处完全重新实现数据存储机制,否则您必须满足于在 QStandardItemModel.

中仅存储字符串

您可以使用 QDate,而不是不必要地使事情复杂化,它具有与 datetime.date 相同的功能并且由 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_()

这是对 eyllanesc 出色回答的补充。我真的真的真的很想输入一个 空日期 !

事实上,我并不喜欢这个 QDateTimeEdit + 弹出式解决方案,所以我选择只为我的编辑器使用 QLineEdit,数据值为 QDate。空白字符串然后转换为空 QDate.

但我首先使用了 QDateEdit,并希望能够设置一个空日期值。在这个问题上有一些挫败感。可能有一个使用验证器的更优雅的解决方案,但这个虽然很老套,但似乎可以完成这项工作:

class MyDateEdit(QtWidgets.QDateEdit):
    def __init__(self, parent):
        super().__init__(parent)
        self.null_date = False
    
    def keyPressEvent(self, event):
        if event.matches(QtGui.QKeySequence.Delete):
            self.null_date = True
            press_return_key = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Enter, QtCore.Qt.NoModifier)
            QtWidgets.QApplication.sendEvent(self, press_return_key)
            return
        super().keyPressEvent(event)

class MyDelegate(QtWidgets.QStyledItemDelegate):
    def createEditor(self, parent, option, index):
        print(f'parent {parent} type {type(parent)}')
        editor = super().createEditor(parent, option, index)
        if isinstance(editor, QtWidgets.QDateTimeEdit):
            # use the subclass instead:
            editor = MyDateEdit(parent)
            editor.setDisplayFormat(DATE_FORMAT)
            editor.setCalendarPopup(True)
        return editor
    
    def setModelData(self, editor, model, index):
        if editor.null_date:
            # as far as I'm aware, this is pretty much all that setModelData does*
            model.setData(index, None)
        else:
            super().setModelData(editor, model, index)

开始编辑后按“删除”,编辑会话立即结束,在模型中将值设置为 None。添加一个确认消息框可能是件好事。

* source codesetModelData