Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

How to make a QTextStream stream to QTextBrowser



  • My application can interact with users in either console or GUI mode. Both modes have the sole purpose of collecting user inputs (source file, destination file, etc etc...), do some verifications, and then pass those inputs as parameters to the function that will take care of the computations.

    However, there are 2 parameters that aren't user inputs. These are outStream and errStream, and they are used by the computation function to report current status and errors. Both parameters are of type QTextStream.

    If in console mode, the function will receive QTextStream outStream(stdout) and QTextStream outStream(stderr), effectively sending the messages to stdout and stderr.

    If in GUI mode, I want those message to appear in a QTextBrowser (with the error messages in red, but whatever). To do this, I wrote a class TextBrowserStream that extends QTextStream and implements (overrides?) the << operator. This solution behaves as intended as long as I stay in the MainWindow method where the TextBrowserStream instances were created. When those instances are passed in argument, all I get is messages on stderr saying "QTextStream: No device".

    I searched for solutions, and notably found those two posts, presenting a few solutions in the replies:

    Two problems:

    • My situation requires the use of QTextStream because this is how the reports are sent to stdout and stderr when in console mode. This requirement isn't part of the posts I linked.
    • I am a beginner in both Qt and C++ and I cannot discern wether those solutions can be applied to my situation, or if they're fundamentally flawed, or if it's my basic design (using QTextStream to print on either stdout/stderr or a QTextBrowser) that doomed me from the start.

    Code snippets

    I tried to edit the snippets down to the minimum, so I omitted most of the include directives, notably.

    main.cpp

    int main(int argc, char *argv[]) {
        if (argc == 1) {
            QApplication a(argc, argv);
            MainWindow w;
            w.show();
            return a.exec();
        } else {
            return mainConsole(argc, argv);
        }
    }
    

    mainconsole.cpp

    int mainConsole(int argc, char *argv[]) {
        QTextStream outStream(stdout);
        QTextStream errStream(stderr);
        return doItAll(outStream, errStream); //terrific name, I know
    }
    

    mainwindow.cpp

    #include "textbrowserstream.h"
    MainWindow::MainWindow(QWidget *parent)
        : QMainWindow(parent), ui(new Ui::MainWindow) {
        ui->setupUi(this);
    }
    
    MainWindow::~MainWindow() {
        delete ui;
    }
    
    void MainWindow::on_buttonBox_accepted() {
        TextBrowserStream outStream(*ui->statusTextBrowser, Qt::GlobalColor::black);
        TextBrowserStream errStream(*ui->statusTextBrowser, Qt::GlobalColor::red);
        QString const HelloCpp = "Hello C++";
        outStream << HelloCpp << endl;  // Will appear in statusTextBrowser
        doItAll(outStream, errStream);
    }
    

    doitall.cpp

    #include "textbrowserstream.h"
    int doItAll(QTextStream &outStream, QTextStream &errStream) {
        QString const HelloQt = "Hello Qt";
        outStream << HelloQt << endl;
        // When called by mainConsole, will send "Hello Qt" to stdout, as intended  
        // When called by MainWindow, will send "QTextStream: No device" to stderr (!)
        return 0;
    }
    

    textbrowserstream.h

    This one is an unedited copy-pasta from my code, since this is where I am struggling.

    #ifndef TEXTBROWSERSTREAM_H
    #define TEXTBROWSERSTREAM_H
    
    #include <QWidget>
    #include <QTextStream>
    #include <QTextBrowser>
    #include <QColor>
    
    class TextBrowserStream : public QTextStream
    {
        QTextBrowser &browser;
        QColor const &color;
    
    public:
        TextBrowserStream(QTextBrowser &browser,
                          QColor const &color = Qt::GlobalColor::black)
            : browser(browser), color(color)
        {
        }
    
        // Based on gregseth's answer at https://stackoverflow.com/a/2351479
        TextBrowserStream &operator<<(QString const &string)
        {
            browser.setTextColor(color);
            browser.append(string);
    
            return *this;
        }
    };
    
    #endif // TEXTBROWSERSTREAM_H
    
    

    Final Notes

    • My code compiles, and my app outputs correct results both in console and GUI. The current issue is strictly a matter of communicating progress or errors to the user
    • I am using both Qt 5.9.5 and 5.15.0 to build (long story), and the two of them have the same behaviour
    • Let me know if more code snippets are needed
    • Thank you for your time


  • TextBrowserStream &operator<<(QString const &string) does not override QTextStream &operator<<(QString const &string) so there is no why doItAll even knows the first function exists.

    What you should do, instead of subclassing the stream is to subclass a QIODevice, override write() so that it writes to the text browser and then use a normal QTextStream on such device.

    The quick-and-easy solution, however is have a QBuffer operate on a QByteArray, use the QBuffer as device for QTextStream then connect to the QBuffer::channelBytesWritten signal to know when new data was writted by the QTextStream and copy that data to he text browser


  • Lifetime Qt Champion

    @Amelia-SZK said in How to make a QTextStream stream to QTextBrowser:

    int doItAll(QTextStream &outStream, QTextStream &errStream)

    Change it to

    int doItAll(QTextStream *outStream, QTextStream *errStream)
    


  • TextBrowserStream &operator<<(QString const &string) does not override QTextStream &operator<<(QString const &string) so there is no why doItAll even knows the first function exists.

    What you should do, instead of subclassing the stream is to subclass a QIODevice, override write() so that it writes to the text browser and then use a normal QTextStream on such device.

    The quick-and-easy solution, however is have a QBuffer operate on a QByteArray, use the QBuffer as device for QTextStream then connect to the QBuffer::channelBytesWritten signal to know when new data was writted by the QTextStream and copy that data to he text browser



  • @VRonin I subclassed a QIODevice with this stackoverflow answer (with some modifications for color support), and it worked. My outStream messages appeared in black in the QTextBrowser, and my errStream messages appeared in red, as intended.

    However, the messages are meant to inform the user on how the computations are progressing, and currently they only appear all together once doItAll has returned. Is that normal for a QIODevice?

    Would using your QBuffer solution fix this problem? I tried to do it, but couldn't manage to connect the signals and do something with them.

    Updated code snippets

    QIODevice

    This is my current implementation, using the QIODevice solution.

    mainwindow.cpp

    TextBrowserStream has been replaced by QTextStream.

    #include "texteditiodevice.h"
    // constructor and destructor omitted
    
    void MainWindow::on_buttonBox_accepted() {
        // User has clicked on OK button
    
        // Credits to TimW https://stackoverflow.com/a/2354778
        QTextStream outStream(new TextEditIoDevice(ui->statusTextBrowser, this, Qt::GlobalColor::black));
        QTextStream errStream(new TextEditIoDevice(ui->statusTextBrowser, this, Qt::GlobalColor::red));
        QString const HelloCpp = "Hello C++";
        outStream << HelloCpp << endl;  // Will appear in statusTextBrowser
        doItAll(outStream, errStream);
    }
    

    doitall.cpp

    Updated to show the new behaviour.

    int doItAll(QTextStream &outStream, QTextStream &errStream) {
        QString const Start = "Starting...";
        QString const End = "Finished.";
        outStream << Start << endl; // Should appear immediately after clicking OK. 
                                    // Instead, it appears ~4 seconds after
        /* 
        Computations taking ~4 seconds...
        */
        outStream << End << endl; // Should appear ~4 seconds after clicking OK
        return 0;
    }
    

    texteditiodevice.h

    Just like TextBrowserStream in the original snippets, this one is an unedited copy-paste.

    #ifndef TEXTEDITIODEVICE_H
    #define TEXTEDITIODEVICE_H
    
    #include <QObject>
    #include <QTextBrowser>
    #include <QPointer>
    
    // Credits to TimW https://stackoverflow.com/a/2354778
    class TextEditIoDevice : public QIODevice
    {
        Q_OBJECT
    
    public:
        TextEditIoDevice(QTextEdit *const textEdit, QObject *const parent, QColor color)
            : QIODevice(parent), textEdit(textEdit), color(color)
        {
            open(QIODevice::WriteOnly | QIODevice::Text);
        }
    
    protected:
        qint64 readData(char *data, qint64 maxSize) { return 0; }
        qint64 writeData(const char *data, qint64 maxSize)
        {
            if (textEdit)
            {
                textEdit->setTextColor(color);
                textEdit->insertPlainText(data);
            }
            return maxSize;
        }
    
    private:
        QPointer<QTextEdit> textEdit;
        QColor color;
    };
    
    #endif // TEXTEDITIODEVICE_H
    
    

    QBuffer

    This snippet describe my attempt to use the QBuffer solution. It compiled, but during runtime, the "Hello C++" printed before the call to doItAll stopped appearing, and I got those messages on stderr:

    QObject::connect: No such signal QBuffer::channelBytesWritten()
    QObject::connect:  (receiver name: 'MainWindow')
    

    mainwindow.cpp

    This is how I tried to implement the QBuffer solution. This code is no longer part of the "current" implementation.

    // constructor and destructor omitted
    
    void MainWindow::on_buttonBox_accepted() {
        // User has clicked on OK button
    
        QBuffer outBuffer(this);
        QBuffer errBuffer(this);
    
        outBuffer.open(QIODevice::ReadWrite | QIODevice::Text);
        errBuffer.open(QIODevice::ReadWrite | QIODevice::Text);
    
        QTextStream outStream(&outBuffer);
        QTextStream errStream(&errBuffer);
    
        connect(&outBuffer, SIGNAL(channelBytesWritten()),
                this, SLOT(on_stream_write(&outBuffer, Qt::GlobalColor::black)));
    
        connect(&errBuffer, SIGNAL(channelBytesWritten()),
                this, SLOT(on_stream_write(&outBuffer, Qt::GlobalColor::red)));
    
        QString const HelloCpp = "Hello C++";
        outStream << HelloCpp << endl;  // Will appear in statusTextBrowser
        doItAll(outStream, errStream);
    }
    
    void MainWindow::on_stream_write(QBuffer &buffer, Qt::GlobalColor color) {
        ui->statusTextBrowser->setTextColor(color);
        while (!buffer.atEnd()) {
            ui->statusTextBrowser->append(buffer.readLine());
        }
    }
    

    So I guess that my follow-up questions are:

    • Is it normal & expected that the QIODevice doesn't print to QTextBrowser before doItAll returns?
    • Can this be fixed with signals and slots?
    • When I tried to implement the QBuffer solution, was I on the right track?

    Thank you for your time


  • Lifetime Qt Champion

    Hi,

    Your doItAll function is blocking the event loop, therefore no repaint is done while it's doing its stuff hence you only see the messages once done.


Log in to reply