如何使用 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() 但它不适合文本宽度(它略小)。
- 如何获得良好的文字大小提示?
- 另外,有人告诉我要减少编辑器的最小尺寸提示以避免编辑 QTableWidgetItem 的尺寸。它是否解决了我的问题以及如何执行它?
项视图的编辑器不应更改其索引的大小。这被认为有效的唯一情况是 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()
来实现上述条件。
我创建了我自己的 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() 但它不适合文本宽度(它略小)。
- 如何获得良好的文字大小提示?
- 另外,有人告诉我要减少编辑器的最小尺寸提示以避免编辑 QTableWidgetItem 的尺寸。它是否解决了我的问题以及如何执行它?
项视图的编辑器不应更改其索引的大小。这被认为有效的唯一情况是 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()
来实现上述条件。