带有持久编辑器的复选框

问题描述

我正在使用表格来控制绘图的可见性和颜色。我想要一个复选框来切换可见性和一个下拉菜单来选择颜色。为此,我有如下内容。感觉就像有一个持久的编辑器可以防止使用复选框。

这个例子有点人为(模型/视图的设置方式),但说明了在编辑器打开时复选框如何不起作用。

我怎样才能拥有一个可以与可见组合框一起使用的复选框?使用两列更好吗?

import sys
from PyQt5 import QtWidgets,QtCore

class ComboDelegate(QtWidgets.QItemDelegate):

    def __init__(self,parent):
        super().__init__(parent=parent)

    def createEditor(self,parent,option,index):
        combo = QtWidgets.QComboBox(parent)
        li = []
        li.append("Red")
        li.append("Green")
        li.append("Blue")
        li.append("Yellow")
        li.append("Purple")
        li.append("Orange")
        combo.addItems(li)
        combo.currentIndexChanged.connect(self.currentIndexChanged)
        return combo

    def setEditorData(self,editor,index):
        editor.blockSignals(True)
        data = index.model().data(index)
        if data:
            idx = int(data)
        else:
            idx = 0
        editor.setCurrentIndex(0)
        editor.blockSignals(False)

    def setModelData(self,model,index):
        model.setData(index,editor.currentIndex())

    @QtCore.pyqtSlot()
    def currentIndexChanged(self):
        self.commitData.emit(self.sender())


class PersistentEditorTableView(QtWidgets.QTableView):

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

    QtCore.pyqtSlot('QVariant','QVariant')
    def data_changed(self,top_left,bottom_right):
        for row in range(len(self.model().tableData)):
            self.openPersistentEditor(self.model().index(row,0))


class TableModel(QtCore.QAbstractTableModel):
    def __init__(self,parent=None):
        super(TableModel,self).__init__(parent)
        self.tableData = [[1,2,3],[1,3]]
        self.checks = {}

    def columnCount(self,*args):
        return 3

    def rowCount(self,*args):
        return 3

    def checkState(self,index):
        if index in self.checks.keys():
            return self.checks[index]
        else:
            return QtCore.Qt.Unchecked

    def data(self,index,role=QtCore.Qt.displayRole):
        row = index.row()
        col = index.column()
        if role == QtCore.Qt.displayRole:
            return '{0}'.format(self.tableData[row][col])
        elif role == QtCore.Qt.CheckStateRole and col == 0:
            return self.checkState(QtCore.QPersistentModelIndex(index))
        return None

    def setData(self,value,role=QtCore.Qt.EditRole):
        if not index.isValid():
            return False
        if role == QtCore.Qt.CheckStateRole:
            self.checks[QtCore.QPersistentModelIndex(index)] = value
            self.dataChanged.emit(index,index)
            return True
        return False

    def flags(self,index):
        fl = QtCore.QAbstractTableModel.flags(self,index)
        if index.column() == 0:
            fl |= QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable
        return fl


if __name__ == "__main__":

    app = QtWidgets.QApplication(sys.argv)

    view = PersistentEditorTableView()
    view.setItemDelegateForColumn(0,ComboDelegate(view))

    model = TableModel()
    view.setModel(model)
    model.dataChanged.connect(view.data_changed)
    model.layoutChanged.connect(view.data_changed)

    index = model.createIndex(0,0)
    persistet_index = QtCore.QPersistentModelIndex(index)
    model.checks[persistet_index] = QtCore.Qt.Checked
    view.data_changed(index,index)

    view.show()
    sys.exit(app.exec_())

解决方法

注意:在对 Qt 源代码进行一些重新思考和分析之后,我意识到我原来的答案虽然有效,但有点不准确。

问题依赖于这样一个事实,即在具有活动编辑器的索引上接收到的所有事件都会自动发送到编辑器,但由于编辑器的几何图形可能小于索引的可视矩形,如果鼠标事件被发送到几何体之外,该事件被忽略并且不会像通常没有编辑器的情况那样被视图处理。

UPDATE: 事实上,视图确实接收了事件;问题是,如果编辑器存在,则事件会自动被进一步忽略(因为假设编辑器处理了它)并且没有任何内容发送到委托的 editorEvent 方法。

只要您实现了virtual edit(index,trigger,event) 功能(这与{{3} } 投币口)。请注意,这意味着如果您覆盖该函数,则无法再调用默认的 edit(index),除非您创建一个单独的函数来调用默认实现(通过 super().edit(index))。

如果委托实际上是 QStyledItemDelegate,而不是更简单的 QItemDelegate 类,请考虑以下代码效果更好:Qt 开发团队本身 edit(index) 使用样式类而不是基本类(旨在用于非常基本或特定的用法),因为它通常被认为更一致。

class PersistentEditorTableView(QtWidgets.QTableView):
    # ...
    def edit(self,index,event):
        # if the edit involves an index change,there's no event
        if (event and index.column() == 0 and 
            index.flags() & QtCore.Qt.ItemIsUserCheckable and
            event.type() in (event.MouseButtonPress,event.MouseButtonDblClick) and 
            event.button() == QtCore.Qt.LeftButton):
                opt = self.viewOptions()
                opt.rect = self.visualRect(index)
                opt.features |= opt.HasCheckIndicator
                checkRect = self.style().subElementRect(
                    QtWidgets.QStyle.SE_ItemViewItemCheckIndicator,opt,self)
                if event.pos() in checkRect:
                    if index.data(QtCore.Qt.CheckStateRole):
                        state = QtCore.Qt.Unchecked
                    else:
                        state = QtCore.Qt.Checked
                    return self.model().setData(
                        index,state,QtCore.Qt.CheckStateRole)
        return super().edit(index,event)

显然,您可以通过在视图上实现 mousePressEvent 来做类似的事情,但是如果您还需要鼠标按下事件的一些不同实现,这可能会使事情复杂化,并且您还应该考虑双击事件。实现 edit() 在概念上更好,因为它更符合目的:点击 -> 切换。

只有最后一个问题:键盘事件。
持久化编辑器会自动获取键盘焦点,因此如果编辑器处理该事件,您不能使用任何键(通常是空格键)来切换状态;由于组合框处理一些“切换”事件以显示其弹出窗口,因此视图不会接收这些事件(焦点在组合上,而不是视图上!)除非您忽略该事件通过为键盘事件和焦点更改正确实现委托的 eventFilter

原答案

有多种方法可以解决此问题:

  1. 按照您的建议,使用单独的列;这并不总是可行或建议的,因为模型结构不允许这样做;
  2. 创建一个包含 QCheckBox 的编辑器;只要总是有一个打开的编辑器,这不是问题,但如果编辑器实际上可以打开和销毁,则可能会造成一定程度的不一致;
  3. 将组合添加到具有固定边距的容器中,以便委托事件过滤器可以捕获未由组合处理的鼠标事件;

第三种可能稍微复杂一些,但它可以确保显示和交互都与正常行为保持一致。
为此,建议使用 QStyledItemDelegate,因为它提供了对样式选项的访问。

class ComboDelegate(QtWidgets.QStyledItemDelegate):
    # ...
    def createEditor(self,parent,option,index):
        option = QtWidgets.QStyleOptionViewItem(option)
        self.initStyleOption(option,index)
        style = option.widget.style()
        textRect = style.subElementRect(
            style.SE_ItemViewItemText,option.widget)

        editor = QtWidgets.QWidget(parent)
        editor.index = QtCore.QPersistentModelIndex(index)
        layout = QtWidgets.QHBoxLayout(editor)
        layout.setContentsMargins(textRect.left(),0)
        editor.combo = QtWidgets.QComboBox()
        layout.addWidget(editor.combo)
        editor.combo.addItems(
            ("Red","Green","Blue","Yellow","Purple","Orange"))
        editor.combo.currentIndexChanged.connect(self.currentIndexChanged)
        return editor

    def setEditorData(self,editor,index):
        editor.combo.blockSignals(True)
        data = index.model().data(index)
        if data:
            idx = int(data)
        else:
            idx = 0
        editor.combo.setCurrentIndex(0)
        editor.combo.blockSignals(False)

    def eventFilter(self,event):
        if (event.type() in (event.MouseButtonPress,event.MouseButtonDblClick)
            and event.button() == QtCore.Qt.LeftButton):
                style = editor.style()
                size = style.pixelMetric(style.PM_IndicatorWidth)
                left = editor.layout().contentsMargins().left()
                r = QtCore.QRect(
                    (left - size) / 2,(editor.height() - size) / 2,size,size)
                if event.pos() in r:
                    model = editor.index.model()
                    index = QtCore.QModelIndex(editor.index)
                    if model.data(index,QtCore.Qt.CheckStateRole):
                        value = QtCore.Qt.Unchecked
                    else:
                        value = QtCore.Qt.Checked
                    model.setData(
                        index,value,QtCore.Qt.CheckStateRole)
                return True
        return super().eventFilter(editor,event)

    def updateEditorGeometry(self,index):
        # ensure that the editor fills the whole index rect
        editor.setGeometry(opt.rect)

不相关,但仍然重要:

考虑到在这些正常情况下通常不需要 @pyqtSlot 装饰器。另请注意,您错过了 @ 装饰器的 data_changed,并且签名也是无效的,因为它与您连接到的信号不兼容。更正确的插槽装饰如下:

@QtCore.pyqtSlot(QtCore.QModelIndex,QtCore.QModelIndex,'QVector<int>')
@QtCore.pyqtSlot('QList<QPersistentModelIndex>',QtCore.QAbstractItemModel.LayoutChangeHint)
def data_changed(self,top_left,bottom_right):
    # ...

第一个装饰用于 dataChanged 信号,第二个装饰用于 layoutChanged。但是,如前所述,通常没有必要使用插槽,因为它们通常只用于更好的线程处理,并且有时提供略微改进的性能(这对于此目的并不重要)。
另请注意,如果您想确保在模型更改其“布局”时始终有一个打开的编辑器,您还应该连接到 rowsInsertedcolumnsInserted 信号,因为这些操作不会 发送布局改变信号。