QT QWebEngine рендеринг после прокрутки?

Сохранение изображения веб-страницы с 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, который говорит, что отображаемая веб-страница была обновлена ​​в экранном буфере. Существует ли что-то подобное?

2

Решение

Когда обратный вызов runJavaScript запущен сценарий закончен. Тем не менее, окно должно быть перекрашено (или, по крайней мере, подготовлено для перекраски), чтобы использовать QWidget::render(&pixmap),

Кажется, что некоторое событие рисования может быть полезно для обнаружения перерисовки виджета. К сожалению QWebEngineView не перехватывает практически ни одно событие (кроме входа и выхода мыши, недавно добавленных необработанных событий клавиатуры), например, см. «[QTBUG-43602] WebEngineView не обрабатывает события мыши».

Почти все события (например, перемещение мыши или рисование) обрабатываются QWebEngineView дочерний делегат закрытого типа RenderWidgetHostViewQtDelegateWidget что происходит от QOpenGLWidget,

Можно поймать нового ребенка QWebEngineView типа QOpenGLWidget и установить на этом дочернем элементе фильтр событий для всех необходимых событий.

Это решение опирается на недокументированную структуру 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::delegatePaint испускается в WebEngineView::eventFilter,

Сигнал delegatePaint всегда генерируется, когда веб-представление изменяется каким-либо сценарием или выделением некоторых веб-элементов управления из-за наведения мыши или по любой другой причине.

Сигнал испускается из фильтра событий до фактического выполнения QOpenGLWidget::paintEvent(), Таким образом, похоже, что снимок страницы нужно делать только после полной рисования (возможно, с использованием асинхронного Qt::QueuedConnection подключение). Похоже, что в этот момент в фильтре событий, когда delegatePaint срабатывает из-за JavaScript виджет готов к render(), Однако возможно получить событие рисования по какой-то другой причине (например, из-за активации окна), и это может привести к появлению предупреждающего сообщения:

QWidget :: repaint: обнаружена рекурсивная перекраска

Итак, все же лучше использовать Qt::QueuedConnection чтобы избежать таких проблем.

Теперь хитрость заключается в использовании события delegatePaint только один раз, когда JavaScipt закончен. Эта часть может быть адаптирована к фактическим требованиям.

Вид страницы может быть перекрашен в любой момент из-за некоторых сценариев или загрузки новых изображений. Давайте предположим, что нам нужно захватить страницу, как она выглядит после выполнения скрипта. Итак, можно подключить 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();
}

В тех случаях, когда событие вызвано каким-либо другим взаимодействием с окном, можно получить неправильный снимок. Если окно не было затронуто во время выполнения скрипта, результат верный.


Чтобы избежать обработки неправильных событий рисования, можно сравнить растровое изображение веб-представления с ранее сохраненным изображением. Если разница между этими изображениями невелика, это означает, что текущее событие рисования должно быть пропущено, и необходимо дождаться следующего события рисования:

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. Можно подумать о других алгоритмах сравнения. Также порог сходства может быть скорректирован в зависимости от конкретного случая. Существует ограничение такого сравнения, если прокручиваемый вид очень похож на предыдущее изображение (например, в случае длинного пустого пространства).

1

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]