I have a bunch of QTextEdits in a QVBoxLayout in a QScrollArea.
The texts can often get very long and the horizontal space is limited by design, and QTextEdit automatically wraps text in multiple lines which is good.
I want to automatically resize the QTextEdit to fit to the wrapped text, the text itself will always be in one line, and the wrapped text can have multiple lines, I want the QTextEdits to fit to the height of wrapped lines.
By hours upon hours of Google searching, I have found a solution, but it doesn't work as expected, there can sometimes be one extra line at the bottom, I will post example code below:
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
font = QFont('Noto Serif', 9)
class Editor(QTextEdit):
doubleClicked = pyqtSignal(QTextEdit)
def __init__(self):
super().__init__()
self.setReadOnly(True)
self.setFont(font)
self.textChanged.connect(self.autoResize)
self.margins = self.contentsMargins()
self.setAttribute(Qt.WidgetAttribute.WA_DontShowOnScreen)
@pyqtSlot(QMouseEvent)
def mouseDoubleClickEvent(self, e: QMouseEvent) -> None:
self.doubleClicked.emit(self)
def autoResize(self):
self.show()
height = int(self.document().size().height() + self.margins.top() + self.margins.bottom())
self.setFixedHeight(height)
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.resize(405, 720)
frame = self.frameGeometry()
center = self.screen().availableGeometry().center()
frame.moveCenter(center)
self.move(frame.topLeft())
self.centralwidget = QWidget(self)
self.vbox = QVBoxLayout(self.centralwidget)
self.scrollArea = QScrollArea(self.centralwidget)
self.scrollArea.setWidgetResizable(True)
self.scrollAreaWidgetContents = QWidget()
self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
self.verticalLayout = QVBoxLayout(self.scrollAreaWidgetContents)
self.verticalLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.scrollArea.setWidget(self.scrollAreaWidgetContents)
self.scrollArea.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
self.vbox.addWidget(self.scrollArea)
self.setCentralWidget(self.centralwidget)
def addItems():
items = [
"L'Estro Armonico No. 9 in D Major\u2014III. Allegro",
"L'Estro Armonico No. 6 in A Minor\u2014III. Presto",
"L'Estro Armonico No. 6 in A Minor\u2014I. Allegro",
"L'Estro Armonico No. 1 in D major\u2014I. Allegro",
"12 Concertos Op.3 \u2014 L'estro Armonico \u2014 Concerto No. 6 In A Minor For Solo Violin RV 356\u2014Presto",
"Ultimate Mozart\u2014 The Essential Masterpieces",
"Serenade in G K.525 Eine kleine Nachtmusik\u20141. Allegro",
"Vivaldi\u2014 L'estro Armonico",
"Academy of St. Martin in the Fields",
"Are You With Me \u2014 Reality"
]
for i in items:
textbox = Editor()
textbox.setText(i)
window.verticalLayout.addWidget(textbox)
app = QApplication([])
window = Window()
window.show()
addItems()
app.exec()
Note you will need Noto Serif for it to run correctly (and of course you can just replace it), and of course you need PyQt6.
In the example, the first seven textboxs all have one extra empty line at the bottom, and the last three don't have it.
What caused the extra line and how to remove it?
Update:
I have to set Qt.WidgetAttribute.WA_DontShowOnScreen because if I don't set it, calling .show() of QTextEdit will cause the QTextEdit show up in the middle of the screen and quickly disappear, and it's annoying.
I have to call .show() because calling document().size() without show(), the values will all be 0 and I don't know why it is like this.
The problem comes from the fact that you're trying to set the height too early. In fact, if you add a print(self.size())
just after show()
, you'll see that all will show a default size (probably, 256x192).
This depends on two aspects:
Then another problem arises: if the window is resized, the contents will not adapt until the text is changed.
In order to properly set a vertical height based on the contents, you should set the document's textWidth
, and also call autoResize
everytime the widget is resized.
class Editor(QTextEdit):
def __init__(self):
super().__init__()
self.setReadOnly(True)
self.setFont(font)
self.textChanged.connect(self.autoResize)
def autoResize(self):
self.document().setTextWidth(self.viewport().width())
margins = self.contentsMargins()
height = int(self.document().size().height() + margins.top() + margins.bottom())
self.setFixedHeight(height)
def resizeEvent(self, event):
self.autoResize()
Note that:
__init__
;mouseDoubleClickEvent
is a function that is called on a mouse event, it's not (nor it should) be a slot, so using the pyqtSlot
decorator is pointless;