Can't lay out vertical items with main scrollbar properly in QT

47 Views Asked by At

I want to make a widget that contains 0 to N child QTextEdit so that QTextEdits don't occupy all available space. Main widget should manage both scrollbars. In other words, I want vertical layout to only manage children placement but not their size, their size should be managed by its contents (text in that case). Contents can be dynamic and changed in runtime

My code looks like that:

scroll_area = new QScrollArea();
scroll_widget = new QWidget();

scroll_vbox = new QVBoxLayout(scroll_widget);
scroll_vbox->setContentsMargins(0, 0, 0, 0);
scroll_vbox->setSpacing(0);

QTextEdit *chunk0 = new QTextEdit();
chunk0->setText("line 1\nline 2");
chunk0->setSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Minimum);
chunk0->setWordWrapMode(QTextOption::NoWrap);
scroll_vbox->addWidget(chunk0);

QTextEdit *chunk1 = new QTextEdit();
chunk1->setText("line 1\nline 2\nline 3\nline 4");
chunk1->setSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Minimum);
chunk1->setWordWrapMode(QTextOption::NoWrap);
scroll_vbox->addWidget(chunk1);

QTextEdit *chunk2 = new QTextEdit();
chunk2->setText("looooooooooooooooong text");
chunk2->setSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Minimum);
chunk2->setWordWrapMode(QTextOption::NoWrap);
scroll_vbox->addWidget(chunk2);

QTextEdit *chunk3 = new QTextEdit();
chunk3->setText("even loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooonger teeeeeeeeeeeeeeeeeeeeeeeeexxxxxxxxxxxxxxxtttttttttttt");
chunk3->setSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Minimum);
chunk3->setWordWrapMode(QTextOption::NoWrap);
scroll_vbox->addWidget(chunk3);

QTextEdit *chunk4 = new QTextEdit();
chunk4->setText("line 1\nline 2\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");
chunk4->setSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Minimum);
chunk4->setWordWrapMode(QTextOption::NoWrap);
scroll_vbox->addWidget(chunk4);

scroll_area->setWidget(scroll_widget);
scroll_area->setWidgetResizable(true);

vbox0 = new QVBoxLayout(main_widget);
vbox0->addWidget(scroll_area);

Below are images of what I want and what I get

Wanted layout:
wanted

Actual layout:
actual

There are a few problems with my layout:

  1. Children are not resized to have minimum required size. If I add only one item, it gets expanded to the full parent width ignoring it's contents (text)
  2. Children have scrollbars. I can hide them but they will still work with mouse wheel and parent widget wouldn't create a global scrollbar for them
1

There are 1 best solutions below

0
Atmo On

If I made no mistake, what you want should look like the code below. In total, I have:

  1. Placed your code in an int main(int argc, char** argv) function for ease of testing. Your inital code has been placed in extra brackets { }.
    Note that this forced me to declare variables separately from their initialization.
  2. Factored the code a little bit to treat all the chunks the same way with less code.
  3. added a call to setHorizontalScrollBarPolicy and setVerticalScrollBarPolicy.
  4. Performed the size calculation, which works both initially and when you edit the text.
  5. Added a spacer at the bottom to cramp the chunks together.

Here is the code:

#include <QtGui/QFontMetrics>
#include <QtGui/QTextFormat>
#include <QtGui/QTextFrame>

#include <QtWidgets/QApplication>
#include <QtWidgets/QBoxLayout>
#include <QtWidgets/QScrollArea>
#include <QtWidgets/QStyle>
#include <QtWidgets/QTextEdit>

int main(int argc, char** argv) {
    QApplication app(argc, argv);
    app.setQuitOnLastWindowClosed(true);

    auto main_widget = new QWidget();
    main_widget->setMinimumSize(800, 600);

    QScrollArea* scroll_area = nullptr;
    QWidget* scroll_widget = nullptr;
    QVBoxLayout* scroll_vbox = nullptr;
    {
        //Question code starts here.
        scroll_area = new QScrollArea();
        scroll_widget = new QWidget();

        scroll_vbox = new QVBoxLayout(scroll_widget);
        scroll_vbox->setContentsMargins(0, 0, 0, 0);
        scroll_vbox->setSpacing(0);
        //5 chunks (type QTextEdit) are used for this test.
        QTextEdit* chunk0 = new QTextEdit(),
            * chunk1 = new QTextEdit(),
            * chunk2 = new QTextEdit(),
            * chunk3 = new QTextEdit(),
            * chunk4 = new QTextEdit();
        chunk0->setText("line 1\nline 2");
        chunk1->setText("line 1\nline 2\nline 3\nline 4");
        chunk2->setText("looooooooooooooooong text");
        chunk3->setText("even loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooonger teeeeeeeeeeeeeeeeeeeeeeeeexxxxxxxxxxxxxxxtttttttttttt");
        chunk4->setText("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");

        for (auto chunk : { chunk0, chunk1, chunk2, chunk3, chunk4 }) {
            //This line, from the question, is becoming useless.
            //chunk->setSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Minimum);
            chunk->setWordWrapMode(QTextOption::NoWrap);
            chunk->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
            chunk->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
            scroll_vbox->addWidget(chunk);
            //Below, we retrieve all the elements necessary to calculate the chunks' sizes. We repeat all the steps for every chunk in case they have different stylesheets attached.
            //A QTextEdit has margins, that cause it to grow a little bit bigger than what the text needs to render. 
            auto margins = chunk->contentsMargins();
            auto margin = chunk->document()->rootFrame()->frameFormat().margin();
            qreal horizontalmarginSize = 2 * margin + margins.left() + margins.right(),
                  verticalmarginSize = 2 * margin + margins.top() + margins.bottom();
            //sizeCalculation will be used both to calculate the initial size for the chunk and will be connected to the textChanged signal.
            auto sizeCalculation = [chunk, horizontalmarginSize, verticalmarginSize]()
            {
                QFontMetrics fm(chunk->font());
                //rect is sized precisely to contain the text.
                auto rect = chunk->style()->itemTextRect(fm, QRect(0, 0, 1, 1), Qt::AlignLeft, true, chunk->toPlainText());
                chunk->setFixedSize(rect.width() + horizontalmarginSize, rect.height() + verticalmarginSize);
            };
            sizeCalculation();
            QObject::connect(chunk, &QTextEdit::textChanged, sizeCalculation);
        }

        scroll_vbox->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding));


        scroll_area->setWidget(scroll_widget);
        scroll_area->setWidgetResizable(true);

        auto vbox0 = new QVBoxLayout(main_widget);
        vbox0->addWidget(scroll_area);
        //Question code ends here.
    }


    main_widget->show();
    
    return app.exec();
}

What I did not do, however, is implement an optimization to detect when the widget size does not need to be changed, i.e. when text is being edited without changing the number of line (height does not change) and not on the longest line of text (width does not change).