Qt CheckBox 委托生成两个复选框

Qt CheckBox delegate generates two checkboxes

我正在尝试在 PySide GUI 中实现某种列表视图,让用户有机会在最终处理列表之前 enable/disable 列表的某些条目。

我决定将 QTableView 和 QAbstractTableModel 与 CheckBoxDelegate class 一起使用,后者为 table 视图中的每一行呈现一个复选框。选中和取消选中条目将相应地设置基础列表对象的启用属性。这使我可以在处理时轻松跳过条目。

我想画一个居中的复选框。因此,基于这个 SO 问题 ,我在 CheckBoxDelegate 中使用 QCheckbox 的子 class。 现在我的问题是我在第 0 列中得到两个复选框。但我不明白为什么...

这是我的代码

# -*- coding: UTF-8 -*-
import sys

from sip import setdestroyonexit
from PySide import QtCore
from PySide import QtGui


def do_action(obj):
    print "do stuff for", obj.data_value


class MyObject(object):
    def __init__(self, data_value, enabled=True):
        self.data_value = data_value
        self.enabled = enabled
        self.result = None
        self.action = ''


class MyCheckBox(QtGui.QCheckBox):
    def __init__(self, parent):
        QtGui.QCheckBox.__init__(self, parent)
        # create a centered checkbox
        self.cb = QtGui.QCheckBox(parent)
        cbLayout = QtGui.QHBoxLayout(self)
        cbLayout.addWidget(self.cb, 0, QtCore.Qt.AlignCenter)
        self.cb.clicked.connect(self.stateChanged)

    def isChecked(self):
        return self.cb.isChecked()

    def setChecked(self, value):
        self.cb.setChecked(value)

    @QtCore.Slot()
    def stateChanged(self):
        print "sender", self.sender()
        self.clicked.emit()


class CheckBoxDelegate(QtGui.QItemDelegate):
    """
    A delegate that places a fully functioning QCheckBox in every
    cell of the column to which it's applied
    """
    def __init__(self, parent):
        QtGui.QItemDelegate.__init__(self, parent)

    def createEditor(self, parent, option, index):
        cb = MyCheckBox(parent)
        cb.clicked.connect(self.stateChanged)
        return cb

    def paint(self, painter, option, index):
        value = index.data()
        if value:
            value = QtCore.Qt.Checked
        else:
            value = QtCore.Qt.Unchecked
        self.drawCheck(painter, option, option.rect, value)
        self.drawFocus(painter, option, option.rect)

    def setEditorData(self, editor, index):
        """ Update the value of the editor """
        editor.blockSignals(True)
        editor.setChecked(index.model().checked_state(index))
        editor.blockSignals(False)

    def setModelData(self, editor, model, index):
        """ Send data to the model """
        model.setData(index, editor.isChecked(), QtCore.Qt.EditRole)

    @QtCore.Slot()
    def stateChanged(self):
        print "sender", self.sender()
        self.commitData.emit(self.sender())


class TableView(QtGui.QTableView):
    """
    A simple table to demonstrate the QCheckBox delegate.
    """
    def __init__(self, *args, **kwargs):
        QtGui.QTableView.__init__(self, *args, **kwargs)
        # Set the delegate for column 0 of our table
        self.setItemDelegateForColumn(0, CheckBoxDelegate(self))


class MyWindow(QtGui.QWidget):

    def __init__(self, *args):
        QtGui.QWidget.__init__(self, *args)
        # setGeometry(x_pos, y_pos, width, height)
        self.setGeometry(300, 200, 640, 480)
        self.setWindowTitle("CheckBoxDelegate with two Checkboxes?")
        self.object_list = [
            MyObject('Task 1'),
            MyObject('Task 2'),
            MyObject('Task 3'),
        ]
        self.header = ['Active', 'Data value', 'Result', 'Action']
        table_model = MyTableModel(self,
                                   self.object_list,
                                   ['enabled', 'data_value', 'result', 'action'],
                                   self.header)

        self.table_view = TableView()
        self.table_view.setModel(table_model)

        active_col = self.header.index('Active')
        for row in range(0, table_model.rowCount()):
            self.table_view.openPersistentEditor(table_model.index(row, active_col))

        action_col = self.header.index('Action')
        for i, bo in enumerate(self.object_list):
            btn = QtGui.QPushButton(self.table_view)
            btn.setText("View")
            self.table_view.setIndexWidget(table_model.index(i, action_col), btn)
            btn.clicked.connect(lambda obj=bo: do_action(obj))

        # set font
        font = QtGui.QFont("Calibri", 10)
        self.table_view.setFont(font)
        # set column width to fit contents (set font first!)
        self.table_view.resizeColumnsToContents()

        layout = QtGui.QVBoxLayout(self)
        layout.addWidget(self.table_view)

        self.setLayout(layout)


class MyTableModel(QtCore.QAbstractTableModel):
    def __init__(self, parent, rows, columns, header, *args):
        QtCore.QAbstractTableModel.__init__(self, parent, *args)
        self.rows = rows
        self.columns = columns
        self.header = header
        self.CB_COL = 0
        assert len(columns) == len(header), "Header names dont have the same " \
                                            "length as supplied columns"

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

    def columnCount(self, parent=QtCore.QModelIndex()):
        return len(self.columns)

    def checked_state(self, index):
        if not index.isValid():
            return None
        elif index.column() == self.CB_COL:
            attr_name = self.columns[index.column()]
            row = self.rows[index.row()]
            return getattr(row, attr_name)
        else:
            return None

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if not index.isValid():
            return None
        elif role == QtCore.Qt.DisplayRole:
            attr_name = self.columns[index.column()]
            row = self.rows[index.row()]
            if index.column() == self.CB_COL:
                # no text for checkbox column's
                return None
            else:
                return getattr(row, attr_name)
        elif role == QtCore.Qt.CheckStateRole:
            return None
        else:
            return None

    def setData(self, index, value, role=QtCore.Qt.EditRole):
        if role == QtCore.Qt.EditRole:
            attr_name = self.columns[index.column()]
            row = self.rows[index.row()]

            if ((index.column() == self.CB_COL)
                    and (value != self.rows[index.row()].enabled)):
                if value:
                    print "Enabled",
                else:
                    print "Disabled",
                print self.rows[index.row()].data_value

            setattr(row, attr_name, value)

            self.emit(QtCore.SIGNAL("dataChanged(const QModelIndex&, const QModelIndex &)"),
                      index, index)
            return True
        else:
            return False

    def headerData(self, col, orientation, role):
        if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
            return self.header[col]
        return None

    def flags(self, index):
        if (index.column() == self.CB_COL):
            return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled
        else:
            return QtCore.Qt.ItemIsEnabled


if __name__ == "__main__":
    # avoid crash on exit
    setdestroyonexit(False)
    app = QtGui.QApplication(sys.argv)
    window = MyWindow()
    window.show()
    sys.exit(app.exec_())

任何人都可以解释为什么会发生这种情况(以及我该如何解决)?

你遇到问题是因为你的 MyCheckBox class 一个 QCheckBox (通过继承)和 通过在其初始化 (self.cb).

中构造一个新的 QCheckBox 实例,拥有 QCheckBox

你真的只想做其中之一。只是为了演示,我像这样重写了 MyCheckBox class:

class MyCheckBox(QtGui.QWidget):
    def __init__(self, parent):
        QtGui.QWidget.__init__(self, parent)
        # create a centered checkbox
        self.cb = QtGui.QCheckBox(parent)
        cbLayout = QtGui.QHBoxLayout(self)
        cbLayout.addWidget(self.cb, 0, QtCore.Qt.AlignCenter)
        self.cb.clicked.connect(self.amClicked)

    clicked = QtCore.Signal()

    def amClicked(self):
        self.clicked.emit()

这解决了问题(尽管您还需要进行一些其他更改)。 请注意,您使用的点击信号需要来自 MyCheckBox 而不是 QCheckBox,因此我已通过 amClicked 插槽将其添加到包含 class 中。您不需要区分模型中的 data()checked_state() 方法,因此我将它们合并为一个:

def data(self, index, role=QtCore.Qt.DisplayRole):
    if not index.isValid():
        return None
    elif role == QtCore.Qt.DisplayRole:
        attr_name = self.columns[index.column()]
        row = self.rows[index.row()]
        return getattr(row, attr_name)
    elif role == QtCore.Qt.CheckStateRole:
        return None
    else:
        return None

那么Delegate是这个样子的。如果标志说它是可编辑的,我已经安排它只提供一个编辑器。如果不是,那么它负责绘图,所以它也必须在 paint 方法中做正确的事情。

class CheckBoxDelegate(QtGui.QItemDelegate):
    """
    A delegate that places a fully functioning QCheckBox in every
    cell of the column to which it's applied
    """
    def __init__(self, parent):
        QtGui.QItemDelegate.__init__(self, parent)

    def createEditor(self, parent, option, index):
        if not (QtCore.Qt.ItemIsEditable & index.flags()):
            return None
        cb = MyCheckBox(parent)
        cb.clicked.connect(self.stateChanged)
        return cb

    def setEditorData(self, editor, index):
        """ Update the value of the editor """
        editor.blockSignals(True)
        editor.setChecked(index.data())
        editor.blockSignals(False)

    def setModelData(self, editor, model, index):
        """ Send data to the model """
        model.setData(index, editor.isChecked(), QtCore.Qt.EditRole)

    def paint(self, painter, option, index):
        value = index.data()
        if value:
            value = QtCore.Qt.Checked
        else:
            value = QtCore.Qt.Unchecked
        self.drawCheck(painter, option, option.rect, value)
        self.drawFocus(painter, option, option.rect)

    @QtCore.Slot()
    def stateChanged(self):
        print "sender", self.sender()
        self.commitData.emit(self.sender())

另一种方法是使用继承而不是 inclusion/delegation。这是一个使用它的例子:

class MyCheckBox(QtGui.QCheckBox):
    def __init__(self, parent):
        QtGui.QCheckBox.__init__(self, parent)
        # Do some customisation here

    # Might want to customise the paint here
    # def paint(self, painter, option, index):


class CheckBoxDelegate(QtGui.QItemDelegate):
    """
    A delegate that places a fully functioning QCheckBox in every
    cell of the column to which it's applied
    """
    def __init__(self, parent):
        QtGui.QItemDelegate.__init__(self, parent)

这似乎更简单,但是,在这种情况下它有几个问题。很难绘制以 MyCheckBox class 为中心的复选框 - 这需要我们覆盖 paintEvent 并且需要仔细绘制。它也不会完全覆盖委托的绘制。所以你可以把它拿出来。但是只有在为该行创建了编辑器时它才会起作用。所以第一个解决方案在这种情况下可能是最简单的。