QtWebEngine - 同步执行 JavaScript 来读取函数结果
QtWebEngine - synchronously execute JavaScript to read function result
我的一个 C++ 类(使用 QtWebEngine)中有以下方法:
QString get()
{
QString result;
view->page()->runJavaScript("test();", [this](const QVariant &v)
{
result = v.toString();
});
return result;
}
就是执行test()
JS函数,return这个调用的结果
不幸的是,回调是异步的,程序崩溃了。我怎样才能让它发挥作用?
回调是异步的,因为 JavaScript 执行不仅发生在另一个线程中,而且发生在另一个 进程 中。所以没办法让它完全同步。
最好的解决方案是迁移您的 C++ 代码以异步工作。如果你做不到,唯一可行的解决方案是使用 QEventLoop
,有点像这样:
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()
的调用分解到另一个插槽中,该插槽将在 after 开始事件时异步调用环形。对于这样一个看似简单的任务,需要大量的胶水代码,但这就是它所需要的。这是一个例子:
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>\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 的解决方案只部分起作用。在他的示例中,代码是在按下按钮时调用的。但是,如果在加载 window 时移动相同的代码来执行,则永远不会调用已完成脚本 (onScriptEnded) 的插槽。为了解决这个问题,我不得不将调用 JS 本身的评估(在我的项目中称为 evaluateJavaScript)包装在另一个 QTimer::singleShot:
QTimer::singleShot(0, this, [&] { evaluateJavaScript(); });
不幸的是,在我的真实项目中,我有很多电话,通常是一个评估在等待另一个评估完成之前。几乎不可能每次都使用它,所以我仍在寻找解决方案。
在继承自QWebEngineView的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中
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;
}
我的一个 C++ 类(使用 QtWebEngine)中有以下方法:
QString get()
{
QString result;
view->page()->runJavaScript("test();", [this](const QVariant &v)
{
result = v.toString();
});
return result;
}
就是执行test()
JS函数,return这个调用的结果
不幸的是,回调是异步的,程序崩溃了。我怎样才能让它发挥作用?
回调是异步的,因为 JavaScript 执行不仅发生在另一个线程中,而且发生在另一个 进程 中。所以没办法让它完全同步。
最好的解决方案是迁移您的 C++ 代码以异步工作。如果你做不到,唯一可行的解决方案是使用 QEventLoop
,有点像这样:
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()
的调用分解到另一个插槽中,该插槽将在 after 开始事件时异步调用环形。对于这样一个看似简单的任务,需要大量的胶水代码,但这就是它所需要的。这是一个例子:
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>\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 的解决方案只部分起作用。在他的示例中,代码是在按下按钮时调用的。但是,如果在加载 window 时移动相同的代码来执行,则永远不会调用已完成脚本 (onScriptEnded) 的插槽。为了解决这个问题,我不得不将调用 JS 本身的评估(在我的项目中称为 evaluateJavaScript)包装在另一个 QTimer::singleShot:
QTimer::singleShot(0, this, [&] { evaluateJavaScript(); });
不幸的是,在我的真实项目中,我有很多电话,通常是一个评估在等待另一个评估完成之前。几乎不可能每次都使用它,所以我仍在寻找解决方案。
在继承自QWebEngineView的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中
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;
}