Reputation: 4415
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
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