3

WebEngineView を使用して Web ページの画像を保存すると問題なく動作しますが、別の画像をスクロールして保存しようとすると、結果の画像に Web サイトがスクロールされたことが表示されません (Web ページの上部が表示されます)。

私の質問は、QWebEngineView を下にスクロールして、正しくスクロールされた Web ページを示すスクリーンショットを保存するにはどうすればよいですか?

Web ページの上部でスクリーンショットを撮り、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 コールバックと、スクロールされたページのレンダリングの間には、ある程度の待ち時間があります。これを完全な解決策とは呼ばないので、投稿を更新するだけです。私が本当に欲しいのは、レンダリングされた Web ページが画面バッファーで更新されたことを示す QT​​ からのコールバックです。このようなものは存在しますか?

4

1 に答える 1

1

のコールバックrunJavaScriptがトリガーされると、スクリプトは終了します。ただし、 を使用するには、ウィンドウを再描画する (または少なくとも再描画の準備をする) 必要がありますQWidget::render(&pixmap)

ウィジェットの再描画を検出するには、一部のペイント イベントが役立つようです。残念ながら、QWebEngineViewはほとんどすべてのイベントをキャッチしません (マウスの開始と終了、最近追加された未処理のキーボード イベントを除く)

ほとんどすべてのイベント (マウスの移動やペイントなど) は、 から派生したQWebEngineViewプライベート タイプの子デリゲートによって処理されます。RenderWidgetHostViewQtDelegateWidgetQOpenGLWidget

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何らかのスクリプトによって、またはマウス ホバーやその他の理由により一部の Web コントロールが強調表示されることによって、Web ビューが変更されると、常にシグナルが発せられます。

信号は、 の実際の実行前にイベント フィルタから発信されQOpenGLWidget::paintEvent()ます。そのため、フル ペイントが完了した後にのみページのスナップショットを取得する必要があるようです (非同期Qt::QueuedConnection接続を使用する可能性があります)。この時点でdelegatePaint、JavaScript が原因でイベント フィルターがトリガーされたときに、ウィジェットの準備ができているように見えますrender()。ただし、他の理由 (ウィンドウのアクティブ化など) でペイント イベントを受け取る可能性があり、次の警告メッセージが表示される場合があります。

QWidget::repaint: 再帰的な再描画が検出されました

したがって、Qt::QueuedConnectionこのような問題を回避するために使用することをお勧めします。

ここでの秘訣はdelegatePaint、JavaScipt が終了したときにイベントを 1 回だけ使用することです。その部分は、実際の要件に合わせて調整できます。

ページ ビューは、スクリプトや新しい画像の読み込みにより、いつでも再描画できます。スクリプトの実行後にページがどのように見えるかをキャプチャする必要があると仮定しましょう。そのため、スクリプト コールバックでのみdelegatePaint信号をsaveSnapshotScrollスロットに接続し、その接続を で切断することができますsaveSnapshotScroll。次のテストでは、3 つの異なるスクロール位置のループでスナップショットを生成します。同様のスナップショットは、フォルダー01およびによって編成され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();
}

イベントが他のウィンドウ インタラクションによってトリガーされた場合、間違ったスナップショットを取得する可能性があります。スクリプトの実行中にウィンドウが触れられていない場合、結果は正しいです。


誤った描画イベントの処理を避けるために、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 が提供するツールに基づいています。他の比較アルゴリズムについて考えることができます。また、類似度のしきい値は、特定のケースに合わせて調整することもできます。スクロールされたビューが前の画像と非常に似ている場合 (たとえば、長い空白の場合)、このような比較には制限があります。

于 2015-11-06T21:37:05.917 に答える