Friday, 25 May 2018

Add checkbox in QTableView header using icons

There is no API to insert a checkbox into the header of a QTableView. The recommended way of achieving this is to subclass the QHeaderView and draw the checkbox in the paintSection() method which is in my opinion overkill for such a simple feature.

In this post, I show how to achieve a similar effect by simply adding icons in the header. The desired effect is shown below:



Code description

First, a table model is created by subclassing the QAbstractTableModel. The data for the table is initialised in self._array (which is a numpy array in this example) and self.test holds the check states for each row in the data. if it was not obvious self.header_icon will hold the current icon in the header. The current icon is determined by the setHeaderIcon() method.

class NumpyModel(QtCore.QAbstractTableModel):
def __init__(self, narray, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self._array = narray.point
self.test = narray.enabled
self.header_icon = None
self.setHeaderIcon()

The QAbstractTableModel.headerData() method is overloaded and the checkbox icon is drawn into the horizontal header of the table.

def headerData(self, index, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DecorationRole:
if index == 3:
return QtCore.QVariant(QtGui.QPixmap(self.header_icon).scaled(100, 100, Qt.KeepAspectRatio, Qt.SmoothTransformation))
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
if index != 3:
return QtCore.QVariant(index+1)
return QtCore.QVariant()

In the setHeaderIcon() method, the header icon is set depending on the states of the checkboxes in the columns. After changing the icon, a headerDataChanged signal is emitted to notify the table view that the header has been changed without this line the header icon will not be updated.

def setHeaderIcon(self):
if numpy.all(self.test == True):
self.header_icon = 'checked.png'
elif numpy.all(self.test == False):
self.header_icon = 'unchecked.png'
else:
self.header_icon = 'intermediate.png'
self.headerDataChanged.emit(Qt.Horizontal, 3, 3)

The icons used are checked.png (left), intermediate.png (middle) and unchecked.png (right).

The toggleCheckState() method toggles the check state of the data. The dataChanged signal is emitted to notify the table that the data has changed and the header is updated by calling setHeaderIcon() method.

def toggleCheckState(self, index):
if index == 3:
if numpy.all(self.test == False):
self.test.fill(True)
else:
self.test.fill(False)
topLeft =self.index(0, 3)
bottomRight = self.index(self.rowCount(), 3)
self.dataChanged.emit(topLeft, bottomRight)
self.setHeaderIcon()

In the main script, a table view is created with a NumpyModel object set as its model. Finally, the sectionPressed signal of the table view header is connected to the toggleState() method of the model, this ensures that clicking on the header changed the data's check state.

The complete python code is available below:

import numpy
from PyQt5 import QtCore, QtWidgets, QtGui
Qt = QtCore.Qt
class NumpyModel(QtCore.QAbstractTableModel):
def __init__(self, narray, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self._array = narray.point
self.test = narray.enabled
self.header_icon = None
self.setHeaderIcon()
def rowCount(self, _parent=None):
return self._array.shape[0]
def columnCount(self, _parent=None):
return self._array.shape[1] + 1
def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return None
if (index.column() == 3):
value = ''
else:
value = QtCore.QVariant(
"%.5f" % self._array[index.row(), index.column()])
if role == QtCore.Qt.EditRole:
return value
elif role == QtCore.Qt.DisplayRole:
return value
elif role == QtCore.Qt.CheckStateRole:
if index.column() == 3:
if self.test[index.row()]:
return QtCore.Qt.Checked
else:
return QtCore.Qt.Unchecked
elif role == Qt.TextAlignmentRole:
return Qt.AlignCenter
return QtCore.QVariant()
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid():
return False
if role == Qt.CheckStateRole and index.column() == 3:
if value == Qt.Checked:
self.test[index.row()] = True
else:
self.test[index.row()] = False
self.setHeaderIcon()
elif role == Qt.EditRole and index.column() != 3:
row = index.row()
col = index.column()
if value.isdigit():
self._array[row, col] = value
return True
def flags(self, index):
if not index.isValid():
return None
if index.column() == 3:
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsUserCheckable
else:
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
def headerData(self, index, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DecorationRole:
if index == 3:
return QtCore.QVariant(QtGui.QPixmap(self.header_icon).scaled(100, 100, Qt.KeepAspectRatio, Qt.SmoothTransformation))
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
if index != 3:
return QtCore.QVariant(index+1)
return QtCore.QVariant()
def toggleCheckState(self, index):
if index == 3:
if numpy.all(self.test == False):
self.test.fill(True)
else:
self.test.fill(False)
topLeft =self.index(0, 3)
bottomRight = self.index(self.rowCount(), 3)
self.dataChanged.emit(topLeft, bottomRight)
self.setHeaderIcon()
def setHeaderIcon(self):
if numpy.all(self.test == True):
self.header_icon = 'checked.png'
elif numpy.all(self.test == False):
self.header_icon = 'unchecked.png'
else:
self.header_icon = 'intermediate.png'
self.headerDataChanged.emit(Qt.Horizontal, 3, 3)
if __name__ == "__main__":
a = QtWidgets.QApplication([])
w = QtWidgets.QTableView()
d = numpy.rec.array([([1., 2., 3.], True), ([4., 5., 6.], False), ([7., 8., 9.], True)],
dtype=[('point', 'f4', 3), ('enabled', '?')])
m = NumpyModel(d)
w.setModel(m)
w.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
w.setAlternatingRowColors(True)
w.verticalHeader().setVisible(False)
w.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
w.horizontalHeader().setSectionResizeMode(3, QtWidgets.QHeaderView.Fixed)
w.horizontalHeader().setMinimumSectionSize(40)
w.horizontalHeader().setDefaultSectionSize(40)
header = w.horizontalHeader()
header.sectionPressed.connect(m.toggleCheckState)
w.show()
a.exec_()
view raw demo_view.py hosted with ❤ by GitHub

4 comments:

  1. This comment has been removed by a blog administrator.

    ReplyDelete
  2. This comment has been removed by a blog administrator.

    ReplyDelete
  3. Good night my friend. I managed to convert your code to PyQt6, but the checkbox in rows didn't accept being selected, just deselected.

    ReplyDelete
    Replies
    1. This worked for me
      https://gist.github.com/StephenNneji/dd94418ecef749525e6251e37c38f25a

      Delete