I would like to have a QPlainTextEdit
widget that initially has height of one line (when it is empty). When user adds more text to the widget, it should automatically change its height to avoid scrolling.
How can I do that? I myself use PySide6
, but C++ code is fine, too.
Allow me to answer since you accept C++ code. Editing my initial answer to simplify the code (thanks @musicamante for the suggestion; it was even more simple than you commented) + address 2 flaws of my previous answer:
Step 1: Event filter
In Qt, getting the size widgets before they are shown on screen is hard; instead, we aim at getting the size when the plain text edit is shown for the first time.
To do that and to keep track of subsequent size changes, we first need an event filter class.
#include <QtCore/QObject.h>
#include <QtGui/QEvent.h>
class SizeEventFilter : public QObject {
Q_OBJECT
public:
SizeEventFilter(QObject* parent) : QObject(parent) {}
protected:
bool eventFilter(QObject* obj, QEvent* event)
{
if (event->type() == QEvent::Resize)
emit sizeChanged(obj);
return false;
}
signals:
void sizeChanged(QObject* object);
};
Step 2: Height calculation lambda
We use a lambda to recalculate the size of plainTextEdit
. The lambda uses 2 additional variables in its capture list, the definition of which I included below.
auto margins = plainTextEdit->contentsMargins();
auto margin = plainTextEdit->document()->rootFrame()->frameFormat().margin();
qreal verticalmarginSize = 2 * margin + margins.top() + margins.bottom();
QFont font = plainTextEdit->font();
QFontMetrics fontMetrics(font);
int lineHeight = fontMetrics.lineSpacing();
auto sizeCalculation = [plainTextEdit, lineHeight, verticalmarginSize]() {
auto document = plainTextEdit->document();
auto size = document->size();
int height = lineHeight * size.height() + verticalmarginSize + 1;
if (height != plainTextEdit->height())
plainTextEdit->setFixedHeight(height);
};
Step 3: Making everything work together
Right after the above code (step 2), insert the event filter, the connections and some tweaking of plainTextEdit
as shown below.
If you want to change the font of plainTextEdit
, as it is used in the capture list of the lambda, you must add the line before calling QFont font = plainTextEdit->font();
.
QObject::connect(plainTextEdit, &QPlainTextEdit::textChanged, sizeCalculation);
SizeEventFilter* sizeEventFilter = new SizeEventFilter(plainTextEdit);
plainTextEdit->installEventFilter(sizeEventFilter);
QObject::connect(sizeEventFilter, &SizeEventFilter::sizeChanged, plainTextEdit, sizeCalculation, Qt::QueuedConnection);
plainTextEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
plainTextEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
A problem remains...
In a more complete example, there are situations where several QPlainTextEdit
in a layout interfere with one another.
The scenario is as follow:
QPlainTextEdit
are added to a QScrollArea
. The verticalBarPolicy
property is set to Qt::ScrollBarAsNeeded
.QPlainTextEdit
. The scrollbar appears, adding an additional line break elsewhere.I have put up the below full example to demonstrate this effect.
The code uses what I presented above and behaves like the way shown in the screenshots.
If you remove the portion I marked with comments and uncomment the lines under that portion, you will get a more correct behavior, where the layout is recalculated without the vertical scrollbar just to see if it fits.
#include <QtGui/QFontMetrics>
#include <QtGui/QTextDocument>
#include <QtGui/QTextFrame>
#include <QtWidgets/QApplication>
#include <QtWidgets/QBoxLayout>
#include <QtWidgets/QScrollArea>
#include <QtWidgets/QPlainTextEdit>
#include "SizeEventFilter.h"
int main(int argc, char** argv) {
QApplication app(argc, argv);
app.setQuitOnLastWindowClosed(true);
auto main_widget = new QWidget();
main_widget->setMinimumSize(800, 410);
QScrollArea* scroll_area = new QScrollArea();
QWidget* scroll_widget = new QWidget();
QVBoxLayout* scroll_vbox = new QVBoxLayout(scroll_widget);
scroll_vbox->setContentsMargins(0, 0, 0, 0);
scroll_vbox->setSpacing(0);
scroll_widget->setLayout(scroll_vbox);
scroll_area->setWidget(scroll_widget);
scroll_area->setWidgetResizable(true);
auto vbox0 = new QVBoxLayout(main_widget);
main_widget->setLayout(vbox0);
vbox0->addWidget(scroll_area);
QPlainTextEdit
* plainTextEdit0 = new QPlainTextEdit(),
* plainTextEdit1 = new QPlainTextEdit(),
* plainTextEdit2 = new QPlainTextEdit();
plainTextEdit1->setPlainText("A looooooooooooooooong text carefully tailored to fit exactly in 1 line of text, and without leaving enough space for the scrollbar exactly here.");
plainTextEdit2->setPlainText("line 1\nline 2\r\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20");
for (auto plainTextEdit : { plainTextEdit0, plainTextEdit1, plainTextEdit2 }) {
scroll_vbox->addWidget(plainTextEdit);
auto margins = plainTextEdit->contentsMargins();
auto margin = plainTextEdit->document()->rootFrame()->frameFormat().margin();
qreal verticalmarginSize = 2 * margin + margins.top() + margins.bottom();
QFont font = plainTextEdit->font();
QFontMetrics fontMetrics(font);
int lineHeight = fontMetrics.lineSpacing();
// Portion with the incorrect behavior -> To be removed
auto sizeCalculation = [plainTextEdit, lineHeight, verticalmarginSize]() {
auto document = plainTextEdit->document();
auto size = document->size();
int height = lineHeight * size.height() + verticalmarginSize + 1;
if (height != plainTextEdit->height())
plainTextEdit->setFixedHeight(height);
};
QObject::connect(plainTextEdit, &QPlainTextEdit::textChanged, sizeCalculation);
SizeEventFilter* sizeEventFilter = new SizeEventFilter(plainTextEdit);
plainTextEdit->installEventFilter(sizeEventFilter);
QObject::connect(sizeEventFilter, &SizeEventFilter::sizeChanged, plainTextEdit, sizeCalculation, Qt::QueuedConnection);
// End of Portion with the incorrect behavior
//// Better behavior -> To be uncommented
//auto sizeCalculation1 = [plainTextEdit, scroll_vbox, lineHeight, verticalmarginSize]()
//{
// auto document = plainTextEdit->document();
// auto size = document->size();
// int height = lineHeight * size.height() + verticalmarginSize + 1;
// if (height != plainTextEdit->height())
// plainTextEdit->setFixedHeight(height);
// scroll_vbox->invalidate();
// scroll_vbox->activate();
//};
//auto sizeCalculation2 = [plainTextEdit, scroll_area, scroll_vbox, lineHeight, verticalmarginSize]()
//{
// auto document = plainTextEdit->document();
// auto size = document->size();
// int height = lineHeight * size.height() + verticalmarginSize + 1;
// bool tryWithoutScrollBar = height < plainTextEdit->height() && scroll_area->verticalScrollBarPolicy() == Qt::ScrollBarAsNeeded;
// if (tryWithoutScrollBar) {
// scroll_area->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
// scroll_vbox->invalidate();
// //scroll_vbox->update();
// scroll_vbox->activate();
// document = plainTextEdit->document();
// size = document->size();
// height = lineHeight * size.height() + verticalmarginSize + 1;
// plainTextEdit->setFixedHeight(height);
// scroll_area->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
// scroll_vbox->invalidate();
// //scroll_vbox->update();
// scroll_vbox->activate();
// }
// else if (height != plainTextEdit->height())
// plainTextEdit->setFixedHeight(height);
//};
//QObject::connect(plainTextEdit, &QPlainTextEdit::textChanged, sizeCalculation2);
//SizeEventFilter* sizeEventFilter = new SizeEventFilter(plainTextEdit);
//plainTextEdit->installEventFilter(sizeEventFilter);
//QObject::connect(sizeEventFilter, &SizeEventFilter::sizeChanged, plainTextEdit, sizeCalculation1, Qt::QueuedConnection);
//QObject::connect(sizeEventFilter, &SizeEventFilter::sizeChanged, plainTextEdit, sizeCalculation2, Qt::DirectConnection);
//// End of better behavior
plainTextEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
plainTextEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
}
scroll_vbox->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding));
main_widget->show();
return app.exec();
}
This turned out to be a lengthy answer, sorry for that...