QT QWebEngine在滚动后渲染

QT QWebEngine render after scrolling?

本文关键字:滚动 QWebEngine QT      更新时间:2023-09-26

使用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()设置在约100ms或更长的计时器上(即滚动后等待100ms保存快照),而不是在页面滚动后立即截图,它就可以工作了。因此,在执行滚动时的javascript回调和滚动页面的呈现之间存在一些延迟。我不认为这是一个完整的解决方案,因此我只更新帖子。我真正想要的是QT的回调,它说渲染的网页已经在屏幕缓冲区中更新。这样的东西存在吗?

runJavaScript的回调被触发时,脚本就完成了。但是,应该重新绘制(或至少准备重新绘制)窗口以使用QWidget::render(&pixmap)

似乎某些绘制事件可以用于检测小部件的重新绘制。不幸的是,QWebEngineView几乎没有捕捉到任何事件(除了鼠标进入和退出,最近添加的未处理的键盘事件),例如,请参阅"[QTBUG-43602]WebEngineView不处理鼠标事件"。

几乎所有事件(如鼠标移动或绘制)都由QOpenGLWidget派生的私有类型RenderWidgetHostViewQtDelegateWidgetQWebEngineView子委托处理。

可以捕获类型为QOpenGLWidgetQWebEngineView的新子级,并在该子级上安装所有所需事件的事件过滤器挂钩。

该解决方案依赖于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

子添加被WebEngineView::event捕获。将保存子指针,并在此子指针上安装事件筛选器。在子绘制事件中,在WebEngineView::eventFilter中发出信号WebEngineView::delegatePaint

当由于鼠标悬停或任何其他原因通过某些脚本或通过突出显示某些网络控件来改变网络视图时,总是发出信号delegatePaint

该信号是在QOpenGLWidget::paintEvent()的实际执行之前从事件过滤器发出的。因此,看起来只有在完全绘制完成后才需要拍摄页面快照(可能使用异步Qt::QueuedConnection连接)。在事件过滤器的这一点上,当delegatePaint由于JavaScript而被触发时,小部件似乎已经为render()做好了准备。但是,可能由于其他原因(例如,由于窗口激活)接收到喷漆事件,这可能会导致警告消息:

QWidget::重新绘制:检测到递归重新绘制

因此,最好使用Qt::QueuedConnection来避免此类问题。

现在的诀窍是在JavaScipt完成时只使用事件delegatePaint一次。该部分可根据实际需要进行调整。

由于某些脚本或加载新图像,页面视图可以随时重新绘制。让我们假设我们需要捕捉页面在脚本执行后的外观。因此,可以仅在脚本回调中将delegatePaint信号连接到saveSnapshotScroll插槽,并在saveSnapshotScroll中断开该连接。以下测试在一个循环中为三个不同的滚动位置生成快照。类似的快照由文件夹012:组织

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

在这些情况下,当事件被其他窗口交互触发时,可能会得到错误的快照。如果在脚本执行期间没有触摸窗口,则结果是正确的。


为了避免处理错误的绘制事件,可以将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提供的工具。可以考虑其他比较算法。相似性阈值也可以根据具体情况进行调整。如果滚动视图与之前的图像非常相似(例如,在长空白的情况下),则这种比较是有局限性的。