带有持久编辑器的复选框

Checkbox with persistent editor

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

这个例子有点做作(model/view 的设置方式),但说明了当编辑器打开时复选框不起作用。

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

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, editor, 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, 2, 3], [1, 2, 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, index, 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 源代码后,我意识到我原来的答案虽然有效,但有点不准确。

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

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

只要实现virtual edit(index, trigger, event) function (which is not the same as the edit(index)槽,就可以拦截“鼠标编辑”事件。请注意,这意味着如果您覆盖该函数,则不能再调用默认的 edit(index),除非您创建一个单独的函数来调用默认实现(通过 super().edit(index))。

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

class PersistentEditorTableView(QtWidgets.QTableView):
    # ...
    def edit(self, index, trigger, 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, trigger, event)

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

只有最后一个问题:键盘事件。
持久性编辑器会自动获取键盘焦点,因此如果编辑器处理了该事件,则您不能使用任何键(通常是 space 栏)来切换状态;由于组合框处理一些“切换”事件以显示其弹出窗口,因此视图不会接收到这些事件(焦点在组合上,而不是在视图上!)除非你通过为键盘事件和焦点更改正确实现委托的 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, option.widget)

        editor = QtWidgets.QWidget(parent)
        editor.index = QtCore.QPersistentModelIndex(index)
        layout = QtWidgets.QHBoxLayout(editor)
        layout.setContentsMargins(textRect.left(), 0, 0, 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, editor, 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, editor, opt, 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 信号,因为这些操作确实 发送布局改变信号。