使 QTableView 的行随着编辑器高度的增加而扩展
Make row of QTableView expand as editor grows in height
这直接来自 。这是一个 MRE:
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle('Get a grip of table view row height MRE')
self.setGeometry(QtCore.QRect(100, 100, 1000, 800))
layout = QtWidgets.QVBoxLayout()
central_widget = QtWidgets.QWidget( self )
central_widget.setLayout(layout)
self.table_view = SegmentsTableView(self)
self.setCentralWidget(central_widget)
layout.addWidget(self.table_view)
rows = [
['one potatoe two potatoe', 'one potatoe two potatoe'],
['Sed ut perspiciatis, unde omnis iste natus error sit voluptatem accusantium doloremque',
'Sed ut <b>perspiciatis, unde omnis <i>iste natus</b> error sit voluptatem</i> accusantium doloremque'],
['Nemo enim ipsam voluptatem, quia voluptas sit, aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui do lorem ipsum, quia dolor sit amet consectetur adipiscing velit, sed quia non numquam do eius modi tempora incididunt, ut labore et dolore magnam aliquam quaerat voluptatem.',
'Nemo enim ipsam <i>voluptatem, quia voluptas sit, <b>aspernatur aut odit aut fugit, <u>sed quia</i> consequuntur</u> magni dolores eos</b>, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui do lorem ipsum, quia dolor sit amet consectetur adipiscing velit, sed quia non numquam do eius modi tempora incididunt, ut labore et dolore magnam aliquam quaerat voluptatem.'
],
['Ut enim ad minima veniam',
'Ut enim ad minima veniam'],
['Quis autem vel eum iure reprehenderit',
'Quis autem vel eum iure reprehenderit'],
['At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga.',
'At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga.'
]]
for n_row, row in enumerate(rows):
self.table_view.model().insertRow(n_row)
self.table_view.model().setItem(n_row, 0, QtGui.QStandardItem(row[0]))
self.table_view.model().setItem(n_row, 1, QtGui.QStandardItem(row[1]))
self.table_view.setColumnWidth(0, 400)
self.table_view.setColumnWidth(1, 400)
self.qle = QtWidgets.QLineEdit()
layout.addWidget(self.qle)
self._second_timer = QtCore.QTimer(self)
self._second_timer.timeout.connect(self.show_doc_size)
# every 1s
self._second_timer.start(1000)
def show_doc_size(self, *args):
if self.table_view.itemDelegate().editor == None:
self.qle.setText('no editor yet')
else:
self.qle.setText(f'self.table_view.itemDelegate().editor.document().size() {self.table_view.itemDelegate().editor.document().size()}')
class SegmentsTableView(QtWidgets.QTableView):
def __init__(self, parent):
super().__init__(parent)
self.setItemDelegate(SegmentsTableViewDelegate(self))
self.setModel(QtGui.QStandardItemModel())
v_header = self.verticalHeader()
#
v_header.setMinimumSectionSize(5)
v_header.sectionHandleDoubleClicked.disconnect()
v_header.sectionHandleDoubleClicked.connect(self.resizeRowToContents)
self.horizontalHeader().sectionResized.connect(self.resizeRowsToContents)
def resizeRowToContents(self, row):
print(f'row {row}')
super().resizeRowToContents(row)
self.verticalHeader().resizeSection(row, self.sizeHintForRow(row))
def resizeRowsToContents(self):
header = self.verticalHeader()
for row in range(self.model().rowCount()):
hint = self.sizeHintForRow(row)
header.resizeSection(row, hint)
def sizeHintForRow(self, row):
super_result = super().sizeHintForRow(row)
print(f'row {row} super_result {super_result}')
return super_result
class SegmentsTableViewDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, *args):
super().__init__(*args)
self.editor = None
def createEditor(self, parent, option, index):
class Editor(QtWidgets.QTextEdit):
def resizeEvent(self, event):
print(f'event {event}')
super().resizeEvent(event)
self.editor = Editor(parent)
# does not seem to solve things:
self.editor.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
class Document(QtGui.QTextDocument):
def __init__(self, *args):
super().__init__(*args)
self.contentsChange.connect(self.contents_change)
def drawContents(self, p, rect):
print(f'p {p} rect {rect}')
super().drawContents(p, rect)
def contents_change(self, position, chars_removed, chars_added):
# strangely, after a line break, this shows a higher rect NOT when the first character
# causes a line break... but after that!
print(f'contents change, size {self.size()}')
# parent.parent() is the table view
parent.parent().resizeRowToContents(index.row())
self.editor.setDocument(Document())
return self.editor
def paint(self, painter, option, index):
doc = QtGui.QTextDocument()
doc.setDocumentMargin(0)
doc.setDefaultFont(option.font)
self.initStyleOption(option, index)
painter.save()
doc.setTextWidth(option.rect.width())
doc.setHtml(option.text)
option.text = ""
option.widget.style().drawControl(QtWidgets.QStyle.CE_ItemViewItem, option, painter)
painter.translate(option.rect.left(), option.rect.top())
clip = QtCore.QRectF(0, 0, option.rect.width(), option.rect.height())
painter.setClipRect(clip)
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
ctx.clip = clip
doc.documentLayout().draw(painter, ctx)
painter.restore()
def sizeHint(self, option, index):
self.initStyleOption(option, index)
doc = QtGui.QTextDocument()
if self.editor != None and index.row() == 0:
print(f'self.editor.size() {self.editor.size()}')
print(f'self.editor.document().size() {self.editor.document().size()}')
doc.setTextWidth(option.rect.width())
doc.setDefaultFont(option.font)
doc.setDocumentMargin(0)
doc.setHtml(option.text)
doc_height_int = int(doc.size().height())
if self.editor != None and index.row() == 0:
print(f'...row 0 doc_height_int {doc_height_int}')
return QtCore.QSize(int(doc.idealWidth()), doc_height_int)
app = QtWidgets.QApplication([])
app.setFont(default_font)
main_window = MainWindow()
main_window.show()
exec_return = app.exec()
sys.exit(exec_return)
如果我开始编辑第 0 行右侧的单元格(F2 或双击),然后开始在现有文本的末尾慢慢键入“四五六七”,我发现当我键入 7 的“n”时出现换行符(自动换行)。但是此时打印f'contents change, size {self.size()}'
的行显示文件高度仍然只有32.0。只有当我输入另一个字符时,它才会增加到 56.0。
我希望随着编辑器(或其文档?)的高度增加,行的高度也会增加。
还有一些其他的困惑:当我在这个编辑器中输入时,字符目前有点上下跳动。其次,当我键入每个字符时,self.editor.document().size()
行(在 sizeHint
中)打印了 4 次。对我来说,这两种现象都表明我可能以某种方式使信号短路,或者以某种方式以错误的方式做事。
如前所述,我无法找到任何方法来测量 QTextDocument
(或其 QTextEdit
编辑器)在换行后的真实高度,或者类似发生换行时发出的信号(在这方面我还查看了 QTextCursor
,例如)。
编辑
我现在稍微更改了主 window 构造函数,这样 QLE 可以以延迟方式显示 QTextDocument
的尺寸(注意不能使用按钮,因为点击会失去焦点并摧毁编辑器)。所以如果有兴趣,请尝试上面的新版本。
这显示的内容相当具有启发性:您会看到在自动换行发生后的下一个 1 秒“滴答”,文档 的正确高度在中给出QLE。这向我表明,这里正在进行某种延迟触发。但是因为我还没有找到合适的方法或信号在 QTextDocument
改变大小时激活,所以我不确定如何对此做出响应。
PS 它也以另一种方式工作:如果在引起自动换行后慢慢删除字符,直到文本再次变成一行,QLE 显示正确的高度 32.0 而 contents_change
继续显示不正确的高度 56.0。
经过反复试验,我发现关键是 是 一个信号,当 QTextDocument
改变高度 cursorPositionChanged
时触发。所以你让后者触发parent.parent().resizeRowToContents(index.row())
。此时可能有检查范围,以查看文档是否实际更改了高度(当然,并非所有光标更改都暗示了这一点)。
然后你可以在sizeHint
中这样做:
if self.editor == None:
doc_height_int = int(doc.size().height())
else:
doc_height_int = int(self.editor.document().size().height())
然后您需要在 Document
构造函数中 self.setDocumentMargin(0)
并将此方法添加到委托中(如果编辑器被销毁但没有被销毁 None
你会得到那些可怕的“包装的 C++ 对象已销毁“运行时错误”:
def destroyEditor(self, editor, index):
super().destroyEditor(editor, index)
self.editor = None
在单行文本中书写时仍然有点晃动...这可能是由于某处的竞争冲动说出某物的大小提示是什么。
编辑
根据 Musicamante 的评论,这是我新的完整版 MRE。您必须 pip 安装模块 dominate(创建 HTML 个实体)。
注意我还没有解决结束编辑会话的方法:可以(至少在 W10 OS 上)通过按下 Ctrl-Down 来结束编辑。
import sys, html.parser, dominate
from PyQt5 import QtWidgets, QtCore, QtGui
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle('Implement HTML editor for QTableView')
self.setGeometry(QtCore.QRect(100, 100, 1000, 800))
self.table_view = SegmentsTableView(self)
self.setCentralWidget(self.table_view)
rows = [
['one potatoe two potatoe', 'one potatoe two potatoe'],
['Sed ut perspiciatis, unde omnis iste natus error sit voluptatem accusantium doloremque',
'Sed ut <b>perspiciatis, unde omnis <i>iste natus</b> error sit voluptatem</i> accusantium doloremque'],
['Nemo enim ipsam voluptatem, quia voluptas sit, aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui do lorem ipsum, quia dolor sit amet consectetur adipiscing velit, sed quia non numquam do eius modi tempora incididunt, ut labore et dolore magnam aliquam quaerat voluptatem.',
'Nemo enim ipsam <i>voluptatem, quia voluptas sit, <b>aspernatur aut odit aut fugit, <u>sed quia</i> consequuntur</u> magni dolores eos</b>, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui do lorem ipsum, quia dolor sit amet consectetur adipiscing velit, sed quia non numquam do eius modi tempora incididunt, ut labore et dolore magnam aliquam quaerat voluptatem.'
],
['Ut enim ad minima veniam',
'Ut enim ad minima veniam'],
['Quis autem vel eum iure reprehenderit',
'Quis autem vel eum iure reprehenderit'],
['At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga.',
'At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga.'
]]
for n_row, row in enumerate(rows):
self.table_view.model().insertRow(n_row)
self.table_view.model().setItem(n_row, 0, QtGui.QStandardItem(row[0]))
self.table_view.model().setItem(n_row, 1, QtGui.QStandardItem(row[1]))
self.table_view.setColumnWidth(0, 400)
self.table_view.setColumnWidth(1, 400)
class SegmentsTableView(QtWidgets.QTableView):
def __init__(self, parent):
super().__init__(parent)
self.setItemDelegate(SegmentsTableViewDelegate(self))
self.setModel(QtGui.QStandardItemModel())
v_header = self.verticalHeader()
#
v_header.setMinimumSectionSize(5)
v_header.sectionHandleDoubleClicked.disconnect()
v_header.sectionHandleDoubleClicked.connect(self.resizeRowToContents)
self.horizontalHeader().sectionResized.connect(self.resizeRowsToContents)
def resizeRowToContents(self, row):
super().resizeRowToContents(row)
self.verticalHeader().resizeSection(row, self.sizeHintForRow(row))
def resizeRowsToContents(self):
header = self.verticalHeader()
for row in range(self.model().rowCount()):
hint = self.sizeHintForRow(row)
header.resizeSection(row, hint)
class SegmentsTableViewDelegate(QtWidgets.QStyledItemDelegate):
class EditorDocument(QtGui.QTextDocument):
def __init__(self, parent):
super().__init__(parent)
self.setDocumentMargin(0)
self.contentsChange.connect(self.contents_change)
self.height = None
parent.setDocument(self)
def contents_change(self, position, chars_removed, chars_added):
def resize_func():
if self.size().height() != self.height:
doc_size = self.size()
self.parent().resize(int(doc_size.width()), int(doc_size.height()))
QtCore.QTimer.singleShot(0, resize_func)
class EditorHTMLSanitiserParser(html.parser.HTMLParser):
def feed(self, html_string):
self.reset()
self.started_constructing = False
self.sanitised_html_string = ''
super().feed(html_string)
def handle_starttag(self, tag, attrs):
# I believe you can't insert a P into your displayed HTML (Shift-Return inserts a BR)
if tag == 'p':
self.started_constructing = True
elif self.started_constructing:
if tag in dir(dominate.tags):
new_tag_entity = getattr(dominate.tags, tag)()
for attr_name, attr_val in attrs:
new_tag_entity[attr_name] = attr_val
# remove the closing tag characters from the end of the rendered HTML
self.sanitised_html_string += new_tag_entity.render()[:-(len(tag) + 3)]
else:
print(f'*** unrecognised HTML tag: |{tag}|')
def handle_endtag(self, tag):
if self.started_constructing:
if tag in dir(dominate.tags):
new_tag_entity = getattr(dominate.tags, tag)()
# append only the closing tag characters from the end of the rendered HTML
self.sanitised_html_string += new_tag_entity.render()[-(len(tag) + 3):]
else:
print(f'*** unrecognised HTML tag: |{tag}|')
def handle_data(self, data):
if self.started_constructing:
self.sanitised_html_string += html.escape(data)
def __init__(self, *args):
super().__init__(*args)
self.pm_index_to_editor_dict = {}
self.html_sanitiser = SegmentsTableViewDelegate.EditorHTMLSanitiserParser()
def createEditor(self, parent, option, index):
class Editor(QtWidgets.QTextEdit):
def resizeEvent(self, event):
super().resizeEvent(event)
parent.parent().resizeRowToContents(index.row())
# taken from Musicamante: apply bold/italic/underline
def keyPressEvent(self, event):
if event.modifiers() == QtCore.Qt.ControlModifier:
if event.key() in (QtCore.Qt.Key_Return, ):
self.commit.emit(self)
return
elif event.key() == QtCore.Qt.Key_B:
if self.fontWeight() == QtGui.QFont.Bold:
self.setFontWeight(QtGui.QFont.Normal)
else:
self.setFontWeight(QtGui.QFont.Bold)
elif event.key() == QtCore.Qt.Key_I:
self.setFontItalic(not self.fontItalic())
elif event.key() == QtCore.Qt.Key_U:
self.setFontUnderline(not self.fontUnderline())
super().keyPressEvent(event)
pm_index = QtCore.QPersistentModelIndex(index)
if pm_index in self.pm_index_to_editor_dict:
editor = self.pm_index_to_editor_dict[pm_index]
else:
editor = Editor(parent)
editor.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
editor.setFrameShape(0)
self.pm_index_to_editor_dict[pm_index] = editor
SegmentsTableViewDelegate.EditorDocument(editor)
return editor
def destroyEditor(self, editor, index):
super().destroyEditor(editor, index)
pm_index = QtCore.QPersistentModelIndex(index)
del self.pm_index_to_editor_dict[pm_index]
# from Musicamante: case of rejected edit
self.parent().resizeRowToContents(index.row())
def setModelData(self, editor, model, index):
self.html_sanitiser.feed(editor.toHtml())
model.setData(index, self.html_sanitiser.sanitised_html_string, QtCore.Qt.EditRole)
def paint(self, painter, option, index):
doc = QtGui.QTextDocument()
doc.setDocumentMargin(0)
doc.setDefaultFont(option.font)
self.initStyleOption(option, index)
painter.save()
doc.setTextWidth(option.rect.width())
doc.setHtml(option.text)
option.text = ""
option.widget.style().drawControl(QtWidgets.QStyle.CE_ItemViewItem, option, painter)
painter.translate(option.rect.left(), option.rect.top())
# from Musicamante
doc.drawContents(painter)
painter.restore()
def sizeHint(self, option, index):
self.initStyleOption(option, index)
pm_index = QtCore.QPersistentModelIndex(index)
if pm_index in self.pm_index_to_editor_dict:
doc = self.pm_index_to_editor_dict[pm_index].document()
else:
doc = QtGui.QTextDocument()
doc.setTextWidth(option.rect.width())
doc.setDefaultFont(option.font)
doc.setDocumentMargin(0)
doc.setHtml(option.text)
return QtCore.QSize(int(doc.idealWidth()), int(doc.size().height()))
app = QtWidgets.QApplication([])
app.setFont(default_font)
main_window = MainWindow()
main_window.show()
exec_return = app.exec()
sys.exit(exec_return)
根据内容调整行的大小可能会出现一些问题,并且可能会导致递归你不是很小心,或者至少不必要地调用很多函数,因为很多很多次。
Qt 委托的默认行为是根据内容调整字符串编辑器(QLineEdit 子class),使其最终大于(永远不会小于)其原始单元格,以便显示尽可能多的内容,但不要大于视图的右边距。
虽然这种行为工作正常,但为多行编辑器实现它变得更加复杂:可能应该显示一个垂直滚动条(但是由于基于可见性的文档大小的递归,这会产生一些问题),并且围绕编辑器应该可见,以便了解实际内容何时 ends(否则您可能会将 next 行的内容误认为是编辑器的内容);考虑到这一点,调整行的大小可能是有意义的,但是,如前所述,可能会采取谨慎的预防措施。 well 书面实现应该考虑到这一点,并可能通过适当调整编辑器的大小来继承相同的行为。
也就是说,这里有一些建议:
- 在函数内部创建 classes 通常没有真正的好处;如果你想将 class 设置为“私有”,只需在其名称中使用双下划线前缀即可;如果您这样做是为了访问局部变量,那么这可能意味着整个逻辑在概念上是错误的:class 应该(理论上)不关心创建它的局部范围,而只关心全局环境;
- QTextEdit文档内容的改变需要事件循环处理才能生效:滚动区域需要根据布局系统更新内容,然后有效调整文档布局大小;您必须使用 0 超时计时器才能获得文档的实际 new 大小;
- 虽然在您的情况下理论上在任何给定时间都只存在一个编辑器,但委托人不应为编辑器保留唯一的静态引用:您可能想在某个时候使用
openPersistentEditor()
,这将打破很多东西;不幸的是,Qt 没有为给定索引处的当前打开的编辑器提供 public API,但是您可以为此创建一个字典;要注意的是,您应该使用 QPersistentModelIndex 来确保绝对安全(如果模型支持 sorting/filtering,则 非常 很重要,否则它可以通过另一个函数从外部更新或线程);
toHtml()
函数自动设置 p, li { white-space: pre-wrap; }
样式表(它是硬编码的,因此不能被覆盖,在源代码中搜索 QTextHtmlExporter::toHtml
);由于第一段总是以 <p>
标记的新行开始,这意味着基于 HTML 生成的 QTextDocument 将有一个使用段落行间距的 pre-wrap
新行。由于item delegates使用编辑器的user
属性(即QTextEdit的html
属性)设置编辑器数据,然后提交给模型,解决方法是创建一个自定义 Qt 属性(user
标志设置为 True
,这将覆盖现有的)和 return toHtml()
的结果,没有第一行在 <body>
标签后中断;
- 在索引外部单击以提交数据不直观;您可以覆盖委托
eventFilter
函数以捕获键盘快捷键,例如 Ctrl+Return;
- 在这些情况下通常不需要使用带有绘图上下文的文档布局,您可以只翻译绘图器并使用
drawContents
;
考虑到以上情况,这里有一个可能的解决方案:
class DelegateRichTextEditor(QtWidgets.QTextEdit):
commit = QtCore.pyqtSignal(QtWidgets.QWidget)
sizeHintChanged = QtCore.pyqtSignal()
storedSize = None
def __init__(self, parent):
super().__init__(parent)
self.setFrameShape(0)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.contentTimer = QtCore.QTimer(self,
timeout=self.contentsChange, interval=0)
self.document().setDocumentMargin(0)
self.document().contentsChange.connect(self.contentTimer.start)
@QtCore.pyqtProperty(str, user=True)
def content(self):
text = self.toHtml()
# find the end of the <body> tag and remove the new line character
bodyTag = text.find('>', text.find('<body')) + 1
if text[bodyTag] == '\n':
text = text[:bodyTag] + text[bodyTag + 1:]
return text
@content.setter
def content(self, text):
self.setHtml(text)
def contentsChange(self):
newSize = self.document().size()
if self.storedSize != newSize:
self.storedSize = newSize
self.sizeHintChanged.emit()
def keyPressEvent(self, event):
if event.modifiers() == QtCore.Qt.ControlModifier:
if event.key() in (QtCore.Qt.Key_Return, ):
self.commit.emit(self)
return
elif event.key() == QtCore.Qt.Key_B:
if self.fontWeight() == QtGui.QFont.Bold:
self.setFontWeight(QtGui.QFont.Normal)
else:
self.setFontWeight(QtGui.QFont.Bold)
elif event.key() == QtCore.Qt.Key_I:
self.setFontItalic(not self.fontItalic())
elif event.key() == QtCore.Qt.Key_U:
self.setFontUnderline(not self.fontUnderline())
super().keyPressEvent(event)
def showEvent(self, event):
super().showEvent(event)
cursor = self.textCursor()
cursor.movePosition(cursor.End)
self.setTextCursor(cursor)
class SegmentsTableViewDelegate(QtWidgets.QStyledItemDelegate):
rowSizeHintChanged = QtCore.pyqtSignal(int)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.editors = {}
def createEditor(self, parent, option, index):
pIndex = QtCore.QPersistentModelIndex(index)
editor = self.editors.get(pIndex)
if not editor:
editor = DelegateRichTextEditor(parent)
editor.sizeHintChanged.connect(
lambda: self.rowSizeHintChanged.emit(pIndex.row()))
self.editors[pIndex] = editor
return editor
def eventFilter(self, editor, event):
if (event.type() == event.KeyPress and
event.modifiers() == QtCore.Qt.ControlModifier and
event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return)):
self.commitData.emit(editor)
self.closeEditor.emit(editor)
return True
return super().eventFilter(editor, event)
def destroyEditor(self, editor, index):
# remove the editor from the dict so that it gets properly destroyed;
# this avoids any "wrapped C++ object destroyed" exception
self.editors.pop(QtCore.QPersistentModelIndex(index))
super().destroyEditor(editor, index)
# emit the signal again: if the data has been rejected, we need to
# restore the correct hint
self.rowSizeHintChanged.emit(index.row())
def paint(self, painter, option, index):
self.initStyleOption(option, index)
painter.save()
doc = QtGui.QTextDocument()
doc.setDocumentMargin(0)
doc.setTextWidth(option.rect.width())
doc.setHtml(option.text)
option.text = ""
option.widget.style().drawControl(
QtWidgets.QStyle.CE_ItemViewItem, option, painter)
painter.translate(option.rect.left(), option.rect.top())
doc.drawContents(painter)
painter.restore()
def sizeHint(self, option, index):
self.initStyleOption(option, index)
editor = self.editors.get(QtCore.QPersistentModelIndex(index))
if editor:
doc = QtGui.QTextDocument.clone(editor.document())
else:
doc = QtGui.QTextDocument()
doc.setDocumentMargin(0)
doc.setHtml(option.text)
doc.setTextWidth(option.rect.width())
doc_height_int = int(doc.size().height())
return QtCore.QSize(int(doc.idealWidth()), doc_height_int)
class SegmentsTableView(QtWidgets.QTableView):
def __init__(self, parent):
super().__init__(parent)
delegate = SegmentsTableViewDelegate(self)
self.setItemDelegate(delegate)
delegate.rowSizeHintChanged.connect(self.resizeRowToContents)
# ...
补充说明:
- 虽然
paint
函数通常在事件循环中调用 last,但在覆盖 option
值时应注意一些事项;当“modified”选项只是临时使用时(例如,使用更改的值查询当前样式),根据给定的选项创建一个 new 选项是个好习惯;您可以通过执行以下操作来使用 new 选项:
newOption = option.__class__(option)
- “其次,当我输入每个字符时,行
self.editor.document().size()
(在 sizeHint
中)打印 4 次”:这是因为 resizeRowToContents
被 [=31= 触发]; resize 函数自动为给定行中的所有索引调用委托的 sizeHint()
以获取所有可用提示,然后它根据宽度计算调整所有部分的大小,但由于 resizeRowsToContents
连接到 resizeRowsToContents
如果行大小不匹配,它将被再次调用;这是一个典型的例子,在某些“调整大小”事件之后,必须非常小心地更改几何图形,因为它们可能会导致某些(可能是无限的)递归级别;
- 唯一的缺点是没有考虑键盘重复,所以编辑器(和视图)不会更新,直到 repeated 键被实际释放;这可以通过使用备用计时器来解决,该计时器在捕获
isAutoRepeat()
键事件时触发编辑器的 contentsChange
;
这直接来自
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle('Get a grip of table view row height MRE')
self.setGeometry(QtCore.QRect(100, 100, 1000, 800))
layout = QtWidgets.QVBoxLayout()
central_widget = QtWidgets.QWidget( self )
central_widget.setLayout(layout)
self.table_view = SegmentsTableView(self)
self.setCentralWidget(central_widget)
layout.addWidget(self.table_view)
rows = [
['one potatoe two potatoe', 'one potatoe two potatoe'],
['Sed ut perspiciatis, unde omnis iste natus error sit voluptatem accusantium doloremque',
'Sed ut <b>perspiciatis, unde omnis <i>iste natus</b> error sit voluptatem</i> accusantium doloremque'],
['Nemo enim ipsam voluptatem, quia voluptas sit, aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui do lorem ipsum, quia dolor sit amet consectetur adipiscing velit, sed quia non numquam do eius modi tempora incididunt, ut labore et dolore magnam aliquam quaerat voluptatem.',
'Nemo enim ipsam <i>voluptatem, quia voluptas sit, <b>aspernatur aut odit aut fugit, <u>sed quia</i> consequuntur</u> magni dolores eos</b>, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui do lorem ipsum, quia dolor sit amet consectetur adipiscing velit, sed quia non numquam do eius modi tempora incididunt, ut labore et dolore magnam aliquam quaerat voluptatem.'
],
['Ut enim ad minima veniam',
'Ut enim ad minima veniam'],
['Quis autem vel eum iure reprehenderit',
'Quis autem vel eum iure reprehenderit'],
['At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga.',
'At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga.'
]]
for n_row, row in enumerate(rows):
self.table_view.model().insertRow(n_row)
self.table_view.model().setItem(n_row, 0, QtGui.QStandardItem(row[0]))
self.table_view.model().setItem(n_row, 1, QtGui.QStandardItem(row[1]))
self.table_view.setColumnWidth(0, 400)
self.table_view.setColumnWidth(1, 400)
self.qle = QtWidgets.QLineEdit()
layout.addWidget(self.qle)
self._second_timer = QtCore.QTimer(self)
self._second_timer.timeout.connect(self.show_doc_size)
# every 1s
self._second_timer.start(1000)
def show_doc_size(self, *args):
if self.table_view.itemDelegate().editor == None:
self.qle.setText('no editor yet')
else:
self.qle.setText(f'self.table_view.itemDelegate().editor.document().size() {self.table_view.itemDelegate().editor.document().size()}')
class SegmentsTableView(QtWidgets.QTableView):
def __init__(self, parent):
super().__init__(parent)
self.setItemDelegate(SegmentsTableViewDelegate(self))
self.setModel(QtGui.QStandardItemModel())
v_header = self.verticalHeader()
#
v_header.setMinimumSectionSize(5)
v_header.sectionHandleDoubleClicked.disconnect()
v_header.sectionHandleDoubleClicked.connect(self.resizeRowToContents)
self.horizontalHeader().sectionResized.connect(self.resizeRowsToContents)
def resizeRowToContents(self, row):
print(f'row {row}')
super().resizeRowToContents(row)
self.verticalHeader().resizeSection(row, self.sizeHintForRow(row))
def resizeRowsToContents(self):
header = self.verticalHeader()
for row in range(self.model().rowCount()):
hint = self.sizeHintForRow(row)
header.resizeSection(row, hint)
def sizeHintForRow(self, row):
super_result = super().sizeHintForRow(row)
print(f'row {row} super_result {super_result}')
return super_result
class SegmentsTableViewDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, *args):
super().__init__(*args)
self.editor = None
def createEditor(self, parent, option, index):
class Editor(QtWidgets.QTextEdit):
def resizeEvent(self, event):
print(f'event {event}')
super().resizeEvent(event)
self.editor = Editor(parent)
# does not seem to solve things:
self.editor.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
class Document(QtGui.QTextDocument):
def __init__(self, *args):
super().__init__(*args)
self.contentsChange.connect(self.contents_change)
def drawContents(self, p, rect):
print(f'p {p} rect {rect}')
super().drawContents(p, rect)
def contents_change(self, position, chars_removed, chars_added):
# strangely, after a line break, this shows a higher rect NOT when the first character
# causes a line break... but after that!
print(f'contents change, size {self.size()}')
# parent.parent() is the table view
parent.parent().resizeRowToContents(index.row())
self.editor.setDocument(Document())
return self.editor
def paint(self, painter, option, index):
doc = QtGui.QTextDocument()
doc.setDocumentMargin(0)
doc.setDefaultFont(option.font)
self.initStyleOption(option, index)
painter.save()
doc.setTextWidth(option.rect.width())
doc.setHtml(option.text)
option.text = ""
option.widget.style().drawControl(QtWidgets.QStyle.CE_ItemViewItem, option, painter)
painter.translate(option.rect.left(), option.rect.top())
clip = QtCore.QRectF(0, 0, option.rect.width(), option.rect.height())
painter.setClipRect(clip)
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
ctx.clip = clip
doc.documentLayout().draw(painter, ctx)
painter.restore()
def sizeHint(self, option, index):
self.initStyleOption(option, index)
doc = QtGui.QTextDocument()
if self.editor != None and index.row() == 0:
print(f'self.editor.size() {self.editor.size()}')
print(f'self.editor.document().size() {self.editor.document().size()}')
doc.setTextWidth(option.rect.width())
doc.setDefaultFont(option.font)
doc.setDocumentMargin(0)
doc.setHtml(option.text)
doc_height_int = int(doc.size().height())
if self.editor != None and index.row() == 0:
print(f'...row 0 doc_height_int {doc_height_int}')
return QtCore.QSize(int(doc.idealWidth()), doc_height_int)
app = QtWidgets.QApplication([])
app.setFont(default_font)
main_window = MainWindow()
main_window.show()
exec_return = app.exec()
sys.exit(exec_return)
如果我开始编辑第 0 行右侧的单元格(F2 或双击),然后开始在现有文本的末尾慢慢键入“四五六七”,我发现当我键入 7 的“n”时出现换行符(自动换行)。但是此时打印f'contents change, size {self.size()}'
的行显示文件高度仍然只有32.0。只有当我输入另一个字符时,它才会增加到 56.0。
我希望随着编辑器(或其文档?)的高度增加,行的高度也会增加。
还有一些其他的困惑:当我在这个编辑器中输入时,字符目前有点上下跳动。其次,当我键入每个字符时,self.editor.document().size()
行(在 sizeHint
中)打印了 4 次。对我来说,这两种现象都表明我可能以某种方式使信号短路,或者以某种方式以错误的方式做事。
如前所述,我无法找到任何方法来测量 QTextDocument
(或其 QTextEdit
编辑器)在换行后的真实高度,或者类似发生换行时发出的信号(在这方面我还查看了 QTextCursor
,例如)。
编辑
我现在稍微更改了主 window 构造函数,这样 QLE 可以以延迟方式显示 QTextDocument
的尺寸(注意不能使用按钮,因为点击会失去焦点并摧毁编辑器)。所以如果有兴趣,请尝试上面的新版本。
这显示的内容相当具有启发性:您会看到在自动换行发生后的下一个 1 秒“滴答”,文档 的正确高度在中给出QLE。这向我表明,这里正在进行某种延迟触发。但是因为我还没有找到合适的方法或信号在 QTextDocument
改变大小时激活,所以我不确定如何对此做出响应。
PS 它也以另一种方式工作:如果在引起自动换行后慢慢删除字符,直到文本再次变成一行,QLE 显示正确的高度 32.0 而 contents_change
继续显示不正确的高度 56.0。
经过反复试验,我发现关键是 是 一个信号,当 QTextDocument
改变高度 cursorPositionChanged
时触发。所以你让后者触发parent.parent().resizeRowToContents(index.row())
。此时可能有检查范围,以查看文档是否实际更改了高度(当然,并非所有光标更改都暗示了这一点)。
然后你可以在sizeHint
中这样做:
if self.editor == None:
doc_height_int = int(doc.size().height())
else:
doc_height_int = int(self.editor.document().size().height())
然后您需要在 Document
构造函数中 self.setDocumentMargin(0)
并将此方法添加到委托中(如果编辑器被销毁但没有被销毁 None
你会得到那些可怕的“包装的 C++ 对象已销毁“运行时错误”:
def destroyEditor(self, editor, index):
super().destroyEditor(editor, index)
self.editor = None
在单行文本中书写时仍然有点晃动...这可能是由于某处的竞争冲动说出某物的大小提示是什么。
编辑
根据 Musicamante 的评论,这是我新的完整版 MRE。您必须 pip 安装模块 dominate(创建 HTML 个实体)。
注意我还没有解决结束编辑会话的方法:可以(至少在 W10 OS 上)通过按下 Ctrl-Down 来结束编辑。
import sys, html.parser, dominate
from PyQt5 import QtWidgets, QtCore, QtGui
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle('Implement HTML editor for QTableView')
self.setGeometry(QtCore.QRect(100, 100, 1000, 800))
self.table_view = SegmentsTableView(self)
self.setCentralWidget(self.table_view)
rows = [
['one potatoe two potatoe', 'one potatoe two potatoe'],
['Sed ut perspiciatis, unde omnis iste natus error sit voluptatem accusantium doloremque',
'Sed ut <b>perspiciatis, unde omnis <i>iste natus</b> error sit voluptatem</i> accusantium doloremque'],
['Nemo enim ipsam voluptatem, quia voluptas sit, aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui do lorem ipsum, quia dolor sit amet consectetur adipiscing velit, sed quia non numquam do eius modi tempora incididunt, ut labore et dolore magnam aliquam quaerat voluptatem.',
'Nemo enim ipsam <i>voluptatem, quia voluptas sit, <b>aspernatur aut odit aut fugit, <u>sed quia</i> consequuntur</u> magni dolores eos</b>, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui do lorem ipsum, quia dolor sit amet consectetur adipiscing velit, sed quia non numquam do eius modi tempora incididunt, ut labore et dolore magnam aliquam quaerat voluptatem.'
],
['Ut enim ad minima veniam',
'Ut enim ad minima veniam'],
['Quis autem vel eum iure reprehenderit',
'Quis autem vel eum iure reprehenderit'],
['At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga.',
'At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga.'
]]
for n_row, row in enumerate(rows):
self.table_view.model().insertRow(n_row)
self.table_view.model().setItem(n_row, 0, QtGui.QStandardItem(row[0]))
self.table_view.model().setItem(n_row, 1, QtGui.QStandardItem(row[1]))
self.table_view.setColumnWidth(0, 400)
self.table_view.setColumnWidth(1, 400)
class SegmentsTableView(QtWidgets.QTableView):
def __init__(self, parent):
super().__init__(parent)
self.setItemDelegate(SegmentsTableViewDelegate(self))
self.setModel(QtGui.QStandardItemModel())
v_header = self.verticalHeader()
#
v_header.setMinimumSectionSize(5)
v_header.sectionHandleDoubleClicked.disconnect()
v_header.sectionHandleDoubleClicked.connect(self.resizeRowToContents)
self.horizontalHeader().sectionResized.connect(self.resizeRowsToContents)
def resizeRowToContents(self, row):
super().resizeRowToContents(row)
self.verticalHeader().resizeSection(row, self.sizeHintForRow(row))
def resizeRowsToContents(self):
header = self.verticalHeader()
for row in range(self.model().rowCount()):
hint = self.sizeHintForRow(row)
header.resizeSection(row, hint)
class SegmentsTableViewDelegate(QtWidgets.QStyledItemDelegate):
class EditorDocument(QtGui.QTextDocument):
def __init__(self, parent):
super().__init__(parent)
self.setDocumentMargin(0)
self.contentsChange.connect(self.contents_change)
self.height = None
parent.setDocument(self)
def contents_change(self, position, chars_removed, chars_added):
def resize_func():
if self.size().height() != self.height:
doc_size = self.size()
self.parent().resize(int(doc_size.width()), int(doc_size.height()))
QtCore.QTimer.singleShot(0, resize_func)
class EditorHTMLSanitiserParser(html.parser.HTMLParser):
def feed(self, html_string):
self.reset()
self.started_constructing = False
self.sanitised_html_string = ''
super().feed(html_string)
def handle_starttag(self, tag, attrs):
# I believe you can't insert a P into your displayed HTML (Shift-Return inserts a BR)
if tag == 'p':
self.started_constructing = True
elif self.started_constructing:
if tag in dir(dominate.tags):
new_tag_entity = getattr(dominate.tags, tag)()
for attr_name, attr_val in attrs:
new_tag_entity[attr_name] = attr_val
# remove the closing tag characters from the end of the rendered HTML
self.sanitised_html_string += new_tag_entity.render()[:-(len(tag) + 3)]
else:
print(f'*** unrecognised HTML tag: |{tag}|')
def handle_endtag(self, tag):
if self.started_constructing:
if tag in dir(dominate.tags):
new_tag_entity = getattr(dominate.tags, tag)()
# append only the closing tag characters from the end of the rendered HTML
self.sanitised_html_string += new_tag_entity.render()[-(len(tag) + 3):]
else:
print(f'*** unrecognised HTML tag: |{tag}|')
def handle_data(self, data):
if self.started_constructing:
self.sanitised_html_string += html.escape(data)
def __init__(self, *args):
super().__init__(*args)
self.pm_index_to_editor_dict = {}
self.html_sanitiser = SegmentsTableViewDelegate.EditorHTMLSanitiserParser()
def createEditor(self, parent, option, index):
class Editor(QtWidgets.QTextEdit):
def resizeEvent(self, event):
super().resizeEvent(event)
parent.parent().resizeRowToContents(index.row())
# taken from Musicamante: apply bold/italic/underline
def keyPressEvent(self, event):
if event.modifiers() == QtCore.Qt.ControlModifier:
if event.key() in (QtCore.Qt.Key_Return, ):
self.commit.emit(self)
return
elif event.key() == QtCore.Qt.Key_B:
if self.fontWeight() == QtGui.QFont.Bold:
self.setFontWeight(QtGui.QFont.Normal)
else:
self.setFontWeight(QtGui.QFont.Bold)
elif event.key() == QtCore.Qt.Key_I:
self.setFontItalic(not self.fontItalic())
elif event.key() == QtCore.Qt.Key_U:
self.setFontUnderline(not self.fontUnderline())
super().keyPressEvent(event)
pm_index = QtCore.QPersistentModelIndex(index)
if pm_index in self.pm_index_to_editor_dict:
editor = self.pm_index_to_editor_dict[pm_index]
else:
editor = Editor(parent)
editor.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
editor.setFrameShape(0)
self.pm_index_to_editor_dict[pm_index] = editor
SegmentsTableViewDelegate.EditorDocument(editor)
return editor
def destroyEditor(self, editor, index):
super().destroyEditor(editor, index)
pm_index = QtCore.QPersistentModelIndex(index)
del self.pm_index_to_editor_dict[pm_index]
# from Musicamante: case of rejected edit
self.parent().resizeRowToContents(index.row())
def setModelData(self, editor, model, index):
self.html_sanitiser.feed(editor.toHtml())
model.setData(index, self.html_sanitiser.sanitised_html_string, QtCore.Qt.EditRole)
def paint(self, painter, option, index):
doc = QtGui.QTextDocument()
doc.setDocumentMargin(0)
doc.setDefaultFont(option.font)
self.initStyleOption(option, index)
painter.save()
doc.setTextWidth(option.rect.width())
doc.setHtml(option.text)
option.text = ""
option.widget.style().drawControl(QtWidgets.QStyle.CE_ItemViewItem, option, painter)
painter.translate(option.rect.left(), option.rect.top())
# from Musicamante
doc.drawContents(painter)
painter.restore()
def sizeHint(self, option, index):
self.initStyleOption(option, index)
pm_index = QtCore.QPersistentModelIndex(index)
if pm_index in self.pm_index_to_editor_dict:
doc = self.pm_index_to_editor_dict[pm_index].document()
else:
doc = QtGui.QTextDocument()
doc.setTextWidth(option.rect.width())
doc.setDefaultFont(option.font)
doc.setDocumentMargin(0)
doc.setHtml(option.text)
return QtCore.QSize(int(doc.idealWidth()), int(doc.size().height()))
app = QtWidgets.QApplication([])
app.setFont(default_font)
main_window = MainWindow()
main_window.show()
exec_return = app.exec()
sys.exit(exec_return)
根据内容调整行的大小可能会出现一些问题,并且可能会导致递归你不是很小心,或者至少不必要地调用很多函数,因为很多很多次。
Qt 委托的默认行为是根据内容调整字符串编辑器(QLineEdit 子class),使其最终大于(永远不会小于)其原始单元格,以便显示尽可能多的内容,但不要大于视图的右边距。
虽然这种行为工作正常,但为多行编辑器实现它变得更加复杂:可能应该显示一个垂直滚动条(但是由于基于可见性的文档大小的递归,这会产生一些问题),并且围绕编辑器应该可见,以便了解实际内容何时 ends(否则您可能会将 next 行的内容误认为是编辑器的内容);考虑到这一点,调整行的大小可能是有意义的,但是,如前所述,可能会采取谨慎的预防措施。 well 书面实现应该考虑到这一点,并可能通过适当调整编辑器的大小来继承相同的行为。
也就是说,这里有一些建议:
- 在函数内部创建 classes 通常没有真正的好处;如果你想将 class 设置为“私有”,只需在其名称中使用双下划线前缀即可;如果您这样做是为了访问局部变量,那么这可能意味着整个逻辑在概念上是错误的:class 应该(理论上)不关心创建它的局部范围,而只关心全局环境;
- QTextEdit文档内容的改变需要事件循环处理才能生效:滚动区域需要根据布局系统更新内容,然后有效调整文档布局大小;您必须使用 0 超时计时器才能获得文档的实际 new 大小;
- 虽然在您的情况下理论上在任何给定时间都只存在一个编辑器,但委托人不应为编辑器保留唯一的静态引用:您可能想在某个时候使用
openPersistentEditor()
,这将打破很多东西;不幸的是,Qt 没有为给定索引处的当前打开的编辑器提供 public API,但是您可以为此创建一个字典;要注意的是,您应该使用 QPersistentModelIndex 来确保绝对安全(如果模型支持 sorting/filtering,则 非常 很重要,否则它可以通过另一个函数从外部更新或线程); toHtml()
函数自动设置p, li { white-space: pre-wrap; }
样式表(它是硬编码的,因此不能被覆盖,在源代码中搜索QTextHtmlExporter::toHtml
);由于第一段总是以<p>
标记的新行开始,这意味着基于 HTML 生成的 QTextDocument 将有一个使用段落行间距的pre-wrap
新行。由于item delegates使用编辑器的user
属性(即QTextEdit的html
属性)设置编辑器数据,然后提交给模型,解决方法是创建一个自定义 Qt 属性(user
标志设置为True
,这将覆盖现有的)和 returntoHtml()
的结果,没有第一行在<body>
标签后中断;- 在索引外部单击以提交数据不直观;您可以覆盖委托
eventFilter
函数以捕获键盘快捷键,例如 Ctrl+Return; - 在这些情况下通常不需要使用带有绘图上下文的文档布局,您可以只翻译绘图器并使用
drawContents
;
考虑到以上情况,这里有一个可能的解决方案:
class DelegateRichTextEditor(QtWidgets.QTextEdit):
commit = QtCore.pyqtSignal(QtWidgets.QWidget)
sizeHintChanged = QtCore.pyqtSignal()
storedSize = None
def __init__(self, parent):
super().__init__(parent)
self.setFrameShape(0)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.contentTimer = QtCore.QTimer(self,
timeout=self.contentsChange, interval=0)
self.document().setDocumentMargin(0)
self.document().contentsChange.connect(self.contentTimer.start)
@QtCore.pyqtProperty(str, user=True)
def content(self):
text = self.toHtml()
# find the end of the <body> tag and remove the new line character
bodyTag = text.find('>', text.find('<body')) + 1
if text[bodyTag] == '\n':
text = text[:bodyTag] + text[bodyTag + 1:]
return text
@content.setter
def content(self, text):
self.setHtml(text)
def contentsChange(self):
newSize = self.document().size()
if self.storedSize != newSize:
self.storedSize = newSize
self.sizeHintChanged.emit()
def keyPressEvent(self, event):
if event.modifiers() == QtCore.Qt.ControlModifier:
if event.key() in (QtCore.Qt.Key_Return, ):
self.commit.emit(self)
return
elif event.key() == QtCore.Qt.Key_B:
if self.fontWeight() == QtGui.QFont.Bold:
self.setFontWeight(QtGui.QFont.Normal)
else:
self.setFontWeight(QtGui.QFont.Bold)
elif event.key() == QtCore.Qt.Key_I:
self.setFontItalic(not self.fontItalic())
elif event.key() == QtCore.Qt.Key_U:
self.setFontUnderline(not self.fontUnderline())
super().keyPressEvent(event)
def showEvent(self, event):
super().showEvent(event)
cursor = self.textCursor()
cursor.movePosition(cursor.End)
self.setTextCursor(cursor)
class SegmentsTableViewDelegate(QtWidgets.QStyledItemDelegate):
rowSizeHintChanged = QtCore.pyqtSignal(int)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.editors = {}
def createEditor(self, parent, option, index):
pIndex = QtCore.QPersistentModelIndex(index)
editor = self.editors.get(pIndex)
if not editor:
editor = DelegateRichTextEditor(parent)
editor.sizeHintChanged.connect(
lambda: self.rowSizeHintChanged.emit(pIndex.row()))
self.editors[pIndex] = editor
return editor
def eventFilter(self, editor, event):
if (event.type() == event.KeyPress and
event.modifiers() == QtCore.Qt.ControlModifier and
event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return)):
self.commitData.emit(editor)
self.closeEditor.emit(editor)
return True
return super().eventFilter(editor, event)
def destroyEditor(self, editor, index):
# remove the editor from the dict so that it gets properly destroyed;
# this avoids any "wrapped C++ object destroyed" exception
self.editors.pop(QtCore.QPersistentModelIndex(index))
super().destroyEditor(editor, index)
# emit the signal again: if the data has been rejected, we need to
# restore the correct hint
self.rowSizeHintChanged.emit(index.row())
def paint(self, painter, option, index):
self.initStyleOption(option, index)
painter.save()
doc = QtGui.QTextDocument()
doc.setDocumentMargin(0)
doc.setTextWidth(option.rect.width())
doc.setHtml(option.text)
option.text = ""
option.widget.style().drawControl(
QtWidgets.QStyle.CE_ItemViewItem, option, painter)
painter.translate(option.rect.left(), option.rect.top())
doc.drawContents(painter)
painter.restore()
def sizeHint(self, option, index):
self.initStyleOption(option, index)
editor = self.editors.get(QtCore.QPersistentModelIndex(index))
if editor:
doc = QtGui.QTextDocument.clone(editor.document())
else:
doc = QtGui.QTextDocument()
doc.setDocumentMargin(0)
doc.setHtml(option.text)
doc.setTextWidth(option.rect.width())
doc_height_int = int(doc.size().height())
return QtCore.QSize(int(doc.idealWidth()), doc_height_int)
class SegmentsTableView(QtWidgets.QTableView):
def __init__(self, parent):
super().__init__(parent)
delegate = SegmentsTableViewDelegate(self)
self.setItemDelegate(delegate)
delegate.rowSizeHintChanged.connect(self.resizeRowToContents)
# ...
补充说明:
- 虽然
paint
函数通常在事件循环中调用 last,但在覆盖option
值时应注意一些事项;当“modified”选项只是临时使用时(例如,使用更改的值查询当前样式),根据给定的选项创建一个 new 选项是个好习惯;您可以通过执行以下操作来使用 new 选项:
newOption = option.__class__(option)
- “其次,当我输入每个字符时,行
self.editor.document().size()
(在sizeHint
中)打印 4 次”:这是因为resizeRowToContents
被 [=31= 触发]; resize 函数自动为给定行中的所有索引调用委托的sizeHint()
以获取所有可用提示,然后它根据宽度计算调整所有部分的大小,但由于resizeRowsToContents
连接到resizeRowsToContents
如果行大小不匹配,它将被再次调用;这是一个典型的例子,在某些“调整大小”事件之后,必须非常小心地更改几何图形,因为它们可能会导致某些(可能是无限的)递归级别; - 唯一的缺点是没有考虑键盘重复,所以编辑器(和视图)不会更新,直到 repeated 键被实际释放;这可以通过使用备用计时器来解决,该计时器在捕获
isAutoRepeat()
键事件时触发编辑器的contentsChange
;