如何在 PyQt5 中使用活动的 QComboBox 作为 QListView 的元素?

How to use an active QComboBox as an element of QListView in PyQt5?

我正在使用PyQt5 制作应用程序。我的一个小部件将是一个 QListView,它显示所需项目的列表,例如例如,需要烹饪特定菜肴。

对于其中的大多数,列出的项目是唯一的可能性。但对于少数项目,有不止一种选择可以满足要求。对于那些有多种可能性的人,我想在功能性 QComboBox 中显示这些可能性。所以如果用户没有全脂牛奶,他们可以点击那个项目,然后看到 2% 的牛奶也可以。

如何在 QListView 的元素中包含工作组合框?

下面是一个示例,展示了我目前的情况。它可以在 Spyder 或使用 python -i 中工作,您只需按照说明进行注释或取消注释。 “工作”是指它在 QListView 中显示所需的项目,但组合框仅显示第一个选项,并且不能用鼠标更改它们的显示。但是,我可以说例如qb1.setCurrentIndex(1) 在 python 提示下,然后当我将鼠标指针移到小部件上时,显示更新为“2% 牛奶”。我发现能够在 Spyder 或 python 解释器中与小部件交互并检查它很有帮助,但我仍然有这个问题。我知道周围有这样的 C++ 示例,但我一直无法很好地理解它们来做我想做的事。如果我们可以 post 一个有效的 Python 示例,我相信它会对我和其他人有所帮助。

from PyQt5.QtWidgets import QApplication, QComboBox, QListView, QStyledItemDelegate
from PyQt5.QtCore import QAbstractListModel, Qt

# A delegate for the combo boxes. 
class QBDelegate(QStyledItemDelegate):    
    def paint(self, painter, option, index):
        painter.drawText(option.rect, Qt.AlignLeft, self.parent().currentText())


# my own wrapper for the abstract list class
class PlainList(QAbstractListModel):
    def __init__(self, elements):
        super().__init__()
        self.elements = elements
        
    def data(self, index, role):
        if role == Qt.DisplayRole:
            text = self.elements[index.row()]
            return text

    def rowCount(self, index):
        try:
            return len(self.elements)
        except TypeError:
            return self.elements.rowCount(index)

app = QApplication([])  # in Spyder, this seems unnecessary, but harmless. 

qb0 = 'powdered sugar'  # no other choice
qb1 = QComboBox()
qb1.setModel(PlainList(['whole milk','2% milk','half-and-half']))
d1 = QBDelegate(qb1)
qb1.setItemDelegate(d1)

qb2 = QComboBox()
qb2.setModel(PlainList(['butter', 'lard']))
d2 = QBDelegate(qb2)
qb2.setItemDelegate(d2)

qb3 = 'cayenne pepper'  # there is no substitute

QV = QListView()
qlist = PlainList([qb0, qb1, qb2, qb3])

QV.setModel(qlist)
QV.setItemDelegateForRow(1, d1)
QV.setItemDelegateForRow(2, d2)
QV.show()

app.exec_() #  Comment this line out, to run in Spyder. Then you can inspect QV etc in the iPython console. Handy! 

您的尝试存在一些误解。

首先,将委托父级设置为组合框,然后将委托设置为列表视图不会使委托显示组合框。

此外,作为 documentation clearly says:

Warning: You should not share the same instance of a delegate between views. Doing so can cause incorrect or unintuitive editing behavior since each view connected to a given delegate may receive the closeEditor() signal, and attempt to access, modify or close an editor that has already been closed.

在任何情况下,将组合框添加到项目列表肯定不是一个选项:视图与它没有任何关系,覆盖 data() 以显示当前组合项目是不是有效的解决方案;虽然理论上项目数据可以包含任何类型的对象,但出于您的目的,模型应该包含 数据,而不是小部件。

为了显示不同的视图小部件,您必须覆盖 createEditor() 和 return 适当的小部件。

然后,由于您可能需要在访问模型和视图时保持数据可用,因此模型应包含可用选项,并最终 return 当前选项 "sub-list" 视情况而定。

最后,rowCount() 必须始终 return 模型的行数,而不是索引内容的行数。

一种可能性是为内部模型的选定选项创建一个支持“当前索引”的“嵌套模型”。

然后您可以使用 openPersistentEditor() or implement flags() 并为包含列表模型的项目添加 Qt.ItemIsEditable

class QBDelegate(QStyledItemDelegate):
    def createEditor(self, parent, option, index):
        value = index.data(Qt.EditRole)
        if isinstance(value, PlainList):
            editor = QComboBox(parent)
            editor.setModel(value)
            editor.setCurrentIndex(value.currentIndex)
            # submit the data whenever the index changes
            editor.currentIndexChanged.connect(
                lambda: self.commitData.emit(editor))
        else:
            editor = super().createEditor(parent, option, index)
        return editor

    def setModelData(self, editor, model, index):
        if isinstance(editor, QComboBox):
            # the default implementation tries to set the text if the
            # editor is a combobox, but we need to set the index
            model.setData(index, editor.currentIndex())
        else:
            super().setModelData(editor, model, index)


class PlainList(QAbstractListModel):
    currentIndex = 0
    def __init__(self, elements):
        super().__init__()
        self.elements = []
        for element in elements:
            if isinstance(element, (tuple, list)) and element:
                element = PlainList(element)
            self.elements.append(element)
        
    def data(self, index, role=Qt.DisplayRole):
        if role == Qt.EditRole:
            return self.elements[index.row()]
        elif role == Qt.DisplayRole:
            value = self.elements[index.row()]
            if isinstance(value, PlainList):
                return value.elements[value.currentIndex]
            else:
                return value

    def flags(self, index):
        flags = super().flags(index)
        if isinstance(index.data(Qt.EditRole), PlainList):
            flags |= Qt.ItemIsEditable
        return flags

    def setData(self, index, value, role=Qt.EditRole):
        if role == Qt.EditRole:
            item = self.elements[index.row()]
            if isinstance(item, PlainList):
                item.currentIndex = value
            else:
                self.elements[index.row()] = value
        return True

    def rowCount(self, parent=None):
        return len(self.elements)

app = QApplication([])

qb0 = 'powdered sugar'  # no other choice
qb1 = ['whole milk','2% milk','half-and-half']
qb2 = ['butter', 'lard']
qb3 = 'cayenne pepper'  # there is no substitute

QV = QListView()
qlist = PlainList([qb0, qb1, qb2, qb3])

QV.setModel(qlist)
QV.setItemDelegate(QBDelegate(QV))

## to always display the combo:
#for i in range(qlist.rowCount()):
#    index = qlist.index(i)
#    if index.flags() & Qt.ItemIsEditable:
#        QV.openPersistentEditor(index)

QV.show()

app.exec_()