javascript - QT QWebEngine 滚动后渲染?

标签 javascript c++ qt qt5 qtwebengine

使用 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 回调与滚动页面的呈现之间存在一些延迟。我不会称这是一个完整的解决方案,这也是为什么我只更新帖子的原因。我真正想要的是来自 QT 的回调,表示渲染的网页已在屏幕缓冲区中更新。这样的东西存在吗?

最佳答案

runJavaScript回调触发脚本完成。但是,窗口应该重新粉刷(或至少准备重新粉刷)以使用 QWidget::render(&pixmap) .

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

几乎所有事件(如鼠标移动或绘画)都由 QWebEngineView 处理。私有(private)类型的子代表 RenderWidgetHostViewQtDelegateWidget派生自 QOpenGLWidget .

有可能抓到QWebEngineView的新子类型 QOpenGLWidget并在这个 child 上安装所有需要事件的事件过滤器钩子(Hook)。

该解决方案依赖于 QWebEngineView 的未记录结构.因此, future 的 Qt 版本可能不支持它。但是,它可用于具有当前 Qt 版本的项目。或许在 future 一些更方便的界面可以捕捉 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::delegatePaintWebEngineView::eventFilter 中发出.

信号delegatePaint当 web View 被某些脚本更改或由于鼠标悬停或任何其他原因突出显示某些 web 控件时,总是会发出。

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

QWidget::repaint: Recursive repaint detected



所以,还是用Qt::QueuedConnection比较好以避免此类问题。

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

由于某些脚本或加载新图像,页面 View 可以随时重新绘制。假设我们需要捕获页面在脚本执行后的外观。因此,可以连接 delegatePaintsaveSnapshotScroll 发出信号仅在脚本回调中插入插槽并在 saveSnapshotScroll 中断开该连接.以下测试为三个不同的滚动位置循环生成快照。类似的快照由文件夹 0 组织, 12 :
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 View 像素图与之前保存的图像进行比较。如果这些图像之间的差异很小,则意味着应跳过当前的绘制事件,并需要等待下一个绘制事件:
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 提供的工具。可以考虑其他比较算法。相似度阈值也可以根据具体情况进行调整。如果 ScrollView 与前一个图像非常相似(例如在长空白的情况下),则这种比较会受到限制。

关于javascript - QT QWebEngine 滚动后渲染?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33467776/

相关文章:

javascript - jquery 对话框可拖动且可调整大小

javascript - 如何使用 ajax 从 IMAP 服务器获取电子邮件

c++: 从 "const variable*"转换为 "uint32"失去精度

c++ - 内核模式 - 通过共享内存进行用户模式通信,无需使用系统线程

qt - 将 Qt 应用程序连接到 Internet (Symbian)

javascript - 表达式中的 AngularJs 函数

javascript - 循环内未定义下划线模板

c++ - _di_IXMLNode 中不需要的 xmlns =""

c++ - 在 qt5 中使用 QSharedPointers 时出现段错误

c++ - QT C++ 在关闭应用程序时防止运行时错误