Я использовал делегата флажка в своем QTableView, первоначально созданном примерно во времена PyQt 4.8 или PySide 1.2.1. В PySide2 это тоже работало, но когда я попытался обновить свое приложение до PySide6, оно уже не работало (сейчас я уже не помню точно, в чем была проблема).
Более новая версия была опубликована для PyQt5 по адресу https://stackoverflow.com/a/50314085/224310 , которая в основном работала, за исключением того, что делегат был подклассом QItemDelegate
вместо QStyledItemDelegate
, флажки были нарисованы ужасно большими. У других, похоже, была такая же проблема, как я нашел на https://forum.qt.io/topic/142124/pyqt-checkbox-solves-for-dependent-scales, но на плакате было указано, что они в конечном итоге решили свою проблему. «путем установки флажков через Qt.ItemIsUserCheckable
, а не через делегата». Я попробовал то же самое, и это сработало, но кажется, что флажки, реализованные с помощью Qt.ItemIsUserCheckable
, не могут быть центрированы в столбце QTableView. Чтобы сосредоточить их, вам нужен... делегат.
Я часами исследовал и пробовал разные решения, и меня поражает то, что то, что кажется распространенным вариантом использования столбца центрированных флажков в QTableView, так сложно реализовать на Python. (Хорошо, я работал с Qt достаточно долго, поэтому меня это не особо удивляет.)
Есть ли у кого-нибудь предложения о том, как реализовать в PySide6 столбец флажков, которые центрируются в столбце и не выглядят ужасно при больших разрешениях?
(Я отвечу на свой вопрос и опубликую свое окончательное решение ниже, но буду рад и другим (лучшим) предложениям!)
Читая посты с похожими проблемами других людей, я наткнулся на ссылку на решение C++ по адресу https://wiki.qt.io/Center_a_QCheckBox_or_Decoration_in_an_Itemview. Мне не удалось найти подобное решение, написанное непосредственно на Python. Я не знаю C++, но с помощью онлайн-инструмента конвертации мне удалось получить версию, похожую на Python, которую затем удалось переписать для работы с PySide6 и с чем-то более современным (я м, используя 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())
Надеюсь, это сэкономит кому-то другому (или мне самому в будущем) несколько часов времени на поиск аналогичного решения!