maggie
maggie

Reputation: 4415

Qt CheckBox delegate generates two checkboxes

I am trying to implement some kind of list view within a PySide GUI which gives the user the opportunity to enable/disable some entries of the list before finally processing the list.

I decided to use a QTableView and QAbstractTableModel with a CheckBoxDelegate class which renders a checkbox for each row in the table view. Checking and unchecking an entry will set the enabled attribute of the underlying list's object accordingly. This allows me to easily skip entries when processing.

I want to draw a centered checkbox. Thus i am using a subclass of QCheckbox within the CheckBoxDelegate based on this SO question https://stackoverflow.com/a/11802138/1504082. Now my problem is that i am getting two checkboxes in column 0. But i dont understand why...

This is my code

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

Can anybody give me an explanation why this happens (and how i could fix it)?

Upvotes: 0

Views: 1492

Answers (1)

strubbly
strubbly

Reputation: 3477

You have the problem because your MyCheckBox class both is a QCheckBox (by inheritance) and also has a QCheckBox by constructing a new QCheckBox instance in its init (self.cb).

You really only want to do one or the other. Just to demonstrate, I rewrote the MyCheckBox class like this:

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

and this fixes the problem (though you need to make some other changes too). Note that the clicked signal you use needs to come from the MyCheckBox not the QCheckBox so I have added it to the containing class via the amClicked slot. You don't need to distinguish the data() and checked_state() methods in your model so I have merged them into one:

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

Then the Delegate looks like this. I have arranged for it only to provide an Editor if the flags say it is editable. If not, then it is responsible for the drawing so it also has to do the correct thing in the paint method.

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

Another approach would be to use inheritance rather than inclusion/delegation. Here's an example using that:

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)

This seems to be more straightforward, however, in this case it has a couple of problems. It is difficult to draw the checkbox centred in the MyCheckBox class - that would need us to override the paintEvent and to do that will need careful drawing. It also will not exactly overwrite the paint of the Delegate. So you could take that out. But then it will only work if the editor has been created for the row. So the first solution is probably easiest in this case.

Upvotes: 2

Related Questions