简体   繁体   English

pyqt QAbstractItemDelegate 在 QListView 中显示不正确

[英]pyqt QAbstractItemDelegate not displaying correctly in QListView

I'm trying to get my head round how Delegates work in pyqt, and wrote the following code to try them.我试图了解代理在 pyqt 中的工作方式,并编写了以下代码来尝试它们。 But I can't work out why the paint method doesn't seem to line up the NameAge widget in the items correctly.但我无法弄清楚为什么 paint 方法似乎没有正确排列项目中的 NameAge 小部件。 can anyone advise?谁能建议?

import sys

from PyQt5.QtCore import (
    Qt, QAbstractListModel, QModelIndex)
from PyQt5.QtWidgets import (
    QApplication, QWidget, QListView, QAbstractItemDelegate,
    QHBoxLayout, QLineEdit, QSpinBox, QVBoxLayout)


class NameAge(QWidget):
    def __init__(self, name='', age=0, parent=None):
        super().__init__(parent)

        self.name_edit = QLineEdit(name)
        self.age_spinbox = QSpinBox()
        self.age_spinbox.setValue(age)

        layout = QHBoxLayout()
        layout.addWidget(self.name_edit)
        layout.addWidget(self.age_spinbox)

        self.setLayout(layout)


class NameAgeModel(QAbstractListModel):
    def __init__(self, data, parent=None):
        super().__init__(parent)
        self.data = data

    def rowCount(self, parent=QModelIndex()):
        return len(self.data)

    def data(self, index, role=Qt.DisplayRole):
        if role == Qt.DisplayRole:
            return self.data[index.row()]
        elif role == Qt.EditRole:
            return self.data[index.row()]
        
    def setData(self, index, value, role):
        if role == Qt.EditRole:
            self.data[index.row()] = value
            self.dataChanged.emit(index, index)
            return True
        return False

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


class NameAgeDelegate(QAbstractItemDelegate):
    def setEditorData(self, editor, index):
        name, age = index.data()
        editor.name_edit.setText(name)
        editor.age_spinbox.setValue(age)

    def setModelData(self, editor, model, index):
        name = editor.name_edit.text()
        age = editor.age_spinbox.value()
        model.setData(index, (name, age), Qt.EditRole)

    def createEditor(self, parent, option, index):
        name, age = index.data(Qt.DisplayRole)
        name_age = NameAge(name, age, self.parent())
        name_age.setGeometry(option.rect)
        return name_age

    def paint(self, painter, option, index):
        name, age = index.data(Qt.DisplayRole)
        name_age = NameAge(name, age, self.parent())
        name_age.setGeometry(option.rect)
        name_age.render(painter, option.rect.topLeft())

    def sizeHint(self, option, index):
        name, age = index.data(Qt.DisplayRole)
        name_age = NameAge(name, age, self.parent())
        return name_age.sizeHint()

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)


class MainWidget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        layout = QVBoxLayout()
        self.setLayout(layout)

        self.list_view = QListView()
        self.list_view.setItemDelegate(NameAgeDelegate(self.list_view))
        self.list_view.setModel(NameAgeModel(
            [('Mark', 38), ('John', 30), ('Jane', 25)]))

        layout.addWidget(self.list_view)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    main_widget = MainWidget()
    main_widget.show()
    sys.exit(app.exec_())

问题截图

any advice would be appreciated Sorry as can't find many examples of people trying to do this.任何建议将不胜感激抱歉,因为找不到很多尝试这样做的人的例子。

Thanks, Mark谢谢,马克

The specific issue is that you're using the targetOffset along with the geometry, while you should only set the size and translate the painter instead:具体问题是您将targetOffset与几何一起使用,而您应该只设置大小并平移画家:

        name_age.resize(option.rect.size())
        painter.save()
        painter.translate(option.rect.topLeft())
        name_age.render(painter)
        painter.restore()

Unfortunately, this is only the correct solution for the wrong problem, as you're having a much bigger issue: you're constantly creating new instances of NameAge , which is terribly wrong for two reasons:不幸的是,这只是错误问题的正确解决方案,因为您遇到了一个更大的问题:您不断创建NameAge的新实例,这是非常错误的,原因有两个:

  1. calls to paint() and sizeHint() are extremely frequent, meaning that you will be creating new widgets hundred (or thousands) of times, even when just hovering items with the mouse or resizing the view;paint()sizeHint()的调用非常频繁,这意味着您将创建数百(或数千)次新的小部件,即使只是用鼠标悬停项目或调整视图大小时也是如此;
  2. creating widgets with a parent means that they will exist as long as the parent does;使用父级创建小部件意味着只要父级存在,它们就会存在; with your code, you can easily get thousands of unused instances;使用您的代码,您可以轻松获得数千个未使用的实例;

Another issue your code has is that you're creating the editor with the wrong parent, self.parent() .您的代码存在的另一个问题是您正在使用错误的父self.parent()创建编辑器。 This is wrong for two reasons:这是错误的,原因有二:

  • createEditor() already provides the parent, which is the view's viewport (not the view;); createEditor()已经提供了父级,它是视图的视口(不是视图;);
  • the parent argument of the delegate constructor is optional and could even not be a QWidget but a plain QObject (for instance, the QApplication);委托构造函数的父参数是可选的,甚至可以不是QWidget 而是普通的 QObject(例如 QApplication); this means that:这意味着:
    • if the parent of the delegate is a QObject and not a QWidget, your code will crash as QWidgets only accept another QWidget or None as parent;如果委托的父对象是 QObject 而不是 QWidget,您的代码将崩溃,因为 QWidgets 只接受另一个 QWidget 或None作为父对象;
    • if the parent is None , the editor will appear as a new top level window;如果父级为None ,编辑器将显示为新的顶级窗口;

Now, avoiding all the above depends on your needs.现在,避免上述所有情况取决于您的需要。

I'm suspecting that you just want the editor to be shown even when the user isn't editing it, so the proper solution is to use a persistent editor , which can be achieved by calling openPersistentEditor() .我怀疑您只是希望即使用户没有编辑它也能显示编辑器,因此正确的解决方案是使用持久性编辑器,这可以通过调用openPersistentEditor()来实现。

With a simple view as QListView, this is quite simple (a QTableView or QTreeView might need different delegates for each column): just ensure that the view opens a persistent editor every time a new row is added to the model, which can be easily done by connecting the rowsInserted signal to a function that calls openPersistentEditor() .对于像 QListView 这样的简单视图,这非常简单(QTableView 或 QTreeView 可能需要对每一列使用不同的委托):只需确保每次向模型添加新行时视图都打开持久性编辑器,这很容易完成通过将rowsInserted信号连接到调用openPersistentEditor()的函数。

Then, in order to get proper behavior (including size hints), you have to keep a reference of the editors based on the index.然后,为了获得正确的行为(包括大小提示),您必须根据索引保留对编辑器的引用。 Note that basic indexes (QModelIndex instances) are "volatile", as the documentation notes :请注意,基本索引(QModelIndex 实例)是“易变的”,如文档所述:

Note: Model indexes should be used immediately and then discarded.注意:模型索引应立即使用,然后丢弃。 You should not rely on indexes to remain valid after calling model functions that change the structure of the model or delete items.在调用更改模型结构或删除项目的模型函数后,您不应依赖索引保持有效。 If you need to keep a model index over time use a QPersistentModelIndex.如果您需要随时间保留模型索引,请使用 QPersistentModelIndex。

So, in order to have proper references between indexes and editors, you must use QPersistentModelIndex in a dictionary, and eventually remove the key/value pair in case the editor is destroyed.因此,为了在索引和编辑器之间有正确的引用,您必须在字典中使用QPersistentModelIndex ,并最终删除键/值对以防编辑器被破坏。

Also note that the editor might change the default size hint of an item (especially if created after the new index).另请注意,编辑器可能会更改项目的默认大小提示(尤其是在新索引之后创建的项目)。 To do so, you have to emitsizeHintChanged whenever the editor is created.为此,您必须在创建编辑器时发出sizeHintChanged

Another important aspect to be aware of is that QObjects support the user property , which is the default property of any object.另一个需要注意的重要方面是 QObjects 支持用户属性,这是任何对象的默认属性。 This property is used within Qt to easily set the most important "aspect" of that object.该属性在 Qt 中用于轻松设置该对象最重要的“方面”。

For instance, QLineEdit has its text property set as user one, while QSpinBox uses value .例如,QLineEdit 将其text属性设置为用户一,而 QSpinBox 使用value Qt delegates automatically set and get those "user properties" and apply them when reading or writing to the model. Qt 委托自动设置和获取那些“用户属性”,并在读取或写入模型时应用它们。

If you properly implement a qt property for your editor, this simplifies things.如果你为你的编辑器正确地实现了一个 qt 属性,这会简化事情。 The only thing to be aware of is that properties are normally basic objects (so, tuples of different types, as in your case, are not supported).唯一要注意的是属性通常是基本对象(因此,不支持不同类型的元组,如您的情况)。 The solution is to implement only what is needed, which, in this case, is the setModelData() .解决方案是只实现需要的东西,在本例中是setModelData()

import sys

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *


class NameAge(QWidget):
    changed = pyqtSignal()
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.name_edit = QLineEdit()
        self.age_spinbox = QSpinBox()

        layout = QHBoxLayout()
        layout.addWidget(self.name_edit)
        layout.addWidget(self.age_spinbox)

        self.setLayout(layout)

        self.name_edit.installEventFilter(self)
        self.age_spinbox.installEventFilter(self)

        # alternatively, you can skip the whole event filter aspect, and just
        # connect to the textChanged and valueChanged of the above widgets to
        # the changed signal; this will potentially update the model whenever
        # the value of those widgets is changed by the user.

    @pyqtProperty(object, user=True)
    def data(self):
        return self.name_edit.text(), self.age_spinbox.value()

    @data.setter
    def data(self, data):
        if data is None:
            return
        try:
            name, age = data
            self.name_edit.setText(name)
            self.age_spinbox.setValue(age)
        except (ValueError, TypeError) as e:
            raise e

    def setData(self, data):
        self.data = data

    def eventFilter(self, obj, event):
        if (
            event.type() == event.FocusOut
            and not self.isAncestorOf(QApplication.focusWidget())
        ):
            self.changed.emit()
        return super().eventFilter(obj, event)


class NameAgeModel(QAbstractListModel):
    def __init__(self, data, parent=None):
        super().__init__(parent)
        self.data = data

    def rowCount(self, parent=QModelIndex()):
        return len(self.data)

    def data(self, index, role=Qt.DisplayRole):
        if role in (Qt.DisplayRole, Qt.EditRole):
            return self.data[index.row()]
        
    def setData(self, index, value, role):
        if role == Qt.EditRole:
            self.data[index.row()] = value
            self.dataChanged.emit(index, index)
            return True
        return False

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


class NameAgeDelegate(QStyledItemDelegate):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.editors = {}

    def setModelData(self, editor, model, index):
        model.setData(index, editor.data, Qt.EditRole)

    def createEditor(self, parent, option, index):
        name_age = NameAge(parent=parent)

        pIndex = QPersistentModelIndex(index)
        if pIndex in self.editors:
            self.editors[pIndex].deleteLater()
        self.editors[pIndex] = name_age

        name_age.changed.connect(lambda: self.commitData.emit(name_age))
        name_age.destroyed.connect(lambda: self.editors.pop(pIndex))

        self.sizeHintChanged.emit(index)
        return name_age

    def sizeHint(self, option, index):
        editor = self.editors.get(QPersistentModelIndex(index))
        if editor:
            return editor.sizeHint()
        return super().sizeHint(option, index)

    def updateEditorGeometry(self, editor, option, index):
        # required to avoid some quirks for custom editors when committing data
        editor.setGeometry(option.rect)


class MainWidget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        layout = QVBoxLayout()
        self.setLayout(layout)

        self.list_view = QListView()
        self.list_view.setItemDelegate(NameAgeDelegate(self.list_view))
        model = NameAgeModel([('Mark', 38), ('John', 30), ('Jane', 25)])
        self.list_view.setModel(model)

        layout.addWidget(self.list_view)

        for row in range(model.rowCount()):
            self.list_view.openPersistentEditor(model.index(row, 0))

        model.rowsInserted.connect(lambda p, row, _: 
            self.list_view.openPersistentEditor(model.index(row, 0)))


if __name__ == '__main__':
    app = QApplication(sys.argv)
    main_widget = MainWidget()
    main_widget.show()
    sys.exit(app.exec_())

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM