I was using a Checkbox Delegate in my QTableView originally created from around the time of PyQt 4.8 or PySide 1.2.1. It was also working in PySide2, but when I tried to update my application to PySide6, it no longer worked (I now no longer remember exactly what the problem was).
A newer version was posted for PyQt5 at https://stackoverflow.com/a/50314085/224310 which mostly worked except that the delegate was a subclass of QItemDelegate
instead of QStyledItemDelegate
the checkboxes were drawn horribly large. Others seemed to have the same issue, as I found at https://forum.qt.io/topic/142124/pyqt-checkbox-resolution-for-different-scales, but the poster indicated they ultimately solved their problem "by implementing the checkboxes through Qt.ItemIsUserCheckable
, rather than through a delegate". I tried the same and it worked, but it seems that checkboxes implemented through Qt.ItemIsUserCheckable
are not able to be centered in a column of a QTableView. To center them you need... a delegate.
I've spent hours upon hours researching and trying different solutions and it amazes me that what seems like a common use-case of a column of centered checkboxes in a QTableView is so difficult to implement in Python. (Ok, I've worked with Qt long enough that I'm not really amazed.)
Does anyone have any suggestions on how to implement a column of check boxes in PySide6 that are centered in a column and do not look horrible at large resolutions?
(I'll be answering my own question and posting my ultimate solution below, but I'd welcome other (better) suggestions as well!)
Reading posts with someone else's similar struggles, I ran across a link to a C++ solution at https://wiki.qt.io/Center_a_QCheckBox_or_Decoration_in_an_Itemview. I was not able to find a similar solution written in Python directly. I don't know C++, but with the help of an online conversion tool, I was able to get version that looked somewhat-like-Python, which I was then able to rewrite to work with PySide6 and with something more modern (I'm using Python 3.12).
from PySide6 import QtCore, QtWidgets, QtGui
class StyledCheckboxDelegate(QtWidgets.QStyledItemDelegate):
"""Centered checkbox delegate to use in a QTableView.
Adapted from https://wiki.qt.io/Center_a_QCheckBox_or_Decoration_in_an_Itemview
"""
def __init__(self, parent=None):
super().__init__(parent=parent)
def paint(self, painter, option, index):
self.initStyleOption(option, index)
widget = option.widget
style = widget.style() if widget else QtWidgets.QApplication.style()
style.drawPrimitive(
QtWidgets.QStyle.PrimitiveElement.PE_PanelItemViewItem,
option,
painter,
widget
)
if (QtWidgets.QStyleOptionViewItem.ViewItemFeature.HasCheckIndicator
in option.features):
if option.checkState == QtCore.Qt.CheckState.Unchecked:
option.state |= QtWidgets.QStyle.StateFlag.State_Off
elif option.checkState == QtCore.Qt.CheckState.PartiallyChecked:
option.state |= QtWidgets.QStyle.StateFlag.State_NoChange
elif option.checkState == QtCore.Qt.CheckState.Checked:
option.state |= QtWidgets.QStyle.StateFlag.State_On
rect = style.subElementRect(
QtWidgets.QStyle.SubElement.SE_ItemViewItemCheckIndicator,
option,
widget
)
option.rect = QtWidgets.QStyle.alignedRect(
option.direction,
QtCore.Qt.AlignmentFlag(
index.data(QtCore.Qt.ItemDataRole.TextAlignmentRole).value
),
rect.size(),
option.rect
)
option.state &= ~QtWidgets.QStyle.StateFlag.State_HasFocus
style.drawPrimitive(
QtWidgets.QStyle.PrimitiveElement.PE_IndicatorItemViewItemCheck,
option,
painter,
widget
)
elif not option.icon.isNull():
icon_rect = style.subElementRect(
QtWidgets.QStyle.SubElement.SE_ItemViewItemDecoration,
option,
widget
)
icon_rect = QtWidgets.QStyle.alignedRect(
option.direction,
QtCore.Qt.AlignmentFlag(
index.data(QtCore.Qt.ItemDataRole.TextAlignmentRole).value
),
icon_rect.size(),
option.rect
)
mode = QtGui.QIcon.Mode.Normal
if QtWidgets.QStyle.StateFlag.State_Enabled not in option.state:
mode = QtGui.QIcon.Mode.Disabled
elif QtWidgets.QStyle.StateFlag.State_Selected in option.state:
mode = QtGui.QIcon.Mode.Selected
state = (
QtGui.QIcon.State.On
if QtWidgets.QStyle.StateFlag.State_Open in option.state
else QtGui.QIcon.State.Off
)
option.icon.paint(
painter,
icon_rect,
option.decorationAlignment,
mode,
state
)
else:
super().paint(painter, option, index)
def editorEvent(self, event, model, option, index):
# Make sure that the item is checkable
flags = model.flags(index)
if (QtCore.Qt.ItemFlag.ItemIsUserCheckable not in flags
or QtWidgets.QStyle.StateFlag.State_Enabled not in option.state
or QtCore.Qt.ItemFlag.ItemIsEnabled not in flags):
return False
# Make sure that we have a check state
state = index.data(QtCore.Qt.ItemDataRole.CheckStateRole)
if state is None:
return False
widget = option.widget
style = widget.style() if widget else QtWidgets.QApplication.style()
# Make sure that we have the right event type
if event.type() in (
QtCore.QEvent.Type.MouseButtonRelease,
QtCore.QEvent.Type.MouseButtonDblClick,
QtCore.QEvent.Type.MouseButtonPress,
):
view_opt = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(view_opt, index)
check_rect = style.subElementRect(
QtWidgets.QStyle.SubElement.SE_ItemViewItemCheckIndicator,
view_opt,
widget,
)
check_rect = QtWidgets.QStyle.alignedRect(
view_opt.direction,
QtCore.Qt.AlignmentFlag(
index.data(QtCore.Qt.ItemDataRole.TextAlignmentRole).value
),
check_rect.size(),
view_opt.rect,
)
if (isinstance(event, QtGui.QMouseEvent)
and (event.button() != QtCore.Qt.MouseButton.LeftButton
or not check_rect.contains(event.position().toPoint())
)):
return False
if event.type() in (
QtCore.QEvent.Type.MouseButtonPress,
QtCore.QEvent.Type.MouseButtonDblClick,
):
return True
elif event.type() == QtCore.QEvent.Type.KeyPress:
if event.key() not in (
QtCore.Qt.Key.Key_Space,
QtCore.Qt.Key.Key_Select,
):
return False
else:
return False
# convert to an Enum for easy comparison
state = QtCore.Qt.CheckState(state)
if QtCore.Qt.ItemFlag.ItemIsUserTristate in flags:
state = QtCore.Qt.CheckState((state.value + 1) % 3)
else:
state = (
QtCore.Qt.CheckState.Checked
if state == QtCore.Qt.CheckState.Unchecked
else QtCore.Qt.CheckState.Unchecked
)
# set the new checkbox state in the model (as its Enum value)
return model.setData(
index,
state.value,
QtCore.Qt.ItemDataRole.CheckStateRole,
)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
model = QtGui.QStandardItemModel()
model.setColumnCount(1)
model.setRowCount(2)
checkable_item = QtGui.QStandardItem()
checkable_item.setFlags(
checkable_item.flags() | QtCore.Qt.ItemFlag.ItemIsUserCheckable
)
checkable_item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
checkable_item.setCheckState(QtCore.Qt.CheckState.Checked)
model.setItem(0, 0, checkable_item)
checkable_item = QtGui.QStandardItem()
checkable_item.setFlags(
checkable_item.flags() | QtCore.Qt.ItemFlag.ItemIsUserCheckable
)
checkable_item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
checkable_item.setCheckState(QtCore.Qt.CheckState.Checked)
model.setItem(1, 0, checkable_item)
table_view = QtWidgets.QTableView()
table_view.setModel(model)
table_view.setItemDelegate(StyledCheckboxDelegate())
#table_view.setItemDelegateForColumn(0, StyledCheckboxDelegate()) # when only a single column should use the delegate
table_view.show()
sys.exit(app.exec())
Hopefully this saves someone else (or my future self) some hours of time looking for a similar solution!