【问题标题】:Checkbox with persistent editor带有持久编辑器的复选框
【发布时间】:2021-09-05 05:18:23
【问题描述】:

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

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

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

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_())

【问题讨论】:

    标签: python pyqt pyqt5 qtableview


    【解决方案1】:

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

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

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

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

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

    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() 在概念上更好,因为它更符合目的:单击 -> 切换。

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

    【讨论】:

    • 亲爱的,我感激不尽!我从中学到了很多东西。对于其他阅读本文并想知道为什么在切换检查时组合会自行重置的人,这是因为setEditorData 调用setCurrentIndex(0)
    • @LoremIpsum 不客气。请考虑,与此同时,在对源代码进行了一些考虑和研究之后,我已经更新了答案。原来的答案仍然有效,但它基于不同的方面;决定使用哪种方法取决于您的需求和实施。
    猜你喜欢
    • 2017-04-01
    • 2013-03-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多