1
votes

I'm trying to implement a cell-based widget similar to the one in Jupyter Notebook in Qt where multiple cells are stacked vertically. Each cell is made of a QTextEdit and multiple buttons and the cell's vertical size would grow by entering more text into the text edit (i.e. no vertical scrollbars in the text edit).

However I'm not sure what the best way would be to implement such a widget in Qt. I've tried a QListWidget with a custom widget (made of a QTextEdit and a couple of buttons) for each item, however the custom widget wouldn't grow vertically by entering more text and instead the QTextEdit scrollbars would show up. I also implemented a second version using QListView and QStyledItemDelegate however I encountered multiple issues including not being able to interact with the painted buttons (through the delegate).

Any suggestions/examples about how to implement such a cell-based widget?

1

1 Answers

1
votes

You can subclass QTextEdit and listen for changes in the document size using document()->size() (not the size of the QTextEdit, mind). You can then resize the QTextEdit's parent to reflect the new size. To fill the parent with the widget, use a layout. I've used QVBoxLayout several times below for this purpose. (One, to hold multiple QTextEdit widgets in a vertical column; Two, to hold a single TextEdit).

#include <QtCore>
#include <QtWidgets>

class DyTextEdit : public QTextEdit
{
    Q_OBJECT
public:
    DyTextEdit(QWidget *parent = nullptr) :
        QTextEdit(parent)
    {
        QTextDocument *doc = document();
        currentSize = doc->size();

        qDebug() << "[Init] Size:" << currentSize;

        //  listen to text/content changes
        connect(this, &QTextEdit::textChanged, this, &DyTextEdit::on_textChanged);

        //  connect <> 💡
        connect(this, &DyTextEdit::sizeChanged, this, &DyTextEdit::resizeParent);

        doc->setTextWidth(-1);

        emit sizeChanged(currentSize);  //  init
    }

signals:
    //  emitted when document size changes
    void sizeChanged(QSizeF size);

public slots:
    void on_textChanged()
    {
        QSizeF newSize = document()->size();

        qDebug() << "[TextChanged] Size:" << newSize;

        //  detect changes in the document size
        if (newSize != currentSize)
            emit sizeChanged(newSize);  //  emit signal💡

        //  update size
        currentSize = newSize;
    }

    void resizeParent(QSizeF size)
    {
        //  resize the parent's height, don't bother with the width
        parentWidget()->setFixedHeight(size.height());
    }

private:
    QSizeF currentSize; //  keeps track of current document size

};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    //  holds all dytextedits
    QWidget aggregateWidget;
    QVBoxLayout aggregateLayout(&aggregateWidget);

    //  note: for scalability, use a QList (or QLists)

    //  first textbox
    QWidget widget;
    QVBoxLayout vbLayout(&widget);
    vbLayout.setMargin(0);
    DyTextEdit dytext(&widget);
    vbLayout.addWidget(&dytext);

    //  first button row
    QHBoxLayout hbLayout(&widget);
    hbLayout.setMargin(0);
    QPushButton pb1a("Push this.", &widget);
    hbLayout.addWidget(&pb1a);
    QPushButton pb1b("Push this.", &widget);
    hbLayout.addWidget(&pb1b);

    //  second textbox
    QWidget widget2;
    QVBoxLayout vbLayout2(&widget2);
    vbLayout2.setMargin(0);
    DyTextEdit dytext2(&widget2);
    vbLayout2.addWidget(&dytext2);

    //  second button row
    QHBoxLayout hbLayout2(&widget2);
    hbLayout2.setMargin(0);
    QPushButton pb2a("Push this.", &widget2);
    hbLayout2.addWidget(&pb2a);
    QPushButton pb2b("Push this.", &widget2);
    hbLayout2.addWidget(&pb2b);

    //  add widgets to layout
    aggregateLayout.addWidget(&widget);     //  cell 1
    aggregateLayout.addLayout(&hbLayout);   //  |
    aggregateLayout.addWidget(&widget2);    //  cell 2
    aggregateLayout.addLayout(&hbLayout2);  //  |

    aggregateLayout.setSizeConstraint(QLayout::SetMinAndMaxSize);

    aggregateWidget.show();

    return a.exec();
}

#include "main.moc" //  include if there are QObject classes in main.cpp

Note that above, I've used rows of buttons using QHBoxLayout and added them to the overall, aggregate layout detached from the textbox widgets. You can still use a say, QGridLayout if you wish to set your buttons in a different format. You can also combine the textedits and button layouts as children under another widget (to serve as an umbrella) covering both. That would more clearly form a cell of widget and may be easier to move around. (I've uploaded a workable example of this on Github.)

Demo (macOS):

This is a screenshot of after typing content and copy-pasting content.

demonstration using both typed and copied content

Note that the aggregate widget will contract on removal of lines of text.