QtWebEngine - 同步执行 JavaScript 读取函数结果

2022-01-03 00:00:00 multithreading qt callback qtwebengine c++

我在我的一个 C++ 类(使用 QtWebEngine)中有以下方法:

I have the following method in one of my C++ classes (using QtWebEngine):

    QString get()
    {
        QString result;

        view->page()->runJavaScript("test();", [this](const QVariant &v)
            {
                result = v.toString();
            });

        return result;
    }

就是执行test() JS函数并返回本次调用的结果.

It is to execute test() JS function and return the result of this invocation.

不幸的是,回调是异步的,程序崩溃了.我怎样才能让它工作?

Unfortunately, the callback is asynchronous and the program crashes. How can I make it work?

推荐答案

回调是异步的,因为 JavaScript 执行不仅发生在另一个线程中,而且发生在另一个进程中.所以没有办法让它完全同步.

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.

最好的解决方案是迁移您的 C++ 代码以异步工作.如果你不能这样做,唯一可行的解??决方案是使用QEventLoop,有点像这样:

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;
}

但是,请注意,对于实际使用,此示例过于简化:您需要确保在事件循环启动之前未运行 JavaScript.最正确的方法是实现一个合适的槽而不是 lambda + 将 view->page()->runJavaScript() 的调用分解到另一个将被调用的槽中异步在启动事件循环之后.对于这样一个看似简单的任务,需要大量的胶水代码,但这就是它所需要的.举个例子:

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_OBJECT
public:
    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>
</head>"
                                  "<script type="text/javascript">function test() { return "A!"; }</script>"
                                  "<body>text
</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));
}

相关文章