最后一列中的 QStyledItemDelegate 禁用按钮将选择移动到下一行
QStyledItemDelegate disabling button in last column moves selection to next row
设置说明
- Table 在 PySide 中用
创建
QMainWindow -> QWidget -> QTableView -> TableModel (QAbstractTableModel) -> array[]
- 为第二列和第三列创建 ButtonDelegate(QStyledItemDelegate)
- 第一列中的按钮切换值 - 但由于我的应用程序中的特定原因,按钮必须分开(使用一个按钮切换不是解决方案)
- 第一列值为“隐藏”的按钮
- 只选择了整行(在我的应用程序中很重要,我单独显示所选行的详细数据)
功能的详细说明
- 我的应用程序中的按钮不一定会切换值。最容易解释我的应用程序功能的是配置列表之类的东西。
- 列表中最初是可以选择的通用项目,两个按钮是“+”(add/select) 和“-”(remove/deselect)。
- 有些项目只能添加一次,在这种情况下,按钮实际上只是切换项目选择。如果未选中,则仅显示按钮“+”,如果选中,则仅显示按钮“-”。
- 有些项目可以添加多次。在那种情况下,最初该项目未被选择。按“+”选择项目,显示“-”按钮,但按钮“+”仍然显示,因为项目可以添加多次。再次按下“+”时,将添加具有相同项目的下一行,同时显示“+”和“-”。然后按“-”以相反的方式工作,删除按下“-”的行,直到同一类型的最后一个项目,其中“-”导致未选择的项目。因此 +/- 的功能取决于含量。
- 我决定将按钮放在单独的列中的原因很少 - 保留根据选择状态排序的可能性,header 为“+”显示“添加”,为“-”显示“删除”
问题描述
- 当最后一列中的按钮被禁用时(按下 False 按钮,然后按下 True 按钮),选择会移动到下一行 - 应该保持不变
- 另外,活动按钮的显示和隐藏可能应该在画图(而不是 openPersistentEditor)中完成。我正在查看 google 的文档和示例以找到方法,但我仍然没有弄明白。如果你能告诉我怎么做,我将不胜感激。另外,如果您 link 有关于此主题(绘画)的一些很好的教程,我会很高兴,因为我仍然不知道如何使用它。
最小功能示例:
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QAbstractItemView
from PySide6.QtWidgets import QTableView, QWidget, QStyledItemDelegate, QPushButton
from PySide6.QtCore import Qt, QModelIndex, QAbstractTableModel, QItemSelectionModel
class TrueButtonDelegate(QStyledItemDelegate):
def __init__(self, parent):
QStyledItemDelegate.__init__(self, parent)
def paint(self, painter, option, index):
self.parent().openPersistentEditor(index) # this should be somewhere else, not in paint
def createEditor(self, parent, option, index):
editor = QPushButton('True', parent)
editor.setEnabled(False)
editor.clicked.connect(self.buttonClicked)
return editor
def setEditorData(self, editor, index):
if not index.data():
editor.setText('True')
editor.setEnabled(True)
editor.setFlat(False)
else:
editor.setText('')
editor.setEnabled(False)
editor.setFlat(True)
def setModelData(self, editor, model, index):
model.setData(index, True, role=Qt.EditRole)
def buttonClicked(self):
self.commitData.emit(self.sender())
def eventFilter(self, obj, event):
if event.type() == event.Type.Wheel:
event.setAccepted(False)
return True
return super().eventFilter(obj, event)
class FalseButtonDelegate(QStyledItemDelegate):
def __init__(self, parent):
QStyledItemDelegate.__init__(self, parent)
def paint(self, painter, option, index):
self.parent().openPersistentEditor(index) # this should be somewhere else, not in paint
def createEditor(self, parent, option, index):
editor = QPushButton('False', parent)
editor.setEnabled(True)
editor.clicked.connect(self.buttonClicked)
return editor
def setEditorData(self, editor, index):
if index.data():
editor.setText('False')
editor.setEnabled(True)
editor.setFlat(False)
else:
editor.setText('')
editor.setEnabled(False)
editor.setFlat(True)
def setModelData(self, editor, model, index):
model.setData(index, False, role=Qt.EditRole)
def buttonClicked(self):
self.commitData.emit(self.sender())
def eventFilter(self, obj, event):
if event.type() == event.Type.Wheel:
event.setAccepted(False)
return True
return super().eventFilter(obj, event)
class TableModel(QAbstractTableModel):
def __init__(self, localData=[[]], parent=None):
super().__init__(parent)
self.modelData = localData
def headerData(self, section: int, orientation: Qt.Orientation, role: int):
if role == Qt.DisplayRole:
if orientation == Qt.Vertical:
return "Row " + str(section)
def columnCount(self, parent=None):
return 3
def rowCount(self, parent=None):
return len(self.modelData)
def data(self, index: QModelIndex, role: int):
if role == Qt.DisplayRole:
row = index.row()
return self.modelData[row]
def setData(self, index, value = None, role=Qt.DisplayRole):
row = index.row()
self.modelData[row] = value
index = self.index(row, 0)
self.dataChanged.emit(index, index)
index = self.index(row, 1)
self.dataChanged.emit(index, index)
index = self.index(row, 2)
self.dataChanged.emit(index, index)
return True
app = QApplication()
data = [True, True, True, True, True, True, True, True, True, True, True, True, True, True]
model = TableModel(data)
tableView = QTableView()
tableView.setModel(model)
selectionModel = QItemSelectionModel(model)
tableView.setSelectionModel(selectionModel)
tableView.setItemDelegateForColumn(1, FalseButtonDelegate(tableView))
tableView.setItemDelegateForColumn(2, TrueButtonDelegate(tableView))
tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
tableView.setSelectionMode(QAbstractItemView.SingleSelection)
widget = QWidget()
widget.horizontalHeader = tableView.horizontalHeader()
widget.horizontalHeader.setStretchLastSection(True)
widget.mainLayout = QVBoxLayout()
widget.mainLayout.setContentsMargins(1,1,1,1)
widget.mainLayout.addWidget(tableView)
widget.setLayout(widget.mainLayout)
mainWindow = QMainWindow()
mainWindow.setCentralWidget(widget)
mainWindow.setGeometry(0, 0, 380, 300)
mainWindow.show()
exit(app.exec())
此行为的原因是禁用小部件会自动将焦点设置到焦点链中的下一个可用小部件。
实际行为基于 focusNextPrevChild
的 QAbstractItemView 的 re-implementation,它创建了一个“虚拟”QKeyPressEvent,带有发送到 keyPressEvent()
的制表符(或后制表符)键处理程序。
默认情况下,这会导致调用 table 视图对 moveCursor()
的重新实现,它着重于下一个选择table 项目(在您的情况下下一行中的第一个项目) ).
一个可能的解决方法是使用 QTableView 的子 class 并覆盖 focusNextPrevChild()
;通过这种方式,您可以 first 检查 current 小部件是否是按钮和视口的 child(这意味着它是您的其中一个编辑器),最终 return True 没有做任何其他事情:
class TableView(QTableView):
def focusNextPrevChild(self, isNext):
if isNext:
current = QApplication.focusWidget()
if isinstance(current, QPushButton) and current.parent() == self.viewport():
return True
return super().focusNextPrevChild(isNext)
不幸的是,这不会解决您的实施的主要问题。
实现像您这样复杂的系统,需要特别注意和了解 Qt 视图的工作原理,主要问题与 setModelData()
可能由各种原因触发这一事实有关;其中之一是每当视图的当前索引发生变化时。这可能发生在键盘导航(tab/backtab、箭头等)时,也可能发生在鼠标更改当前选择时:您可以在 UI 中通过单击并按住鼠标按钮在项目上看到这一点在第一列上,然后开始将鼠标拖到有按钮的项目上;由于该操作更改了选择模型,因此这也会触发当前索引更改,因此会触发委托的 setModelData
,因为持久性编辑器已打开。
更好的实现(也不需要单独的委托)意味着知道当前索引对应于“真”列还是“假”列。只要您知道当值为 True
时用于显示内容的列,那么设置值和显示按钮只是比较这三个值的问题:
value = index.data()
trueColumn = index.column() == self.TrueColumn
if value == trueColumn:
# we are in the column that should show the widget
else:
# we are in the other column (whatever it is)
按下按钮时设置数据遵循相同的概念;如果按钮在“true”列(用于将值设置为 False
的那个),则将其设置为 False,反之亦然:
model.setData(index, index.column() != self.TrueColumn, Qt.EditRole)
然后,还需要做一些进一步的调整:
- 为避免焦点问题,您可以通过设置属性
Qt.WA_TransparentForMouseEvents
让编辑器忽略鼠标事件,并通过将焦点策略设置为 No.Focus
来忽略键盘焦点;然后在“恢复”编辑器时恢复默认行为;
- 要使按钮透明,请使用使每个组件不可见的样式表:
color: transparent; background: transparent; border: none;
;
- 不在paint方法中打开编辑器,而是在设置模型时和添加新行时正确调用
openPersistentIndex()
;
- 如果你想隐藏索引的文本,只需覆盖
displayText()
和return一个空字符串即可;通过这种方式,您可以保留显示所选项目的默认绘画行为;
class ButtonDelegate(QStyledItemDelegate):
TrueColumn = 1
isClicked = False
def buttonClicked(self):
self.isClicked = True
self.commitData.emit(self.sender())
self.isClicked = False
def createEditor(self, parent, option, index):
editor = QPushButton(str(index.column() != self.TrueColumn), parent)
editor.clicked.connect(self.buttonClicked)
return editor
def eventFilter(self, editor, event):
if event.type() == event.MouseMove:
editor.mouseMoveEvent(event)
event.setAccepted(True)
return True
return super().eventFilter(editor, event)
def displayText(self, *args):
return ''
def setEditorData(self, editor, index):
value = index.data()
trueColumn = index.column() == self.TrueColumn
if value == trueColumn:
editor.setAttribute(Qt.WA_TransparentForMouseEvents, False)
editor.setStyleSheet('')
editor.setFocusPolicy(Qt.StrongFocus)
if self.isClicked:
editor.setFocus()
self.parent().setCurrentIndex(index)
else:
editor.setAttribute(Qt.WA_TransparentForMouseEvents, True)
editor.setStyleSheet(
'color:transparent; background: transparent; border: none;')
editor.setFocusPolicy(Qt.NoFocus)
def setModelData(self, editor, model, index):
sender = self.sender()
if sender:
model.setData(index, index.column() != self.TrueColumn, Qt.EditRole)
app = QApplication([])
data = [True] * 16
tableView = QTableView()
tableView.setModel(model)
selectionModel = QItemSelectionModel(model)
tableView.setSelectionModel(selectionModel)
delegate = ButtonDelegate(tableView)
tableView.setItemDelegateForColumn(1, delegate)
tableView.setItemDelegateForColumn(2, delegate)
tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
tableView.setSelectionMode(QAbstractItemView.SingleSelection)
def updateEditors(parent, first, last):
for row in range(first, last + 1):
tableView.openPersistentEditor(model.index(row, 1))
tableView.openPersistentEditor(model.index(row, 2))
updateEditors(None, 0, model.rowCount() - 1)
model.rowsInserted.connect(updateEditors)
# ...
进一步的改进将考虑选项卡导航,为此您需要调整模型 和 视图。通过以下修改,按 Tab 键仅在具有有效数据或活动编辑器的索引之间切换:
class TableModel(QAbstractTableModel):
tabPressed = False
def __init__(self, localData=[[]], parent=None):
super().__init__(parent)
self.modelData = localData
def flags(self, index):
flags = super().flags(index)
if 0 < index.column() < self.columnCount() and self.tabPressed:
if (index.column() != 1) == self.modelData[index.row()]:
flags &= ~(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
return flags
def headerData(self, section: int, orientation: Qt.Orientation, role: int):
if role == Qt.DisplayRole and orientation == Qt.Vertical:
return "Row " + str(section)
def columnCount(self, parent=None):
return 3
def rowCount(self, parent=None):
return len(self.modelData)
def data(self, index: QModelIndex, role: int):
if role == Qt.DisplayRole:
return self.modelData[index.row()]
def setData(self, index, value = None, role=Qt.DisplayRole):
row = index.row()
self.modelData[row] = value
# do not emit dataChanged for each index, emit it for the whole range
self.dataChanged.emit(self.index(row, 0), self.index(row, 2))
return True
class TableView(QTableView):
def moveCursor(self, action, modifiers):
self.model().tabPressed = True
new = super().moveCursor(action, modifiers)
self.model().tabPressed = False
return new
# ...
tableView = TableView()
更新:更多选项
我想到还有另一个可用的替代方案:在保持 two-column 要求的同时,只要 table 正确设置 [=68] 就可以有一个委托=]跨度.
这需要一些独创性,并且需要进一步的 class(具有适当的 user 属性 设置),但它可能会提供更好的结果;诀窍是创建一个包含两个按钮的自定义小部件。还需要进行一些进一步的调整(特别是确保在调整列大小时考虑内部小部件的大小)。
class Switch(QWidget):
valueChanged = Signal(bool)
clicked = Signal()
_value = False
def __init__(self, table, column):
super().__init__(table.viewport())
self.setFocusPolicy(Qt.TabFocus)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
self.spacing = self.style().pixelMetric(QStyle.PM_HeaderGripMargin)
layout.setSpacing(self.spacing)
self.buttons = []
for v in range(2):
button = QPushButton(str(bool(v)))
self.buttons.append(button)
layout.addWidget(button)
button.setMinimumWidth(10)
button.clicked.connect(self.buttonClicked)
self.header = table.horizontalHeader()
self.columns = column, column + 1
self.updateButtons(False)
self.header.sectionResized.connect(self.updateSizes)
self.resizeTimer = QTimer(self, interval=0, singleShot=True,
timeout=self.updateSizes)
@Property(bool, user=True, notify=valueChanged)
def value(self):
return self._value
@value.setter
def value(self, value):
if self._value != value:
self._value = value
self.valueChanged.emit(value)
self.updateButtons(self._value)
def updateButtons(self, value):
focused = False
self.setFocusProxy(None)
for i, button in enumerate(self.buttons):
if i != value:
button.setAttribute(Qt.WA_TransparentForMouseEvents, False)
self.setFocusProxy(button)
button.setStyleSheet('')
else:
if button.hasFocus():
focused = True
button.setAttribute(Qt.WA_TransparentForMouseEvents, True)
button.setStyleSheet(
'color: transparent; background: transparent; border: none;')
if focused:
self.setFocus(Qt.MouseFocusReason)
def buttonClicked(self):
button = self.sender()
self.value = bool(self.buttons.index(button))
self.clicked.emit()
def updateSizes(self):
for i, column in enumerate(self.columns):
size = self.header.sectionSize(column)
if i == 0:
size -= self.spacing
self.layout().setStretch(i, size)
self.layout().activate()
def focusNextPrevChild(self, isNext):
return False
def resizeEvent(self, event):
self.updateSizes()
class SwitchButtonDelegate(QStyledItemDelegate):
def displayText(self, *args):
return ''
def createEditor(self, parent, option, index):
editor = Switch(self.parent(), index.column())
def clicked():
if persistent.isValid():
index = persistent.model().index(
persistent.row(), persistent.column(), persistent.parent())
view.setCurrentIndex(index)
view = option.widget
persistent = QPersistentModelIndex(index)
editor.clicked.connect(clicked)
editor.valueChanged.connect(lambda: self.commitData.emit(editor))
return editor
# ...
tableView.setItemDelegateForColumn(1, SwitchButtonDelegate(tableView))
def updateEditors(parent, first, last):
for row in range(first, last + 1):
tableView.setSpan(row, 1, 1, 2)
tableView.openPersistentEditor(model.index(row, 1))
当然,更简单的解决方案是完全避免任何编辑器,并将绘画委托给项目委托。
class PaintButtonDelegate(QStyledItemDelegate):
_pressIndex = _mousePos = None
def __init__(self, trueColumn=0, parent=None):
super().__init__(parent)
self.trueColumn = trueColumn
def paint(self, painter, option, index):
opt = QStyleOptionViewItem(option)
self.initStyleOption(opt, index)
style = opt.widget.style()
opt.text = ''
opt.state |= style.State_Enabled
style.drawControl(style.CE_ItemViewItem, opt, painter, opt.widget)
if index.data() == (index.column() == self.trueColumn):
btn = QStyleOptionButton()
btn.initFrom(opt.widget)
btn.rect = opt.rect
btn.state = opt.state
btn.text = str(index.column() != self.trueColumn)
if self._pressIndex == index and self._mousePos in btn.rect:
btn.state |= style.State_On
if index == option.widget.currentIndex():
btn.state |= style.State_HasFocus
style.drawControl(style.CE_PushButton, btn, painter, opt.widget)
def editorEvent(self, event, model, option, index):
if event.type() == event.MouseButtonPress:
if index.data() == (index.column() == self.trueColumn):
self._pressIndex = index
self._mousePos = event.pos()
option.widget.viewport().update()
elif event.type() == event.MouseMove and self._pressIndex is not None:
self._mousePos = event.pos()
option.widget.viewport().update()
elif event.type() == event.MouseButtonRelease:
if self._pressIndex == index and event.pos() in option.rect:
model.setData(index, not index.data(), Qt.EditRole)
self._pressIndex = self._mousePos = None
option.widget.viewport().update()
elif event.type() == event.KeyPress:
if event.key() == Qt.Key_Space:
value = not index.data()
model.setData(index, value, Qt.EditRole)
newIndex = model.index(index.row(), self.trueColumn + (not value))
option.widget.setCurrentIndex(newIndex)
option.widget.viewport().update()
return super().editorEvent(event, model, option, index)
# ...
delegate = PaintButtonDelegate(1, tableView)
tableView.setItemDelegateForColumn(1, delegate)
tableView.setItemDelegateForColumn(2, delegate)
注意,在这种情况下,如果要保留有效的键盘(Tab)导航,模型也需要调整:
class TableModel(QAbstractTableModel):
# ...
def flags(self, index):
flags = super().flags(index)
if 0 < index.column() < 3:
if index.data() == index.column() - 1:
flags &= ~Qt.ItemIsEnabled
return flags
不幸的是,这会导致水平 header 出现意外行为,因为只有启用的列会以某些特定样式“突出显示”。
也就是说,这种方法的另一个重要缺点是您将完全失去样式提供的任何动画:因为样式使用 actual 小部件来创建视觉动画,并且绘画仅基于当前的 QStylOption 值,那些动画将不可用。
设置说明
- Table 在 PySide 中用
创建 QMainWindow -> QWidget -> QTableView -> TableModel (QAbstractTableModel) -> array[] - 为第二列和第三列创建 ButtonDelegate(QStyledItemDelegate)
- 第一列中的按钮切换值 - 但由于我的应用程序中的特定原因,按钮必须分开(使用一个按钮切换不是解决方案)
- 第一列值为“隐藏”的按钮
- 只选择了整行(在我的应用程序中很重要,我单独显示所选行的详细数据)
功能的详细说明
- 我的应用程序中的按钮不一定会切换值。最容易解释我的应用程序功能的是配置列表之类的东西。
- 列表中最初是可以选择的通用项目,两个按钮是“+”(add/select) 和“-”(remove/deselect)。
- 有些项目只能添加一次,在这种情况下,按钮实际上只是切换项目选择。如果未选中,则仅显示按钮“+”,如果选中,则仅显示按钮“-”。
- 有些项目可以添加多次。在那种情况下,最初该项目未被选择。按“+”选择项目,显示“-”按钮,但按钮“+”仍然显示,因为项目可以添加多次。再次按下“+”时,将添加具有相同项目的下一行,同时显示“+”和“-”。然后按“-”以相反的方式工作,删除按下“-”的行,直到同一类型的最后一个项目,其中“-”导致未选择的项目。因此 +/- 的功能取决于含量。
- 我决定将按钮放在单独的列中的原因很少 - 保留根据选择状态排序的可能性,header 为“+”显示“添加”,为“-”显示“删除”
问题描述
- 当最后一列中的按钮被禁用时(按下 False 按钮,然后按下 True 按钮),选择会移动到下一行 - 应该保持不变
- 另外,活动按钮的显示和隐藏可能应该在画图(而不是 openPersistentEditor)中完成。我正在查看 google 的文档和示例以找到方法,但我仍然没有弄明白。如果你能告诉我怎么做,我将不胜感激。另外,如果您 link 有关于此主题(绘画)的一些很好的教程,我会很高兴,因为我仍然不知道如何使用它。
最小功能示例:
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QAbstractItemView
from PySide6.QtWidgets import QTableView, QWidget, QStyledItemDelegate, QPushButton
from PySide6.QtCore import Qt, QModelIndex, QAbstractTableModel, QItemSelectionModel
class TrueButtonDelegate(QStyledItemDelegate):
def __init__(self, parent):
QStyledItemDelegate.__init__(self, parent)
def paint(self, painter, option, index):
self.parent().openPersistentEditor(index) # this should be somewhere else, not in paint
def createEditor(self, parent, option, index):
editor = QPushButton('True', parent)
editor.setEnabled(False)
editor.clicked.connect(self.buttonClicked)
return editor
def setEditorData(self, editor, index):
if not index.data():
editor.setText('True')
editor.setEnabled(True)
editor.setFlat(False)
else:
editor.setText('')
editor.setEnabled(False)
editor.setFlat(True)
def setModelData(self, editor, model, index):
model.setData(index, True, role=Qt.EditRole)
def buttonClicked(self):
self.commitData.emit(self.sender())
def eventFilter(self, obj, event):
if event.type() == event.Type.Wheel:
event.setAccepted(False)
return True
return super().eventFilter(obj, event)
class FalseButtonDelegate(QStyledItemDelegate):
def __init__(self, parent):
QStyledItemDelegate.__init__(self, parent)
def paint(self, painter, option, index):
self.parent().openPersistentEditor(index) # this should be somewhere else, not in paint
def createEditor(self, parent, option, index):
editor = QPushButton('False', parent)
editor.setEnabled(True)
editor.clicked.connect(self.buttonClicked)
return editor
def setEditorData(self, editor, index):
if index.data():
editor.setText('False')
editor.setEnabled(True)
editor.setFlat(False)
else:
editor.setText('')
editor.setEnabled(False)
editor.setFlat(True)
def setModelData(self, editor, model, index):
model.setData(index, False, role=Qt.EditRole)
def buttonClicked(self):
self.commitData.emit(self.sender())
def eventFilter(self, obj, event):
if event.type() == event.Type.Wheel:
event.setAccepted(False)
return True
return super().eventFilter(obj, event)
class TableModel(QAbstractTableModel):
def __init__(self, localData=[[]], parent=None):
super().__init__(parent)
self.modelData = localData
def headerData(self, section: int, orientation: Qt.Orientation, role: int):
if role == Qt.DisplayRole:
if orientation == Qt.Vertical:
return "Row " + str(section)
def columnCount(self, parent=None):
return 3
def rowCount(self, parent=None):
return len(self.modelData)
def data(self, index: QModelIndex, role: int):
if role == Qt.DisplayRole:
row = index.row()
return self.modelData[row]
def setData(self, index, value = None, role=Qt.DisplayRole):
row = index.row()
self.modelData[row] = value
index = self.index(row, 0)
self.dataChanged.emit(index, index)
index = self.index(row, 1)
self.dataChanged.emit(index, index)
index = self.index(row, 2)
self.dataChanged.emit(index, index)
return True
app = QApplication()
data = [True, True, True, True, True, True, True, True, True, True, True, True, True, True]
model = TableModel(data)
tableView = QTableView()
tableView.setModel(model)
selectionModel = QItemSelectionModel(model)
tableView.setSelectionModel(selectionModel)
tableView.setItemDelegateForColumn(1, FalseButtonDelegate(tableView))
tableView.setItemDelegateForColumn(2, TrueButtonDelegate(tableView))
tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
tableView.setSelectionMode(QAbstractItemView.SingleSelection)
widget = QWidget()
widget.horizontalHeader = tableView.horizontalHeader()
widget.horizontalHeader.setStretchLastSection(True)
widget.mainLayout = QVBoxLayout()
widget.mainLayout.setContentsMargins(1,1,1,1)
widget.mainLayout.addWidget(tableView)
widget.setLayout(widget.mainLayout)
mainWindow = QMainWindow()
mainWindow.setCentralWidget(widget)
mainWindow.setGeometry(0, 0, 380, 300)
mainWindow.show()
exit(app.exec())
此行为的原因是禁用小部件会自动将焦点设置到焦点链中的下一个可用小部件。
实际行为基于 focusNextPrevChild
的 QAbstractItemView 的 re-implementation,它创建了一个“虚拟”QKeyPressEvent,带有发送到 keyPressEvent()
的制表符(或后制表符)键处理程序。
默认情况下,这会导致调用 table 视图对 moveCursor()
的重新实现,它着重于下一个选择table 项目(在您的情况下下一行中的第一个项目) ).
一个可能的解决方法是使用 QTableView 的子 class 并覆盖 focusNextPrevChild()
;通过这种方式,您可以 first 检查 current 小部件是否是按钮和视口的 child(这意味着它是您的其中一个编辑器),最终 return True 没有做任何其他事情:
class TableView(QTableView):
def focusNextPrevChild(self, isNext):
if isNext:
current = QApplication.focusWidget()
if isinstance(current, QPushButton) and current.parent() == self.viewport():
return True
return super().focusNextPrevChild(isNext)
不幸的是,这不会解决您的实施的主要问题。
实现像您这样复杂的系统,需要特别注意和了解 Qt 视图的工作原理,主要问题与 setModelData()
可能由各种原因触发这一事实有关;其中之一是每当视图的当前索引发生变化时。这可能发生在键盘导航(tab/backtab、箭头等)时,也可能发生在鼠标更改当前选择时:您可以在 UI 中通过单击并按住鼠标按钮在项目上看到这一点在第一列上,然后开始将鼠标拖到有按钮的项目上;由于该操作更改了选择模型,因此这也会触发当前索引更改,因此会触发委托的 setModelData
,因为持久性编辑器已打开。
更好的实现(也不需要单独的委托)意味着知道当前索引对应于“真”列还是“假”列。只要您知道当值为 True
时用于显示内容的列,那么设置值和显示按钮只是比较这三个值的问题:
value = index.data()
trueColumn = index.column() == self.TrueColumn
if value == trueColumn:
# we are in the column that should show the widget
else:
# we are in the other column (whatever it is)
按下按钮时设置数据遵循相同的概念;如果按钮在“true”列(用于将值设置为 False
的那个),则将其设置为 False,反之亦然:
model.setData(index, index.column() != self.TrueColumn, Qt.EditRole)
然后,还需要做一些进一步的调整:
- 为避免焦点问题,您可以通过设置属性
Qt.WA_TransparentForMouseEvents
让编辑器忽略鼠标事件,并通过将焦点策略设置为No.Focus
来忽略键盘焦点;然后在“恢复”编辑器时恢复默认行为; - 要使按钮透明,请使用使每个组件不可见的样式表:
color: transparent; background: transparent; border: none;
; - 不在paint方法中打开编辑器,而是在设置模型时和添加新行时正确调用
openPersistentIndex()
; - 如果你想隐藏索引的文本,只需覆盖
displayText()
和return一个空字符串即可;通过这种方式,您可以保留显示所选项目的默认绘画行为;
class ButtonDelegate(QStyledItemDelegate):
TrueColumn = 1
isClicked = False
def buttonClicked(self):
self.isClicked = True
self.commitData.emit(self.sender())
self.isClicked = False
def createEditor(self, parent, option, index):
editor = QPushButton(str(index.column() != self.TrueColumn), parent)
editor.clicked.connect(self.buttonClicked)
return editor
def eventFilter(self, editor, event):
if event.type() == event.MouseMove:
editor.mouseMoveEvent(event)
event.setAccepted(True)
return True
return super().eventFilter(editor, event)
def displayText(self, *args):
return ''
def setEditorData(self, editor, index):
value = index.data()
trueColumn = index.column() == self.TrueColumn
if value == trueColumn:
editor.setAttribute(Qt.WA_TransparentForMouseEvents, False)
editor.setStyleSheet('')
editor.setFocusPolicy(Qt.StrongFocus)
if self.isClicked:
editor.setFocus()
self.parent().setCurrentIndex(index)
else:
editor.setAttribute(Qt.WA_TransparentForMouseEvents, True)
editor.setStyleSheet(
'color:transparent; background: transparent; border: none;')
editor.setFocusPolicy(Qt.NoFocus)
def setModelData(self, editor, model, index):
sender = self.sender()
if sender:
model.setData(index, index.column() != self.TrueColumn, Qt.EditRole)
app = QApplication([])
data = [True] * 16
tableView = QTableView()
tableView.setModel(model)
selectionModel = QItemSelectionModel(model)
tableView.setSelectionModel(selectionModel)
delegate = ButtonDelegate(tableView)
tableView.setItemDelegateForColumn(1, delegate)
tableView.setItemDelegateForColumn(2, delegate)
tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
tableView.setSelectionMode(QAbstractItemView.SingleSelection)
def updateEditors(parent, first, last):
for row in range(first, last + 1):
tableView.openPersistentEditor(model.index(row, 1))
tableView.openPersistentEditor(model.index(row, 2))
updateEditors(None, 0, model.rowCount() - 1)
model.rowsInserted.connect(updateEditors)
# ...
进一步的改进将考虑选项卡导航,为此您需要调整模型 和 视图。通过以下修改,按 Tab 键仅在具有有效数据或活动编辑器的索引之间切换:
class TableModel(QAbstractTableModel):
tabPressed = False
def __init__(self, localData=[[]], parent=None):
super().__init__(parent)
self.modelData = localData
def flags(self, index):
flags = super().flags(index)
if 0 < index.column() < self.columnCount() and self.tabPressed:
if (index.column() != 1) == self.modelData[index.row()]:
flags &= ~(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
return flags
def headerData(self, section: int, orientation: Qt.Orientation, role: int):
if role == Qt.DisplayRole and orientation == Qt.Vertical:
return "Row " + str(section)
def columnCount(self, parent=None):
return 3
def rowCount(self, parent=None):
return len(self.modelData)
def data(self, index: QModelIndex, role: int):
if role == Qt.DisplayRole:
return self.modelData[index.row()]
def setData(self, index, value = None, role=Qt.DisplayRole):
row = index.row()
self.modelData[row] = value
# do not emit dataChanged for each index, emit it for the whole range
self.dataChanged.emit(self.index(row, 0), self.index(row, 2))
return True
class TableView(QTableView):
def moveCursor(self, action, modifiers):
self.model().tabPressed = True
new = super().moveCursor(action, modifiers)
self.model().tabPressed = False
return new
# ...
tableView = TableView()
更新:更多选项
我想到还有另一个可用的替代方案:在保持 two-column 要求的同时,只要 table 正确设置 [=68] 就可以有一个委托=]跨度.
这需要一些独创性,并且需要进一步的 class(具有适当的 user 属性 设置),但它可能会提供更好的结果;诀窍是创建一个包含两个按钮的自定义小部件。还需要进行一些进一步的调整(特别是确保在调整列大小时考虑内部小部件的大小)。
class Switch(QWidget):
valueChanged = Signal(bool)
clicked = Signal()
_value = False
def __init__(self, table, column):
super().__init__(table.viewport())
self.setFocusPolicy(Qt.TabFocus)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
self.spacing = self.style().pixelMetric(QStyle.PM_HeaderGripMargin)
layout.setSpacing(self.spacing)
self.buttons = []
for v in range(2):
button = QPushButton(str(bool(v)))
self.buttons.append(button)
layout.addWidget(button)
button.setMinimumWidth(10)
button.clicked.connect(self.buttonClicked)
self.header = table.horizontalHeader()
self.columns = column, column + 1
self.updateButtons(False)
self.header.sectionResized.connect(self.updateSizes)
self.resizeTimer = QTimer(self, interval=0, singleShot=True,
timeout=self.updateSizes)
@Property(bool, user=True, notify=valueChanged)
def value(self):
return self._value
@value.setter
def value(self, value):
if self._value != value:
self._value = value
self.valueChanged.emit(value)
self.updateButtons(self._value)
def updateButtons(self, value):
focused = False
self.setFocusProxy(None)
for i, button in enumerate(self.buttons):
if i != value:
button.setAttribute(Qt.WA_TransparentForMouseEvents, False)
self.setFocusProxy(button)
button.setStyleSheet('')
else:
if button.hasFocus():
focused = True
button.setAttribute(Qt.WA_TransparentForMouseEvents, True)
button.setStyleSheet(
'color: transparent; background: transparent; border: none;')
if focused:
self.setFocus(Qt.MouseFocusReason)
def buttonClicked(self):
button = self.sender()
self.value = bool(self.buttons.index(button))
self.clicked.emit()
def updateSizes(self):
for i, column in enumerate(self.columns):
size = self.header.sectionSize(column)
if i == 0:
size -= self.spacing
self.layout().setStretch(i, size)
self.layout().activate()
def focusNextPrevChild(self, isNext):
return False
def resizeEvent(self, event):
self.updateSizes()
class SwitchButtonDelegate(QStyledItemDelegate):
def displayText(self, *args):
return ''
def createEditor(self, parent, option, index):
editor = Switch(self.parent(), index.column())
def clicked():
if persistent.isValid():
index = persistent.model().index(
persistent.row(), persistent.column(), persistent.parent())
view.setCurrentIndex(index)
view = option.widget
persistent = QPersistentModelIndex(index)
editor.clicked.connect(clicked)
editor.valueChanged.connect(lambda: self.commitData.emit(editor))
return editor
# ...
tableView.setItemDelegateForColumn(1, SwitchButtonDelegate(tableView))
def updateEditors(parent, first, last):
for row in range(first, last + 1):
tableView.setSpan(row, 1, 1, 2)
tableView.openPersistentEditor(model.index(row, 1))
当然,更简单的解决方案是完全避免任何编辑器,并将绘画委托给项目委托。
class PaintButtonDelegate(QStyledItemDelegate):
_pressIndex = _mousePos = None
def __init__(self, trueColumn=0, parent=None):
super().__init__(parent)
self.trueColumn = trueColumn
def paint(self, painter, option, index):
opt = QStyleOptionViewItem(option)
self.initStyleOption(opt, index)
style = opt.widget.style()
opt.text = ''
opt.state |= style.State_Enabled
style.drawControl(style.CE_ItemViewItem, opt, painter, opt.widget)
if index.data() == (index.column() == self.trueColumn):
btn = QStyleOptionButton()
btn.initFrom(opt.widget)
btn.rect = opt.rect
btn.state = opt.state
btn.text = str(index.column() != self.trueColumn)
if self._pressIndex == index and self._mousePos in btn.rect:
btn.state |= style.State_On
if index == option.widget.currentIndex():
btn.state |= style.State_HasFocus
style.drawControl(style.CE_PushButton, btn, painter, opt.widget)
def editorEvent(self, event, model, option, index):
if event.type() == event.MouseButtonPress:
if index.data() == (index.column() == self.trueColumn):
self._pressIndex = index
self._mousePos = event.pos()
option.widget.viewport().update()
elif event.type() == event.MouseMove and self._pressIndex is not None:
self._mousePos = event.pos()
option.widget.viewport().update()
elif event.type() == event.MouseButtonRelease:
if self._pressIndex == index and event.pos() in option.rect:
model.setData(index, not index.data(), Qt.EditRole)
self._pressIndex = self._mousePos = None
option.widget.viewport().update()
elif event.type() == event.KeyPress:
if event.key() == Qt.Key_Space:
value = not index.data()
model.setData(index, value, Qt.EditRole)
newIndex = model.index(index.row(), self.trueColumn + (not value))
option.widget.setCurrentIndex(newIndex)
option.widget.viewport().update()
return super().editorEvent(event, model, option, index)
# ...
delegate = PaintButtonDelegate(1, tableView)
tableView.setItemDelegateForColumn(1, delegate)
tableView.setItemDelegateForColumn(2, delegate)
注意,在这种情况下,如果要保留有效的键盘(Tab)导航,模型也需要调整:
class TableModel(QAbstractTableModel):
# ...
def flags(self, index):
flags = super().flags(index)
if 0 < index.column() < 3:
if index.data() == index.column() - 1:
flags &= ~Qt.ItemIsEnabled
return flags
不幸的是,这会导致水平 header 出现意外行为,因为只有启用的列会以某些特定样式“突出显示”。
也就是说,这种方法的另一个重要缺点是您将完全失去样式提供的任何动画:因为样式使用 actual 小部件来创建视觉动画,并且绘画仅基于当前的 QStylOption 值,那些动画将不可用。