如何使用 QStyledItemDelegate 将 QTableWidgetItem 的大小调整为其编辑器大小然后调整为其文本大小?

How to resize QTableWidgetItem to its editor size then to its text size using QStyledItemDelegate?

我创建了我自己的 CustomDelegate class 来自 QStyledItemDelegate:

class CustomDelegate(QStyledItemDelegate):
    def __init__(self, parent):
        super().__init__(parent)

    def createEditor(self, parent, option, index):
        editor = QWidget(parent)
        editor_hlayout = QHBoxLayout(editor)
        button = QPushButton()
        line_edit = QLineEdit()
        editor_hlayout.addWidget(button)
        editor_hlayout.addWidget(line_edit)
        return editor

    def setEditorData(self, editor, index):
        model_data = index.model().data(index, Qt.EditRole)
        editor.layout().itemAt(1).widget().setText(model_data) # Set line_edit value

    def setModelData(self, editor, model, index):
        editor_data =  editor.layout().itemAt(1).widget().text() # Get line_edit value
        model.setData(index, editor_data, Qt.EditRole)
        
    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)

我将它设置为 QTableWidget 的最后一列 my_table :

my_delegate = CustomDelegate(my_window)
my_table.setItemDelegateForColumn(my_table.columnCount()-1, my_delegate)

准确地说,我的目标是在双击时编辑 table 小部件项目大小,使其适合编辑器大小并正确显示,然后编辑 table 小部件项目退出编辑器模式后立即调整大小,使其再次适合其文本大小。

为此,我在 createEditor 方法中添加了 index.model().setData(index, editor.sizeHint(), Qt.SizeHintRole) 行,在 setModelData 方法中添加了 model.setData(index, QTableWidgetItem(editor_data).sizeHint(), Qt.SizeHintRole) 行。

问题是QTableWidgetItem(editor_data).sizeHint() returns (-1, -1) 大小。我也尝试过 QTextDocument(editor_data).size() 但它不适合文本宽度(它略小)。

项视图的编辑器不应更改其索引的大小。这被认为有效的唯一情况是 persistent 编辑器和索引小部件,但由于它们的性质,这是有道理的:它们应该在视图上持久存在,而不仅仅是它被接受table 他们需要视图来最终扩展他们的行或列,但为了避免编辑器隐藏其他项目(或编辑器)也是必要的。

部分大小的任何变化都可能对视图要求很高,特别是如果它有大量数据并且任何 header 使用 ResizeToContents 模式,这就是默认工厂的原因编辑器(QLineEdit、QDate/TimeEdit 和 Q[Double]SpinBox)不会更新索引大小,但最终会暂时扩展它们的几何形状。

我建议遵循这种做法,并最终根据编辑位置更新 updateEditorGeometry() 中的几何图形。

为了优化可用的 space,您可以采取一些预防措施:

  • 为布局明确设置 0 边距;
  • 最小化小部件之间的间距;
  • 禁用 QLineEdit 的框架(与默认编辑器一样);
  • 使用QToolButton代替QPushButton,因为它通常可以做得比后者小;这也使一些焦点方面变得更容易,因为 QToolButton 不通过单击接受焦点;

另请注意:

  • 编辑器应使用 setAutoFillBackground(True) 否则部分基础项目可能可见;
  • 行编辑应该默认聚焦,您可以在编辑器上使用 setFocusProxy(),这样行编辑在 parent 聚焦时得到聚焦;
  • 您不应使用布局来访问行编辑,而应在编辑器中为其创建引用 object;
  • 如果您仍想让编辑器边框可见,请使用适当的样式表;
    def createEditor(self, parent, option, index):
        editor = QWidget(parent)
        editor.setAutoFillBackground(True)
        editor_hlayout = QHBoxLayout(editor)
        editor_hlayout.setContentsMargins(0, 0, 0, 0)
        editor_hlayout.setSpacing(1)
        button = QToolButton()
        editor.line_edit = QLineEdit(frame=False)
        editor_hlayout.addWidget(button)
        editor_hlayout.addWidget(editor.line_edit)
        editor.setFocusProxy(editor.line_edit)

        # eventually (see note)
        editor.setObjectName('delegateEditor')
        editor.setStyleSheet('''
            #delegateEditor {
                border: 1px solid palette(mid); 
                background: palette(base);
            }
        ''')
        return editor

    def setEditorData(self, editor, index):
        model_data = index.model().data(index, Qt.EditRole)
        editor.line_edit.setText(model_data)

    def setModelData(self, editor, model, index):
        editor_data =  editor.line_edit.text()
        model.setData(index, editor_data, Qt.EditRole)

    def updateEditorGeometry(self, editor, option, index):
        super().updateEditorGeometry(editor, option, index)
        editor.resize(editor.sizeHint().width(), editor.height())
        rect = editor.geometry()
        parentRect = editor.parent().rect()
        if not parentRect.contains(rect):
            if rect.right() > parentRect.right():
                rect.moveRight(parentRect.right())
            if rect.x() < parentRect.x():
                rect.moveLeft(parentRect.x())
            if rect.bottom() > parentRect.bottom():
                rect.moveBottom(parentRect.bottom())
            if rect.y() < parentRect.y():
                rect.moveTop(parentRect.y())
            editor.setGeometry(rect)

注意:如果您使用样式表,行编辑可能会部分覆盖边框;在这种情况下,使用 editor_hlayout.setContentsMargins(0, 0, 1, 0).


也就是说,如果您真的想更新视图大小,您仍然可以,但可能有点棘手。

诀窍是跟踪编辑器及其索引,手动调整各部分的大小以适应其大小,然后在编辑器被销毁时恢复这些大小。

class CustomDelegate(QStyledItemDelegate):
    def __init__(self, parent):
        self.editorData = {}
        super().__init__(parent)

    def createEditor(self, parent, option, index):
        editor = QWidget(parent)
        editor.setAutoFillBackground(True)
        editor_hlayout = QHBoxLayout(editor)
        editor_hlayout.setContentsMargins(0, 0, 0, 0)
        editor_hlayout.setSpacing(1)
        button = QToolButton()
        editor.line_edit = QLineEdit(frame=False)
        editor_hlayout.addWidget(button)
        editor_hlayout.addWidget(editor.line_edit)
        editor.setFocusProxy(editor.line_edit)
        view = option.widget
        # store the editor, the view and also the current sizes
        self.editorData[index] = (editor, view, 
            view.horizontalHeader().sectionSize(index.column()), 
            view.verticalHeader().sectionSize(index.row())
            )
        # THEN, resize the row and column, which will call sizeHint()
        view.resizeColumnToContents(index.column())
        view.resizeRowToContents(index.row())
        # delay a forced scroll to the index to ensure that the editor is
        # visible after the sections have been resized
        QTimer.singleShot(1, lambda: view.scrollTo(index))
        return editor

    def sizeHint(self, opt, index):
        if index in self.editorData:
            # sizeHint doesn't provide access to the editor
            editor, *_ = self.editorData[index]
            return editor.sizeHint()
        return super().sizeHint(opt, index)

    def destroyEditor(self, editor, index):
        super().destroyEditor(editor, index)
        if index in self.editorData:
            editor, view, width, height = self.editorData.pop(index)
            view.horizontalHeader().resizeSection(index.column(), width)
            view.verticalHeader().resizeSection(index.row(), height)

请注意,如果索引靠近边缘(最后一行或最后一列),当编辑器被销毁时,如果滚动模式为 ScrollPerItem,则视图可能无法正确调整其滚动条。据我所知,没有简单的解决方法。

记住上面说的,不过。作为Ian Malcolm would say,能不能做到,你应该停下来想一想。

关于焦点问题的更新

正如评论中所指出的,如果用户在编辑器处于活动状态时单击其他位置,编辑器不会按预期自行关闭。

默认情况下,委托的事件过滤器会在失去焦点时关闭编辑器,但过滤器是在编辑器上设置的,而不是它的 children。当 line edit 获得焦点时,事件过滤器识别出新的焦点 widget 是编辑器的一个 child 并且不会关闭它;这很重要,因为如果编辑器有 child 个小部件,您不希望它仅仅因为内部焦点已更改而关闭;这样做的副作用是,当在行编辑外部单击时,接收 FocusOut 事件的是行编辑,而不是编辑器,因此事件过滤器对此一无所知;请注意,即使在使用 setFocusProxy() 时也会发生这种情况,因为 Qt 会自动将焦点事件发送到代理。

根据编辑器类型,至少有两种可能的解决方案。

如果只有 一个 主要 child 小部件应该接受焦点,就像在这种情况下一样,解决方案是做其他复杂的小部件(比如QComboBox 和QAbstractSpinBox): 将编辑器设置为widget 的焦点代理,并且post 事件过滤器中的widget 的相关事件:

    def createEditor(self, parent, option, index):
        # ...
        editor.line_edit.setFocusProxy(editor)

    def eventFilter(self, editor, event):
        if event.type() in (
            event.FocusIn, event.FocusOut, 
            event.KeyPress, event.KeyRelease, 
            event.ShortcutOverride, event.InputMethod, 
            event.ContextMenu
            ):
                editor.line_edit.event(event)
                if event.type() != event.FocusOut:
                    return event.isAccepted()
        return super().eventFilter(editor, event)

对于更复杂的情况,我们需要确保当编辑器或其任何 children 完全失去焦点时,委托事件过滤器的默认实现会收到一个焦点移出事件。

为了实现这一点,一个可能的解决方案是创建一个自定义 object,它将成为 所有 child 个小部件的事件过滤器编辑器,并最终 posts 当焦点小部件不是它本身或其 children 之一(包括 grandchild仁).

class FocusOutFilter(QObject):
    def __init__(self, widget):
        super().__init__(widget)
        self.widget = widget
        self.install(widget)

    def install(self, widget):
        widget.installEventFilter(self)
        for child in widget.findChildren(QWidget):
            child.installEventFilter(self)

    def eventFilter(self, obj, event):
        if (event.type() == event.FocusOut and
            not self.widget.isAncestorOf(QApplication.focusWidget())):
                obj.removeEventFilter(self)
                self.deleteLater()
                return QApplication.sendEvent(self.widget, event)
        elif event.type() == event.ChildAdded:
            self.install(event.child())
        return super().eventFilter(obj, event)


class CustomDelegate(QStyledItemDelegate):
    # ...
    def createEditor(self, parent, option, index):
        editor = QWidget(parent)
        editor.filter = FocusOutFilter(editor)
        # ...

有个问题。问题是即使事件导致打开菜单,focusWidget() 也会 return 小部件。虽然这对于编辑器小部件的任何上下文菜单(包括 QLineEdit 的编辑菜单)都很好,但当视图实现上下文菜单事件(在 [=24= 中显示菜单],在 [=25 中显示菜单时,它可能会产生一些问题=] 事件或使用 CustomContextMenu 政策)。对此没有最终解决方案,因为没有确定的方法来检查 什么 创建了菜单:正确的实现期望 QMenu 是使用创建它的 parent 创建的,但这不能确定(为简单起见,可以在没有任何 parent 的情况下创建动态创建的菜单,这使得无法知道 是什么 创建了它们)。在这些情况下,最简单的解决方案是通过首先正确检查 isPersistentEditorOpen() and ensure that widgetAt() 的 return 值 return 和 table 的 viewport() 来实现上述条件。