PyQt - 在QTableView中添加复选框列

14
我正在使用 Pandas dataframe 动态创建 QTableView。我有示例代码在这里。 我可以创建表格,并显示复选框,但我无法让复选框反映模型数据,甚至无法将其更改为未选中状态。
我正在按照之前这个问题的示例代码,以@raorao的答案为指南。这将在表格中显示框,但没有任何功能正常工作。
有人可以建议做出任何更改,或者这段代码有什么问题。为什么它不能反映模型,为什么它不能更改?
请查看我的完整示例代码在这里。 编辑一:在Frodon的评论后进行更新:用比较xxx == 'True'来更正字符串转换为bool。
class CheckBoxDelegate(QtGui.QStyledItemDelegate):
    """
    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):
        '''
        Important, otherwise an editor is created if the user clicks in this cell.
        ** Need to hook up a signal to the model
        '''
        return None

    def paint(self, painter, option, index):
        '''
        Paint a checkbox without the label.
        '''
        checked = index.model().data(index, QtCore.Qt.DisplayRole) == 'True'
        check_box_style_option = QtGui.QStyleOptionButton()

        if (index.flags() & QtCore.Qt.ItemIsEditable) > 0:
            check_box_style_option.state |= QtGui.QStyle.State_Enabled
        else:
            check_box_style_option.state |= QtGui.QStyle.State_ReadOnly

        if checked:
            check_box_style_option.state |= QtGui.QStyle.State_On
        else:
            check_box_style_option.state |= QtGui.QStyle.State_Off

        check_box_style_option.rect = self.getCheckBoxRect(option)

        # this will not run - hasFlag does not exist
        #if not index.model().hasFlag(index, QtCore.Qt.ItemIsEditable):
            #check_box_style_option.state |= QtGui.QStyle.State_ReadOnly

        check_box_style_option.state |= QtGui.QStyle.State_Enabled

        QtGui.QApplication.style().drawControl(QtGui.QStyle.CE_CheckBox, check_box_style_option, painter)

    def editorEvent(self, event, model, option, index):
        '''
        Change the data in the model and the state of the checkbox
        if the user presses the left mousebutton or presses
        Key_Space or Key_Select and this cell is editable. Otherwise do nothing.
        '''
        print 'Check Box editor Event detected : ' 
        if not (index.flags() & QtCore.Qt.ItemIsEditable) > 0:
            return False

        print 'Check Box edior Event detected : passed first check' 
        # Do not change the checkbox-state
        if event.type() == QtCore.QEvent.MouseButtonRelease or event.type() == QtCore.QEvent.MouseButtonDblClick:
            if event.button() != QtCore.Qt.LeftButton or not self.getCheckBoxRect(option).contains(event.pos()):
                return False
            if event.type() == QtCore.QEvent.MouseButtonDblClick:
                return True
        elif event.type() == QtCore.QEvent.KeyPress:
            if event.key() != QtCore.Qt.Key_Space and event.key() != QtCore.Qt.Key_Select:
                return False
            else:
                return False

        # Change the checkbox-state
        self.setModelData(None, model, index)
        return True

2
这条代码:checked = bool(index.model().data(index, QtCore.Qt.DisplayRole)) 确实返回布尔类型吗?我本来会写成 checked = index.model().data(index, QtCore.Qt.DisplayRole).toBool() 的,因为 data() 的结果是 QVariant 类型的。 - Frodon
1
有趣的是,我确实尝试了那个方法,但是收到了一个错误:AttributeError: 'str' object has no attribute 'toBool',所以最终选择了强制转换。谢谢。 - C Mars
经进一步调查,发现类型转换不正确,如果值非零,则始终返回True。我已经修改为checked = index.model().data(index, QtCore.Qt.DisplayRole) == 'True'。现在数据反映正确,但仍未完全解决问题,因为它仍然无法更改。再次感谢您的评论。我会更新问题。 - C Mars
3个回答

18

这里是同样代码的PyQt5版本。 我将其发布在这里,因为似乎没有另一个工作中的QT5复选框代理的示例,而我一直在尝试让它工作。希望对别人有用。

from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import QModelIndex
from PyQt5.QtGui import QStandardItemModel
from PyQt5.QtWidgets import QApplication, QTableView

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

    def createEditor(self, parent, option, index):
        """
        Important, otherwise an editor is created if the user clicks in this cell.
        """
        return None

    def paint(self, painter, option, index):
        """
        Paint a checkbox without the label.
        """
        self.drawCheck(painter, option, option.rect, QtCore.Qt.Unchecked if int(index.data()) == 0 else QtCore.Qt.Checked)

    def editorEvent(self, event, model, option, index):
        '''
        Change the data in the model and the state of the checkbox
        if the user presses the left mousebutton and this cell is editable. Otherwise do nothing.
        '''
        if not int(index.flags() & QtCore.Qt.ItemIsEditable) > 0:
            return False

        if event.type() == QtCore.QEvent.MouseButtonRelease and event.button() == QtCore.Qt.LeftButton:
            # Change the checkbox-state
            self.setModelData(None, model, index)
            return True

        return False


    def setModelData (self, editor, model, index):
        '''
        The user wanted to change the old state in the opposite.
        '''
        model.setData(index, 1 if int(index.data()) == 0 else 0, QtCore.Qt.EditRole)



if __name__ == '__main__':

    import sys

    app = QApplication(sys.argv)

    model = QStandardItemModel(4, 3)
    tableView = QTableView()
    tableView.setModel(model)

    delegate = CheckBoxDelegate(None)
    tableView.setItemDelegateForColumn(1, delegate)
    for row in range(4):
        for column in range(3):
            index = model.index(row, column, QModelIndex())
            model.setData(index, 1)

    tableView.setWindowTitle("Check Box Delegate")
    tableView.show()
    sys.exit(app.exec_())

14

我已经为您找到了解决方案。诀窍是:

  1. 编写模型的setData方法
  2. data方法中始终返回QVariant

这里是代码示例。(我不得不创建一个名为Dataframe的类,以使示例在没有pandas的情况下正常工作。请用您的代码替换所有if has_pandas语句):

from PyQt4 import QtCore, QtGui

has_pandas = False
try:
  import pandas as pd
  has_pandas = True
except:
  pass

class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, parent=None, *args):
        super(TableModel, self).__init__()
        self.datatable = None
        self.headerdata = None

    def update(self, dataIn):
        print 'Updating Model'
        self.datatable = dataIn
        print 'Datatable : {0}'.format(self.datatable)
        if has_pandas:
          headers = dataIn.columns.values
        else:
          headers = dataIn.columns
        header_items = [
                    str(field)
                    for field in headers
        ]
        self.headerdata = header_items
        print 'Headers'
        print self.headerdata

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

    def columnCount(self, parent=QtCore.QModelIndex()):
        if has_pandas:
          return len(self.datatable.columns.values)
        else:
          return len(self.datatable.columns)

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if role == QtCore.Qt.DisplayRole:
            i = index.row()
            j = index.column()
            return QtCore.QVariant('{0}'.format(self.datatable.iget_value(i, j)))
        else:
            return QtCore.QVariant()

    def setData(self, index, value, role=QtCore.Qt.DisplayRole):
        if index.column() == 4:
            self.datatable.iset_value(index.row(), 4, value)
            return value
        return value

    def headerData(self, col, orientation, role):
        if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
            return '{0}'.format(self.headerdata[col])

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


class TableView(QtGui.QTableView):
    """
    A simple table to demonstrate the QComboBox delegate.
    """
    def __init__(self, *args, **kwargs):
        QtGui.QTableView.__init__(self, *args, **kwargs)
        self.setItemDelegateForColumn(4, CheckBoxDelegate(self))


class CheckBoxDelegate(QtGui.QStyledItemDelegate):
    """
    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):
        '''
        Important, otherwise an editor is created if the user clicks in this cell.
        ** Need to hook up a signal to the model
        '''
        return None

    def paint(self, painter, option, index):
        '''
        Paint a checkbox without the label.
        '''

        checked = index.data().toBool()
        check_box_style_option = QtGui.QStyleOptionButton()

        if (index.flags() & QtCore.Qt.ItemIsEditable) > 0:
            check_box_style_option.state |= QtGui.QStyle.State_Enabled
        else:
            check_box_style_option.state |= QtGui.QStyle.State_ReadOnly

        if checked:
            check_box_style_option.state |= QtGui.QStyle.State_On
        else:
            check_box_style_option.state |= QtGui.QStyle.State_Off

        check_box_style_option.rect = self.getCheckBoxRect(option)

        # this will not run - hasFlag does not exist
        #if not index.model().hasFlag(index, QtCore.Qt.ItemIsEditable):
            #check_box_style_option.state |= QtGui.QStyle.State_ReadOnly

        check_box_style_option.state |= QtGui.QStyle.State_Enabled

        QtGui.QApplication.style().drawControl(QtGui.QStyle.CE_CheckBox, check_box_style_option, painter)

    def editorEvent(self, event, model, option, index):
        '''
        Change the data in the model and the state of the checkbox
        if the user presses the left mousebutton or presses
        Key_Space or Key_Select and this cell is editable. Otherwise do nothing.
        '''
        print 'Check Box editor Event detected : '
        print event.type()
        if not (index.flags() & QtCore.Qt.ItemIsEditable) > 0:
            return False

        print 'Check Box editor Event detected : passed first check'
        # Do not change the checkbox-state
        if event.type() == QtCore.QEvent.MouseButtonPress:
          return False
        if event.type() == QtCore.QEvent.MouseButtonRelease or event.type() == QtCore.QEvent.MouseButtonDblClick:
            if event.button() != QtCore.Qt.LeftButton or not self.getCheckBoxRect(option).contains(event.pos()):
                return False
            if event.type() == QtCore.QEvent.MouseButtonDblClick:
                return True
        elif event.type() == QtCore.QEvent.KeyPress:
            if event.key() != QtCore.Qt.Key_Space and event.key() != QtCore.Qt.Key_Select:
                return False
        else:
            return False

        # Change the checkbox-state
        self.setModelData(None, model, index)
        return True

    def setModelData (self, editor, model, index):
        '''
        The user wanted to change the old state in the opposite.
        '''
        print 'SetModelData'
        newValue = not index.data().toBool()
        print 'New Value : {0}'.format(newValue)
        model.setData(index, newValue, QtCore.Qt.EditRole)

    def getCheckBoxRect(self, option):
        check_box_style_option = QtGui.QStyleOptionButton()
        check_box_rect = QtGui.QApplication.style().subElementRect(QtGui.QStyle.SE_CheckBoxIndicator, check_box_style_option, None)
        check_box_point = QtCore.QPoint (option.rect.x() +
                            option.rect.width() / 2 -
                            check_box_rect.width() / 2,
                            option.rect.y() +
                            option.rect.height() / 2 -
                            check_box_rect.height() / 2)
        return QtCore.QRect(check_box_point, check_box_rect.size())


###############################################################################################################################
class Dataframe(dict):
  def __init__(self, columns, values):
    if len(values) != len(columns):
      raise Exception("Bad values")
    self.columns = columns
    self.values = values
    self.index = values[0]
    super(Dataframe, self).__init__(dict(zip(columns, values)))
    pass

  def iget_value(self, i, j):
    return(self.values[j][i])

  def iset_value(self, i, j, value):
    self.values[j][i] = value


if __name__=="__main__":
    from sys import argv, exit

    class Widget(QtGui.QWidget):
        """
        A simple test widget to contain and own the model and table.
        """
        def __init__(self, parent=None):
            QtGui.QWidget.__init__(self, parent)

            l=QtGui.QVBoxLayout(self)
            cdf = self.get_data_frame()
            self._tm=TableModel(self)
            self._tm.update(cdf)
            self._tv=TableView(self)
            self._tv.setModel(self._tm)
            for row in range(0, self._tm.rowCount()):
                self._tv.openPersistentEditor(self._tm.index(row, 4))
            self.setGeometry(300, 300, 550, 200)
            l.addWidget(self._tv)

        def get_data_frame(self):
            if has_pandas:
              df = pd.DataFrame({'Name':['a','b','c','d'],
              'First':[2.3,5.4,3.1,7.7], 'Last':[23.4,11.2,65.3,88.8], 'Class':[1,1,2,1], 'Valid':[True, False, True, False]})
            else:
              columns = ['Name', 'First', 'Last', 'Class', 'Valid']
              values = [['a','b','c','d'], [2.3,5.4,3.1,7.7], [23.4,11.2,65.3,88.8], [1,1,2,1], [True, False, True, False]]
              df = Dataframe(columns, values)
            return df

    a=QtGui.QApplication(argv)
    w=Widget()
    w.show()
    w.raise_()
    exit(a.exec_())

是的,在修复数据反射后,我也得出了类似的解决方案。设置数据方法是明显的罪魁祸首。你的方法看起来不错,我很乐意接受你的答案。可以进行一些改进。例如,我不喜欢将setData()和flags()函数分别针对每列实现,但绕过这个问题编码起来应该不会太难。 - C Mars
属性错误:'NoneType'对象没有'toBool'属性。出现了许多其他警告,这可能需要重新调整,特别是适应不使用QVariants的PySide... - eric
尝试适应QStandardItemModel,但只有一个复选框显示,已发布后续问题:http://stackoverflow.com/questions/31478121/view-qstandarditemmodel-with-column-of-checkboxes-in-pyside-pyqt - eric
QtCore.QEvent.KeyPress 块中的 else 块是否有意放置,它的作用是什么? - Tim
我认为这是一个打字错误。否则的代码块应该返回True(就我所记得的脚本而言)。 - Frodon
@Frodon 我刚刚弄明白了。缩进不正确,else块应该属于event.type()检查。否则你会在鼠标移动期间看到激活。不幸的是,我不得不进行一些更多(毫无意义的)更改来满足StackOverflow的6个字符限制。 - Tim

4

我添加了

if event.type() == QEvent.MouseButtonPress or event.type() == QEvent.MouseMove:
      return False

为了防止鼠标移动时复选框闪烁,请使用以下方法:

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接