QtWebEngine - synchronously execute JavaScript to read function result QtWebEngine - synchronously execute JavaScript to read function result multithreading multithreading

QtWebEngine - synchronously execute JavaScript to read function result


The callback is asynchronous because the JavaScript execution occurs not only in another thread but in another process. So there is no way to make it fully synchronous.

The best possible solution would be to migrate your C++ code to work asynchronously. If you can't do that, the only feasible solution is to use QEventLoop, somewhat like this:

void ranJavaScript(){    emit notifyRanJavaScript();}QString get(){    QString result;    QEventLoop loop;    QObject::connect(this, SIGNAL(notifyRanJavaScript()), &loop, SLOT(quit()));    view->page()->runJavaScript("test();", [this](const QVariant &v)        {            result = v.toString();            this.ranJavaScript();        });    loop.exec();    return result;}

However, note that this example is oversimplified for a real-world usage: you need to ensure the JavaScript was not ran before the event loop is started. The most proper way to do that would involve implementing a proper slot instead of a lambda + factoring out the call to view->page()->runJavaScript() into another slot which would be called asynchronously after starting the event loop. It is a lot of glue code for such a seemingly simple task but that's what it takes. Here's an example:

MainWindow.h

#ifndef TMP_MAIN_WINDOW_H#define TMP_MAIN_WINDOW_H#include <QMainWindow>#include <QVariant>class QWebEngineView;class QPushButton;class MainWindow : public QMainWindow{    Q_OBJECTpublic:    MainWindow(QWidget * parent = 0);    QString get();    void onScriptEnded(const QVariant & data);Q_SIGNALS:    void notifyRanJavaScript();private Q_SLOTS:    void onButtonPressed();    void startScript();private:    QWebEngineView *    m_view;    QPushButton *       m_button;    QString             m_scriptResult;};#endif // TMP_MAIN_WINDOW_H

MainWindow.cpp

#include "MainWindow.h"#include <QWebEngineView>#include <QPushButton>#include <QHBoxLayout>#include <QVBoxLayout>#include <QMessageBox>#include <QEventLoop>#include <QDebug>#include <QTimer>MainWindow::MainWindow(QWidget * parent) :    QMainWindow(parent){    m_view = new QWebEngineView;    QWebEnginePage * page = new QWebEnginePage(m_view);    m_view->setPage(page);    QString html = QStringLiteral("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\""                                  "\"http://www.w3.org/TR/html4/strict.dtd\"><html>"                                  "<head><h3>head</h3>\n</head>"                                  "<script type=\"text/javascript\">function test() { return \"A!\"; }</script>"                                  "<body>text\n</body></html>");    m_view->page()->setHtml(html);    m_button = new QPushButton;    m_button->setMinimumWidth(35);    m_button->setText(QStringLiteral("Test"));    QObject::connect(m_button, SIGNAL(pressed()), this, SLOT(onButtonPressed()));    QHBoxLayout * buttonLayout = new QHBoxLayout;    buttonLayout->addWidget(m_button);    buttonLayout->addStretch();    QVBoxLayout * viewLayout = new QVBoxLayout;    viewLayout->addLayout(buttonLayout);    viewLayout->addWidget(m_view);    QWidget * widget = new QWidget(this);    widget->setLayout(viewLayout);    setCentralWidget(widget);}QString MainWindow::get(){    QEventLoop loop;    QObject::connect(this, SIGNAL(notifyRanJavaScript()), &loop, SLOT(quit()));    // Schedule the slot to run in 0 seconds but not right now    QTimer::singleShot(0, this, SLOT(startScript()));    // The event loop would block until onScriptEnded slot is executed    loop.exec();    // If we got here, the script has been executed and the result was saved in m_scriptResult    return m_scriptResult;}void MainWindow::onScriptEnded(const QVariant & data){    qDebug() << QStringLiteral("Script ended: ") << data;    m_scriptResult = data.toString();    emit notifyRanJavaScript();}void MainWindow::onButtonPressed(){    QString str = get();    QMessageBox::information(this, QStringLiteral("Script result"), str,                             QMessageBox::StandardButton::Ok);}struct Functor{    Functor(MainWindow & window) : m_window(window) {}    void operator()(const QVariant & data)    {        m_window.onScriptEnded(data);    }    MainWindow & m_window;};void MainWindow::startScript(){    qDebug() << QStringLiteral("Start script");    m_view->page()->runJavaScript(QStringLiteral("test();"), Functor(*this));}


Dmitry's solution only works partially, unfortunately. In his example the code is invoked upon pressing a button. However, if the same code is moved to execute while the window is loading, the slot for the finished script (onScriptEnded) is never called. To fix this, I had to wrap the call to the evaluation of JS itself (called evaluateJavaScript in my project) in another QTimer::singleShot:

QTimer::singleShot(0, this, [&] { evaluateJavaScript(); });

Unfortunately, in my real project I have many calls, often one evaluation waiting for another before it to finish. It's practically impossible to use this every single time so I still look for a solution.


In the kchmviewer project, I found a very easy way to wait for the result of the runJavaScript function. The following code is part of the ViewWindow class which inherits from QWebEngineView.

int ViewWindow::getScrollbarPosition(){    QAtomicInt value = -1;    page()->runJavaScript("document.body.scrollTop", [&value](const QVariant &v)        {            qDebug( "value retrieved: %d\n", v.toInt());            value = v.toInt();        });    while (value == -1)    {        QApplication::processEvents();    }    qDebug( "scroll value %d", value.load() );    return value;}