【问题标题】:QStyledItemDelegate disabling button in last column moves selection to next row最后一列中的 QStyledItemDelegate 禁用按钮将选择移动到下一行
【发布时间】:2022-02-04 13:30:54
【问题描述】:

设置说明

  • PySide 中的表使用
    创建 QMainWindow -> QWidget -> QTableView -> TableModel (QAbstractTableModel) -> array[]
  • 为第二和第三列创建ButtonDelegate(QStyledItemDelegate)
  • 第一列中的按钮切换值 - 但在我的应用程序中,出于特定原因,按钮必须分开(用一个按钮切换不是解决方案)
  • 第一列值为“隐藏”的按钮
  • 只选择了整行(在我的应用程序中很重要,我在所选行上单独显示详细数据)

功能的详细描述

  • 我的应用程序中的按钮不需要切换值。最容易解释我的应用程序的功能是配置列表。
  • 列表中最初是可以选择的通用项目,两个按钮是“+”(添加/选择)和“-”(删除/取消选择)。
  • 有些项目只能添加一次,在这种情况下,按钮实际上只是切换项目选择。如果未选择,则仅显示按钮“+”,如果选择仅显示按钮“-”。
  • 有些项目可以多次添加。在这种情况下,最初该项目未被选中。按“+”选择项目,显示“-”按钮,但仍然显示“+”按钮,因为该项目可以多次添加。再次按下“+”时,将添加具有相同项目的下一行,再次显示“+”和“-”。然后按“-”以相反的方式工作,删除按“-”的行,直到最后一个相同类型的项目,其中“-”导致未选择的项目。因此 +/- 的功能取决于内容。
  • 我决定在单独的列中设置按钮的原因很少 - 保留根据选择状态进行排序的可能性,标题为“+”显示“添加”,为“-”显示“删除”

问题描述

  • 当最后一列中的按钮被禁用时(按下 False 按钮​​,然后按下 True 按钮),选择移至下一行 - 应保持不变
  • 另外,活动按钮的显示和隐藏可能应该在绘画中完成(而不是 openPersistentEditor)。我正在查看来自谷歌的文档和示例以找到方法,但我仍然没有弄清楚。如果你能告诉我怎么做,我将不胜感激。此外,如果您有一些关于这个主题(绘画)的好教程的链接,我会很高兴,因为我仍然不知道如何使用它。

最小功能示例:

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())

【问题讨论】:

  • 您的实现存在一些问题(openPersistentEditor() 除外),但最重要的是,由于每当更改索引时都会调用 setModelData(),因此您的代码每次都会更改模型当其中一个单元格失去焦点时(尝试使用 tab 和 shift-tab ,你会看到)。你说你的按钮必须分开:你能澄清为什么你需要那个吗?此外,不要使用两个单独的列和小部件,为什么不使用带有两个按钮的单个小部件?
  • 是的,用箭头移动也按下按钮。那很不好。如何修复?我在功能的详细描述部分回答了您的其余问题

标签: qt selection pyside qtableview qstyleditemdelegate


【解决方案1】:

这种行为的原因是禁用小部件会自动将焦点设置到焦点链中的下一个可用小部件。

实际行为基于 QAbstractItemView 对focusNextPrevChild 的重新实现,它创建了一个“虚拟”QKeyPressEvent,其中包含一个发送到keyPressEvent() 处理程序的制表符(或后退制表符)键。

默认情况下,这会导致调用表格视图对moveCursor() 的重新实现,它专注于下一个可选择项(在您的情况下为下一行中的第一项)。

可能的解决方法是使用 QTableView 的子类并覆盖 focusNextPrevChild();通过这种方式,您可以首先检查 current 小部件是否是按钮和视口的子项(意味着它是您的编辑器之一),最终只返回 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() 可以由各种原因触发的事实有关;其中之一是视图的当前索引发生变化时。这可能发生在键盘导航(选项卡/后退选项卡、箭头等)中,也可能发生在鼠标更改当前选择时:您可以在 UI 中通过单击并按住鼠标按钮在第一列上的项目上看到这一点,并且然后开始在有按钮的项目上拖动鼠标;由于该操作更改了选择模型,因此这也触发了当前索引更改,并因此触发了委托的setModelData,因为持久编辑器已打开。

更好的实现(也不需要单独的委托)意味着知道当前索引是对应于“true”还是“false”列。只要知道值为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()并返回一个空字符串即可;通过这种方式,您可以保留显示选定项目的默认绘制行为;
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()

更新:更多选项

我突然想到还有另一种可用的替代方法:在保持两列要求的同时,只要表格正确设置了跨度,就可以有一个委托。 p>

这需要一些独创性,并且需要进一步的类(具有适当的 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

不幸的是,这会导致水平标题出现意外行为,因为只有启用的列才会以某些特定样式“突出显示”。

也就是说,这种方法的另一个重要缺点是您将完全失去样式提供的任何动画:因为样式使用 实际 小部件来创建视觉动画,而绘画仅基于在当前的 QStylOption 值上,这些动画将不可用。

【讨论】:

  • 非常感谢。一切都很完美。我不认为解决这个问题会那么复杂,但这可能是初学者的乐观态度。如果你有一些很好的资源来研究这个 Qt 内部工作的主题,你能分享一下吗?
  • @martin_h 拥有两个独立的代表会使事情变得更复杂,拥有一个代表会有所帮助;我突然想到有一个解决方法:使用表跨度(setSpan()),您可以使用单个 QWidget 容器(带有两个按钮)作为编辑器,这至少可以简化焦点和键盘导航。如果以后有时间,我会更新答案。不幸的是,我没有具体的链接建议,其中大部分来自经验,对文档和 Qt 源代码的研究。
  • @martin_h 查看更新。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-01-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-10
相关资源
最近更新 更多