resizing columns in headers with checkboxes
-
(Edit): using PyQt6 on Python 3.10.5, Windows 11
Hi I'm kinda new to qt development and I found some code to add a checkbox into the header of a QTableView but it's not working like how I want it to.
The functionality of the checkbox works great, I just need it to look correct.
Right now, after I paint the checkbox, it is covering the text of the header after I call resizeColumnsToContents() as shown below. I tried to reimplement the sectionSizeHint() and sectionSize() in my custom header so it adds space for the checkbox but those functions don't seem to be getting called.
Is there any way to kind of "attach" the checkbox to the text so that it's being taken into account when the columns are resized? or is there any better way to add a checkbox into the header of a QTableView?
Issue where checkbox is covering the header text.
I want it to look something like this, without the checkbox covering the header text.
Minimal example below.
import sys from PyQt6.QtWidgets import QApplication, QTableView, QHeaderView, QStyleOptionButton, QWidget, QVBoxLayout, QStyle from PyQt6.QtCore import Qt, QRect, QModelIndex, QAbstractTableModel class QCheckableHeader(QHeaderView): def __init__(self, checkable, orientation, parent=None): super().__init__(orientation, parent) self.checkboxes = {k: False for k in checkable} def paintSection(self, painter, rect, logicalIndex): painter.save() super().paintSection(painter, rect, logicalIndex) painter.restore() painter.save() if logicalIndex in self.checkboxes: option = QStyleOptionButton() option.rect = QRect(5, 7, 10, 10) option.state = QStyle.StateFlag.State_Enabled if self.checkboxes[logicalIndex]: option.state |= QStyle.StateFlag.State_On else: option.state |= QStyle.StateFlag.State_Off self.style().drawControl(QStyle.ControlElement.CE_CheckBox, option, painter) painter.restore() def mousePressEvent(self, event): idx = self.logicalIndexAt(event.pos()) if idx >= 0 and idx in self.checkboxes: # Ensure the index is valid self.checkboxes[idx] = not self.checkboxes[idx] self.updateSection(idx) super().mousePressEvent(event) def sectionSizeHint(self, logicalIndex): if logicalIndex in self.checkboxes: return super().sectionSizeHint(logicalIndex) + 20 else: return super().sectionSizeHint(logicalIndex) def sectionSize(self, logicalIndex): if logicalIndex in self.checkboxes: return super().sectionSizeHint(logicalIndex) + 20 else: return super().sectionSizeHint(logicalIndex) class CustomTableModel(QAbstractTableModel): def __init__(self, data): super().__init__() self._data = data def data(self, index: QModelIndex, role=Qt.ItemDataRole.DisplayRole): if role == Qt.ItemDataRole.DisplayRole: return self._data[index.row()][index.column()] return None def rowCount(self, index): return len(self._data) def columnCount(self, index): return len(self._data[0]) if self._data else 0 def headerData(self, section, orientation, role): if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: return f"Column {section + 1}" return None class MainWindow(QWidget): def __init__(self): super().__init__() self.setGeometry(40, 80, 300, 200) layout = QVBoxLayout(self) data = [['Row1-Col1', 'Row1-Col2', 'Row1-Col3', 'Row1-Col4'], ['Row2-Col1', 'Row2-Col2', 'Row2-Col3', 'Row2-Col4']] self.model = CustomTableModel(data) self.table_view = QTableView() self.table_view.setModel(self.model) header = QCheckableHeader([0], Qt.Orientation.Horizontal) self.table_view.setHorizontalHeader(header) layout.addWidget(self.table_view) self.setLayout(layout) self.table_view.resizeColumnsToContents() self.table_view.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch) if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec())
-
(Edit): using PyQt6 on Python 3.10.5, Windows 11
Hi I'm kinda new to qt development and I found some code to add a checkbox into the header of a QTableView but it's not working like how I want it to.
The functionality of the checkbox works great, I just need it to look correct.
Right now, after I paint the checkbox, it is covering the text of the header after I call resizeColumnsToContents() as shown below. I tried to reimplement the sectionSizeHint() and sectionSize() in my custom header so it adds space for the checkbox but those functions don't seem to be getting called.
Is there any way to kind of "attach" the checkbox to the text so that it's being taken into account when the columns are resized? or is there any better way to add a checkbox into the header of a QTableView?
Issue where checkbox is covering the header text.
I want it to look something like this, without the checkbox covering the header text.
Minimal example below.
import sys from PyQt6.QtWidgets import QApplication, QTableView, QHeaderView, QStyleOptionButton, QWidget, QVBoxLayout, QStyle from PyQt6.QtCore import Qt, QRect, QModelIndex, QAbstractTableModel class QCheckableHeader(QHeaderView): def __init__(self, checkable, orientation, parent=None): super().__init__(orientation, parent) self.checkboxes = {k: False for k in checkable} def paintSection(self, painter, rect, logicalIndex): painter.save() super().paintSection(painter, rect, logicalIndex) painter.restore() painter.save() if logicalIndex in self.checkboxes: option = QStyleOptionButton() option.rect = QRect(5, 7, 10, 10) option.state = QStyle.StateFlag.State_Enabled if self.checkboxes[logicalIndex]: option.state |= QStyle.StateFlag.State_On else: option.state |= QStyle.StateFlag.State_Off self.style().drawControl(QStyle.ControlElement.CE_CheckBox, option, painter) painter.restore() def mousePressEvent(self, event): idx = self.logicalIndexAt(event.pos()) if idx >= 0 and idx in self.checkboxes: # Ensure the index is valid self.checkboxes[idx] = not self.checkboxes[idx] self.updateSection(idx) super().mousePressEvent(event) def sectionSizeHint(self, logicalIndex): if logicalIndex in self.checkboxes: return super().sectionSizeHint(logicalIndex) + 20 else: return super().sectionSizeHint(logicalIndex) def sectionSize(self, logicalIndex): if logicalIndex in self.checkboxes: return super().sectionSizeHint(logicalIndex) + 20 else: return super().sectionSizeHint(logicalIndex) class CustomTableModel(QAbstractTableModel): def __init__(self, data): super().__init__() self._data = data def data(self, index: QModelIndex, role=Qt.ItemDataRole.DisplayRole): if role == Qt.ItemDataRole.DisplayRole: return self._data[index.row()][index.column()] return None def rowCount(self, index): return len(self._data) def columnCount(self, index): return len(self._data[0]) if self._data else 0 def headerData(self, section, orientation, role): if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: return f"Column {section + 1}" return None class MainWindow(QWidget): def __init__(self): super().__init__() self.setGeometry(40, 80, 300, 200) layout = QVBoxLayout(self) data = [['Row1-Col1', 'Row1-Col2', 'Row1-Col3', 'Row1-Col4'], ['Row2-Col1', 'Row2-Col2', 'Row2-Col3', 'Row2-Col4']] self.model = CustomTableModel(data) self.table_view = QTableView() self.table_view.setModel(self.model) header = QCheckableHeader([0], Qt.Orientation.Horizontal) self.table_view.setHorizontalHeader(header) layout.addWidget(self.table_view) self.setLayout(layout) self.table_view.resizeColumnsToContents() self.table_view.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch) if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec())
@kevl
I have no idea whether this is the way it is supposed to be done, but given your approach:super().paintSection(painter, rect, logicalIndex)
For this call, to paint the header text, don't you want to subtract out (i.e. increase x, decrease width) of the
rect
you pass on the area which will be allocated for a checkbox if you're going to show one? -
@kevl
I have no idea whether this is the way it is supposed to be done, but given your approach:super().paintSection(painter, rect, logicalIndex)
For this call, to paint the header text, don't you want to subtract out (i.e. increase x, decrease width) of the
rect
you pass on the area which will be allocated for a checkbox if you're going to show one?Thanks for the suggestion. That kinda seems to work and the checkbox is no longer overlapped with the text, but the resizeColumnstoContents() still seems to be taking into account of only the size of the text and I'm not sure how to recalculate that as I dont see sectionSizeHint() or sectionSize() being called.
I added this right before the super() call:
if logicalIndex in self.checkboxes: rect.adjust(23, 0, 0, 0)