QT QWebEngine 滚动后渲染?
QT QWebEngine render after scrolling?
使用 WebEngineView 保存网页图片效果很好,但是当我想滚动并保存另一张图片时,生成的图片不显示网站已滚动(它显示网页顶部)。
我的问题是:如何在 QWebEngineView 中向下滚动然后保存显示正确滚动网页的屏幕截图?
我在网页顶部截图,向下滚动 ~700 像素,等待 javascript 回调触发,然后再截图。 javascript 和回调工作正常(我观察到 QWebEngineView 滚动)。
this->setScrollPageHandlerFunc([&] (const QVariant &result) {
saveSnapshotScroll();
});
saveSnapshotScroll();
view->page()->runJavaScript("scrollPage();",this->scrollPageHandlerFunc);
截图代码:
void MainWindow::saveSnapshotScroll()
{
QPixmap pixmap(this->size());
view->page()->view()->render(&pixmap);
pixmap.save(QString::number(QDateTime::currentMSecsSinceEpoch()) + ".png");
}
Javascript:
function scrollPage()
{
var y = qt_jq.jQuery(window).scrollTop();
qt_jq.jQuery(window).scrollTop(y+708);
}
更新: 我发现,如果我将 saveSnapshotScroll() 放在约 100 毫秒或更长时间的计时器上(即滚动后等待 100 毫秒以保存快照),而不是滚动页面后立即截取屏幕截图,它可以工作。因此,在执行滚动时 javascript 回调与滚动页面的呈现之间存在一些延迟。我不会称这是一个完整的解决方案,因此我只更新 post。我真正想要的是来自 QT 的回调,它表示呈现的网页已在屏幕缓冲区中更新。有这样的东西吗?
当 runJavaScript
的回调被触发时,脚本完成。但是,window 应该重新绘制(或至少准备重新绘制)以使用 QWidget::render(&pixmap)
。
似乎某些绘制事件可用于检测小部件的重新绘制。不幸的是 QWebEngineView
几乎没有捕获任何事件(除了鼠标进入和退出,最近添加的未处理的键盘事件),例如参见 [=40=].
几乎所有事件(如鼠标移动或绘画)都由派生自 QOpenGLWidget
.[=41 的私有类型 RenderWidgetHostViewQtDelegateWidget
QWebEngineView
child 委托处理=]
可以捕获 QOpenGLWidget
类型 QWebEngineView
的新 child 并在此 child 上安装所有需要的事件的事件过滤器挂钩。
该解决方案依赖于 QWebEngineView
的未记录结构。因此,未来的 Qt 版本可能不支持它。但是,它可用于具有当前 Qt 版本的项目。也许将来会实现一些更方便的接口来捕获 QWebEngineView
事件。
下面的例子实现了这个魔法:
#ifndef WEBENGINEVIEW_H
#define WEBENGINEVIEW_H
#include <QEvent>
#include <QChildEvent>
#include <QPointer>
#include <QOpenGLWidget>
#include <QWebEngineView>
class WebEngineView : public QWebEngineView
{
Q_OBJECT
private:
QPointer<QOpenGLWidget> child_;
protected:
bool eventFilter(QObject *obj, QEvent *ev)
{
// emit delegatePaint on paint event of the last added QOpenGLWidget child
if (obj == child_ && ev->type() == QEvent::Paint)
emit delegatePaint();
return QWebEngineView::eventFilter(obj, ev);
}
public:
WebEngineView(QWidget *parent = nullptr) :
QWebEngineView(parent), child_(nullptr)
{
}
bool event(QEvent * ev)
{
if (ev->type() == QEvent::ChildAdded) {
QChildEvent *child_ev = static_cast<QChildEvent*>(ev);
// there is also QObject child that should be ignored here;
// use only QOpenGLWidget child
QOpenGLWidget *w = qobject_cast<QOpenGLWidget*>(child_ev->child());
if (w) {
child_ = w;
w->installEventFilter(this);
}
}
return QWebEngineView::event(ev);
}
signals:
void delegatePaint();
};
#endif // WEBENGINEVIEW_H
Child 添加被 WebEngineView::event
捕获。 child 指针被保存,事件过滤器被安装在这个 child 上。在 child 绘画事件中,信号 WebEngineView::delegatePaint
在 WebEngineView::eventFilter
.
中发出
当 Web 视图被某些脚本更改或由于鼠标悬停或任何其他原因突出显示某些 Web 控件时,总是会发出信号 delegatePaint
。
事件过滤器在实际执行 QOpenGLWidget::paintEvent()
之前发出信号。因此,看起来只需要在完全绘制完成后才需要拍摄页面快照(可能使用异步 Qt::QueuedConnection
连接)。似乎在事件过滤器的这一点上,当 delegatePaint
由于 JavaScript 而被触发时,小部件已准备好 render()
。但是,可能会因为某些其他原因(例如由于 window 激活)而收到绘画事件,这可能会导致警告消息:
QWidget::repaint: Recursive repaint detected
所以,还是用Qt::QueuedConnection
来避免这样的问题比较好。
现在的诀窍是在 JavaScipt 完成时只使用事件 delegatePaint
一次。该部分可以根据实际需要进行调整。
由于某些脚本或加载新图像,页面视图可以随时重新绘制。假设我们需要捕获页面在脚本执行后的样子。因此,可以仅在脚本回调中将 delegatePaint
信号连接到 saveSnapshotScroll
插槽,并在 saveSnapshotScroll
中断开该连接。以下测试为三个不同的滚动位置循环生成快照。类似的快照由文件夹 0
、1
和 2
:
组织
void MainWindow::runJavaScript()
{
// count initialized by 0
if (++count > 1000)
return;
QString script = QString::asprintf("window.scrollTo(0, %d);", 708 * (count % 3));
view->page()->runJavaScript(script,
[&] (const QVariant&) {
connect(view, &WebEngineView::delegatePaint,
this, &MainWindow::saveSnapshotScroll,
Qt::QueuedConnection);
}
);
}
void MainWindow::saveSnapshotScroll()
{
disconnect(view, &WebEngineView::delegatePaint,
this, &MainWindow::saveSnapshotScroll);
QPixmap pixmap(view->size());
view->render(&pixmap);
pixmap.save(QString::number(count % 3) + "/" +
QString::number(QDateTime::currentMSecsSinceEpoch()) + ".png");
runJavaScript();
}
在这些情况下,当事件由其他一些 window 交互触发时,可能会得到错误的快照。如果在脚本执行期间未触及 window,则结果正确。
为避免处理错误的绘制事件,可以将 Web 视图像素图与以前保存的图像进行比较。如果这些图像之间的差异很小,则意味着应该跳过当前的绘制事件,需要等待下一个绘制事件:
void MainWindow::saveSnapshotScroll()
{
QSharedPointer<QPixmap> pixmap(new QPixmap(view->size()));
view->render(pixmap.data());
// wait for another paint event if difference with saved pixmap is small
if (!isNewPicture(pixmap))
return;
pixmap->save(QString::number(count % 3) + "/" +
QString::number(QDateTime::currentMSecsSinceEpoch()) + ".png");
disconnect(view, &WebEngineView::delegatePaint,
this, &MainWindow::saveSnapshotScroll);
runJavaScript();
}
bool MainWindow::isNewPicture(QSharedPointer<QPixmap> pixmap)
{
// initialized by nullptr
if (!prevPixmap) {
prevPixmap = pixmap;
return true;
}
// <pixmap> XOR <previously saved pixmap>
QPixmap prev(*prevPixmap);
QPainter painter;
painter.begin(&prev);
painter.setCompositionMode(QPainter::RasterOp_SourceXorDestination);
painter.drawPixmap(0, 0, *pixmap);
painter.end();
// check difference
QByteArray buf;
QBuffer buffer(&buf);
buffer.open(QIODevice::WriteOnly);
prev.save(&buffer, "PNG");
// almost empty images (small difference) have large compression ratio
const int compression_threshold = 50;
bool isNew = prev.width() * prev.height() / buf.size() < compression_threshold;
if (isNew)
prevPixmap = pixmap;
return isNew;
}
上述解决方案只是一个例子,它是基于Qt提供的工具。可以考虑其他比较算法。相似度阈值也可以根据具体情况进行调整。如果滚动视图与之前的图像非常相似(例如在长空 space 的情况下),则这种比较存在局限性。
使用 WebEngineView 保存网页图片效果很好,但是当我想滚动并保存另一张图片时,生成的图片不显示网站已滚动(它显示网页顶部)。
我的问题是:如何在 QWebEngineView 中向下滚动然后保存显示正确滚动网页的屏幕截图?
我在网页顶部截图,向下滚动 ~700 像素,等待 javascript 回调触发,然后再截图。 javascript 和回调工作正常(我观察到 QWebEngineView 滚动)。
this->setScrollPageHandlerFunc([&] (const QVariant &result) {
saveSnapshotScroll();
});
saveSnapshotScroll();
view->page()->runJavaScript("scrollPage();",this->scrollPageHandlerFunc);
截图代码:
void MainWindow::saveSnapshotScroll()
{
QPixmap pixmap(this->size());
view->page()->view()->render(&pixmap);
pixmap.save(QString::number(QDateTime::currentMSecsSinceEpoch()) + ".png");
}
Javascript:
function scrollPage()
{
var y = qt_jq.jQuery(window).scrollTop();
qt_jq.jQuery(window).scrollTop(y+708);
}
更新: 我发现,如果我将 saveSnapshotScroll() 放在约 100 毫秒或更长时间的计时器上(即滚动后等待 100 毫秒以保存快照),而不是滚动页面后立即截取屏幕截图,它可以工作。因此,在执行滚动时 javascript 回调与滚动页面的呈现之间存在一些延迟。我不会称这是一个完整的解决方案,因此我只更新 post。我真正想要的是来自 QT 的回调,它表示呈现的网页已在屏幕缓冲区中更新。有这样的东西吗?
当 runJavaScript
的回调被触发时,脚本完成。但是,window 应该重新绘制(或至少准备重新绘制)以使用 QWidget::render(&pixmap)
。
似乎某些绘制事件可用于检测小部件的重新绘制。不幸的是 QWebEngineView
几乎没有捕获任何事件(除了鼠标进入和退出,最近添加的未处理的键盘事件),例如参见 [=40=].
几乎所有事件(如鼠标移动或绘画)都由派生自 QOpenGLWidget
.[=41 的私有类型 RenderWidgetHostViewQtDelegateWidget
QWebEngineView
child 委托处理=]
可以捕获 QOpenGLWidget
类型 QWebEngineView
的新 child 并在此 child 上安装所有需要的事件的事件过滤器挂钩。
该解决方案依赖于 QWebEngineView
的未记录结构。因此,未来的 Qt 版本可能不支持它。但是,它可用于具有当前 Qt 版本的项目。也许将来会实现一些更方便的接口来捕获 QWebEngineView
事件。
下面的例子实现了这个魔法:
#ifndef WEBENGINEVIEW_H
#define WEBENGINEVIEW_H
#include <QEvent>
#include <QChildEvent>
#include <QPointer>
#include <QOpenGLWidget>
#include <QWebEngineView>
class WebEngineView : public QWebEngineView
{
Q_OBJECT
private:
QPointer<QOpenGLWidget> child_;
protected:
bool eventFilter(QObject *obj, QEvent *ev)
{
// emit delegatePaint on paint event of the last added QOpenGLWidget child
if (obj == child_ && ev->type() == QEvent::Paint)
emit delegatePaint();
return QWebEngineView::eventFilter(obj, ev);
}
public:
WebEngineView(QWidget *parent = nullptr) :
QWebEngineView(parent), child_(nullptr)
{
}
bool event(QEvent * ev)
{
if (ev->type() == QEvent::ChildAdded) {
QChildEvent *child_ev = static_cast<QChildEvent*>(ev);
// there is also QObject child that should be ignored here;
// use only QOpenGLWidget child
QOpenGLWidget *w = qobject_cast<QOpenGLWidget*>(child_ev->child());
if (w) {
child_ = w;
w->installEventFilter(this);
}
}
return QWebEngineView::event(ev);
}
signals:
void delegatePaint();
};
#endif // WEBENGINEVIEW_H
Child 添加被 WebEngineView::event
捕获。 child 指针被保存,事件过滤器被安装在这个 child 上。在 child 绘画事件中,信号 WebEngineView::delegatePaint
在 WebEngineView::eventFilter
.
当 Web 视图被某些脚本更改或由于鼠标悬停或任何其他原因突出显示某些 Web 控件时,总是会发出信号 delegatePaint
。
事件过滤器在实际执行 QOpenGLWidget::paintEvent()
之前发出信号。因此,看起来只需要在完全绘制完成后才需要拍摄页面快照(可能使用异步 Qt::QueuedConnection
连接)。似乎在事件过滤器的这一点上,当 delegatePaint
由于 JavaScript 而被触发时,小部件已准备好 render()
。但是,可能会因为某些其他原因(例如由于 window 激活)而收到绘画事件,这可能会导致警告消息:
QWidget::repaint: Recursive repaint detected
所以,还是用Qt::QueuedConnection
来避免这样的问题比较好。
现在的诀窍是在 JavaScipt 完成时只使用事件 delegatePaint
一次。该部分可以根据实际需要进行调整。
由于某些脚本或加载新图像,页面视图可以随时重新绘制。假设我们需要捕获页面在脚本执行后的样子。因此,可以仅在脚本回调中将 delegatePaint
信号连接到 saveSnapshotScroll
插槽,并在 saveSnapshotScroll
中断开该连接。以下测试为三个不同的滚动位置循环生成快照。类似的快照由文件夹 0
、1
和 2
:
void MainWindow::runJavaScript()
{
// count initialized by 0
if (++count > 1000)
return;
QString script = QString::asprintf("window.scrollTo(0, %d);", 708 * (count % 3));
view->page()->runJavaScript(script,
[&] (const QVariant&) {
connect(view, &WebEngineView::delegatePaint,
this, &MainWindow::saveSnapshotScroll,
Qt::QueuedConnection);
}
);
}
void MainWindow::saveSnapshotScroll()
{
disconnect(view, &WebEngineView::delegatePaint,
this, &MainWindow::saveSnapshotScroll);
QPixmap pixmap(view->size());
view->render(&pixmap);
pixmap.save(QString::number(count % 3) + "/" +
QString::number(QDateTime::currentMSecsSinceEpoch()) + ".png");
runJavaScript();
}
在这些情况下,当事件由其他一些 window 交互触发时,可能会得到错误的快照。如果在脚本执行期间未触及 window,则结果正确。
为避免处理错误的绘制事件,可以将 Web 视图像素图与以前保存的图像进行比较。如果这些图像之间的差异很小,则意味着应该跳过当前的绘制事件,需要等待下一个绘制事件:
void MainWindow::saveSnapshotScroll()
{
QSharedPointer<QPixmap> pixmap(new QPixmap(view->size()));
view->render(pixmap.data());
// wait for another paint event if difference with saved pixmap is small
if (!isNewPicture(pixmap))
return;
pixmap->save(QString::number(count % 3) + "/" +
QString::number(QDateTime::currentMSecsSinceEpoch()) + ".png");
disconnect(view, &WebEngineView::delegatePaint,
this, &MainWindow::saveSnapshotScroll);
runJavaScript();
}
bool MainWindow::isNewPicture(QSharedPointer<QPixmap> pixmap)
{
// initialized by nullptr
if (!prevPixmap) {
prevPixmap = pixmap;
return true;
}
// <pixmap> XOR <previously saved pixmap>
QPixmap prev(*prevPixmap);
QPainter painter;
painter.begin(&prev);
painter.setCompositionMode(QPainter::RasterOp_SourceXorDestination);
painter.drawPixmap(0, 0, *pixmap);
painter.end();
// check difference
QByteArray buf;
QBuffer buffer(&buf);
buffer.open(QIODevice::WriteOnly);
prev.save(&buffer, "PNG");
// almost empty images (small difference) have large compression ratio
const int compression_threshold = 50;
bool isNew = prev.width() * prev.height() / buf.size() < compression_threshold;
if (isNew)
prevPixmap = pixmap;
return isNew;
}
上述解决方案只是一个例子,它是基于Qt提供的工具。可以考虑其他比较算法。相似度阈值也可以根据具体情况进行调整。如果滚动视图与之前的图像非常相似(例如在长空 space 的情况下),则这种比较存在局限性。