WebView ne zaman bir anlık görüntü () için hazırdır?


9

Bir o JavaFX dokümanlar devlet WebViewne zaman hazır Worker.State.SUCCEEDEDulaşıldığında Bir süre (yani beklemek sürece, bununla birlikte Animation, Transition, PauseTransitionvs.), boş bir sayfa oluşturulur.

Bu WebView içinde bir yakalama için hazır bir olay olduğunu gösterir, ama nedir?

Orada GitHub üzerinde 7.000 kod parçacıklarını kullanımıSwingFXUtils.fromFXImage ancak bunların çoğu ya alakasız gibi görünen WebView, interaktif (insan maskeleri yarış durumu) veya (2,000ms için 100ms yerden) keyfi Geçişleri kullanın.

Denedim:

  • Boyutları changed(...)içinden dinleme WebView( bu şeyleri izleyebilen yükseklik ve genişlik özellikleri DoublePropertyuygular ObservableValue)

    • Vi Uygun değil. Bazen, değerin boya rutininden ayrı olarak değiştiği ve kısmi içeriğe neden olduğu görülmektedir.
  • runLater(...)FX Application Thread'da her şeyi ve her şeyi körü körüne anlatmak .

    • TechniquesBirçok teknik bunu kullanıyor, ancak kendi birim testlerim (diğer geliştiricilerin bazı harika geri bildirimleri gibi), olayların genellikle doğru iş parçacığında olduğunu ve bu çağrının gereksiz olduğunu açıklıyor. Aklıma gelen en iyi bazıları için çalışıyor kuyrukta yeterli bir gecikme ekler olduğunu.
  • Bir DOM dinleyici / tetikleyici veya JavaScript dinleyici / tetikleyici ekleme WebView

    • CaptureKapalı SUCCEEDEDyakalamaya rağmen, her iki JavaScript ve DOM da düzgün yüklenmiş gibi görünüyor . DOM / JavaScript dinleyicileri yardımcı görünmüyor.
  • Ana FX iş parçacığını engellemeden bir Animationveya Transitionetkin bir şekilde "uyku" kullanmak.

    • Approach️ Bu yaklaşım işe yarıyor ve gecikme yeterince uzunsa, birim testlerin% 100'üne kadar çıkabilir, ancak Geçiş süreleri , sadece tahmin ettiğimiz ve kötü tasarımın gelecekteki bir anı gibi görünüyor . Performanslı veya görev açısından kritik uygulamalar için bu, programcıyı hem kullanıcı için potansiyel olarak kötü bir deneyim olan hız veya güvenilirlik arasında bir denge kurmaya zorlar.

Aramak için iyi bir zaman ne zaman WebView.snapshot(...)?

Kullanımı:

SnapshotRaceCondition.initialize();
BufferedImage bufferedImage = SnapshotRaceCondition.capture("<html style='background-color: red;'><h1>TEST</h1></html>");
/**
 * Notes:
 * - The color is to observe the otherwise non-obvious cropping that occurs
 *   with some techniques, such as `setPrefWidth`, `autosize`, etc.
 * - Call this function in a loop and then display/write `BufferedImage` to
 *   to see strange behavior on subsequent calls.
 * - Recommended, modify `<h1>TEST</h1` with a counter to see content from
 *   previous captures render much later.
 */

Kod Parçacığı:

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.WritableImage;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Logger;

public class SnapshotRaceCondition extends Application  {
    private static final Logger log = Logger.getLogger(SnapshotRaceCondition.class.getName());

    // self reference
    private static SnapshotRaceCondition instance = null;

    // concurrent-safe containers for flags/exceptions/image data
    private static AtomicBoolean started  = new AtomicBoolean(false);
    private static AtomicBoolean finished  = new AtomicBoolean(true);
    private static AtomicReference<Throwable> thrown = new AtomicReference<>(null);
    private static AtomicReference<BufferedImage> capture = new AtomicReference<>(null);

    // main javafx objects
    private static WebView webView = null;
    private static Stage stage = null;

    // frequency for checking fx is started
    private static final int STARTUP_TIMEOUT= 10; // seconds
    private static final int STARTUP_SLEEP_INTERVAL = 250; // millis

    // frequency for checking capture has occured 
    private static final int CAPTURE_SLEEP_INTERVAL = 10; // millis

    /** Called by JavaFX thread */
    public SnapshotRaceCondition() {
        instance = this;
    }

    /** Starts JavaFX thread if not already running */
    public static synchronized void initialize() throws IOException {
        if (instance == null) {
            new Thread(() -> Application.launch(SnapshotRaceCondition.class)).start();
        }

        for(int i = 0; i < (STARTUP_TIMEOUT * 1000); i += STARTUP_SLEEP_INTERVAL) {
            if (started.get()) { break; }

            log.fine("Waiting for JavaFX...");
            try { Thread.sleep(STARTUP_SLEEP_INTERVAL); } catch(Exception ignore) {}
        }

        if (!started.get()) {
            throw new IOException("JavaFX did not start");
        }
    }


    @Override
    public void start(Stage primaryStage) {
        started.set(true);
        log.fine("Started JavaFX, creating WebView...");
        stage = primaryStage;
        primaryStage.setScene(new Scene(webView = new WebView()));

        // Add listener for SUCCEEDED
        Worker<Void> worker = webView.getEngine().getLoadWorker();
        worker.stateProperty().addListener(stateListener);

        // Prevents JavaFX from shutting down when hiding window, useful for calling capture(...) in succession
        Platform.setImplicitExit(false);
    }

    /** Listens for a SUCCEEDED state to activate image capture **/
    private static ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
        if (newState == Worker.State.SUCCEEDED) {
            WritableImage snapshot = webView.snapshot(new SnapshotParameters(), null);

            capture.set(SwingFXUtils.fromFXImage(snapshot, null));
            finished.set(true);
            stage.hide();
        }
    };

    /** Listen for failures **/
    private static ChangeListener<Throwable> exceptListener = new ChangeListener<Throwable>() {
        @Override
        public void changed(ObservableValue<? extends Throwable> obs, Throwable oldExc, Throwable newExc) {
            if (newExc != null) { thrown.set(newExc); }
        }
    };

    /** Loads the specified HTML, triggering stateListener above **/
    public static synchronized BufferedImage capture(final String html) throws Throwable {
        capture.set(null);
        thrown.set(null);
        finished.set(false);

        // run these actions on the JavaFX thread
        Platform.runLater(new Thread(() -> {
            try {
                webView.getEngine().loadContent(html, "text/html");
                stage.show(); // JDK-8087569: will not capture without showing stage
                stage.toBack();
            }
            catch(Throwable t) {
                thrown.set(t);
            }
        }));

        // wait for capture to complete by monitoring our own finished flag
        while(!finished.get() && thrown.get() == null) {
            log.fine("Waiting on capture...");
            try {
                Thread.sleep(CAPTURE_SLEEP_INTERVAL);
            }
            catch(InterruptedException e) {
                log.warning(e.getLocalizedMessage());
            }
        }

        if (thrown.get() != null) {
            throw thrown.get();
        }

        return capture.get();
    }
}

İlişkili:


Platform.runLater yedekli değildir. WebView uygulamasının oluşturulmasını tamamlaması için gerekli bekleyen olaylar olabilir. Platform.runLater, ilk denediğim şey.
VGR

Yarış ve birim testleri, olayların beklemede olmadığını, ayrı bir dizide gerçekleştiğini gösterir. Platform.runLatertest edildi ve düzeltilmedi. Kabul etmiyorsanız lütfen kendiniz deneyin. Yanlış yaptığım için mutlu olurum, sorunu kapatırdı.
tresf

Ayrıca, resmi dokümanlar SUCCEEDED(dinleyicinin FX iş parçacığına ateş ettiği) durumu uygun teknik olarak hazırlar. Sıradaki etkinlikleri göstermenin bir yolu varsa, denemekten memnun olurum. Oracle forumlarındaki yorumlar ve WebViewtasarımla kendi başlığında çalışması gereken bazı SO soruları aracılığıyla seyrek öneriler buldum , bu yüzden test günlerinden sonra orada enerjiye odaklanıyorum. Bu varsayım yanlışsa, harika. Sorunu keyfi bekleme süreleri olmadan düzelten makul önerilere açığım.
tresf

Kendi çok kısa testimi yazdım ve yük işçisinin durum dinleyicisinde bir WebView'in anlık görüntüsünü başarıyla elde ettim. Ama programın bana boş bir sayfa veriyor. Hala farkı anlamaya çalışıyorum.
VGR

Bu yalnızca bir loadContentyöntem kullanılırken veya bir dosya URL'si yüklenirken olur .
VGR

Yanıtlar:


1

WebEngine loadContentyöntemlerini kullanırken oluşan bir hata gibi görünüyor . loadYerel bir dosyayı yüklemek için kullanılırken de oluşur , ancak bu durumda, reload () çağrısı dosyayı telafi edecektir.

Ayrıca, fotoğraf çekerken Sahne Alanı'nın gösterilmesi gerektiğinden show(), içeriği yüklemeden önce aramanız gerekir . İçerik eşzamansız olarak yüklendiğinden, çağrıyı izleyen loadveya loadContentbitiren ifadeden önce yüklenmesi tamamen mümkündür .

Çözüm, içeriği bir dosyaya yerleştirmek ve WebEngine reload()yöntemini tam olarak bir kez çağırmaktır . İçerik ikinci kez yüklendiğinde, yükleme işçisinin durum özelliğinin bir dinleyicisinden bir anlık görüntü başarıyla alınabilir.

Normalde bu kolay olurdu:

Path htmlFile = Files.createTempFile("snapshot-", ".html");
Files.writeString(htmlFile, html);

WebEngine engine = myWebView.getEngine();
engine.getLoadWorker().stateProperty().addListener(
    new ChangeListener<Worker.State>() {
        private boolean reloaded;

        @Override
        public void changed(ObservableValue<? extends Worker.State> obs,
                            Worker.State oldState,
                            Worker.State newState) {
            if (reloaded) {
                Image image = myWebView.snapshot(null, null);
                doStuffWithImage(image);

                try {
                    Files.delete(htmlFile);
                } catch (IOException e) {
                    log.log(Level.WARN, "Couldn't delete " + htmlFile, e);
                }
            } else {
                reloaded = true;
                engine.reload();
            }
        }
    });


engine.load(htmlFile.toUri().toString());

Ancak staticher şey için kullandığınız için bazı alanlar eklemeniz gerekir:

private static boolean reloaded;
private static volatile Path htmlFile;

Ve onları burada kullanabilirsiniz:

/** Listens for a SUCCEEDED state to activate image capture **/
private static ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
    if (newState == Worker.State.SUCCEEDED) {
        if (reloaded) {
            WritableImage snapshot = webView.snapshot(new SnapshotParameters(), null);

            capture.set(SwingFXUtils.fromFXImage(snapshot, null));
            finished.set(true);
            stage.hide();

            try {
                Files.delete(htmlFile);
            } catch (IOException e) {
                log.log(Level.WARN, "Couldn't delete " + htmlFile, e);
            }
        } else {
            reloaded = true;
            webView.getEngine().reload();
        }
    }
};

Ardından, her içerik yüklediğinizde sıfırlamanız gerekir:

Path htmlFile = Files.createTempFile("snapshot-", ".html");
Files.writeString(htmlFile, html);

Platform.runLater(new Thread(() -> {
    try {
        reloaded = false;
        stage.show(); // JDK-8087569: will not capture without showing stage
        stage.toBack();
        webView.getEngine().load(htmlFile);
    }
    catch(Throwable t) {
        thrown.set(t);
    }
}));

Çok iş parçacıklı işleme yapmanın daha iyi yolları olduğunu unutmayın. Atom sınıflarını kullanmak yerine, volatilealanları kullanabilirsiniz :

private static volatile boolean started;
private static volatile boolean finished = true;
private static volatile Throwable thrown;
private static volatile BufferedImage capture;

(boole alanları varsayılan olarak yanlıştır ve nesne alanları varsayılan olarak boştur. C programlarının aksine, bu Java tarafından yapılan bir garantidir; başlatılmamış bellek diye bir şey yoktur.)

Başka bir iş parçacığında yapılan değişiklikler için bir döngüde yoklamak yerine, senkronizasyonu, bir Kilidi veya bu şeyleri dahili olarak kullanan CountDownLatch gibi daha üst düzey bir sınıfı kullanmak daha iyidir :

private static final CountDownLatch initialized = new CountDownLatch(1);
private static volatile CountDownLatch finished;
private static volatile BufferedImage capture;
private static volatile Throwable thrown;
private static boolean reloaded;

private static volatile Path htmlFile;

// main javafx objects
private static WebView webView = null;
private static Stage stage = null;

private static ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
    if (newState == Worker.State.SUCCEEDED) {
        if (reloaded) {
            WritableImage snapshot = webView.snapshot(null, null);
            capture = SwingFXUtils.fromFXImage(snapshot, null);
            finished.countDown();
            stage.hide();

            try {
                Files.delete(htmlFile);
            } catch (IOException e) {
                log.log(Level.WARNING, "Could not delete " + htmlFile, e);
            }
        } else {
            reloaded = true;
            webView.getEngine().reload();
        }
    }
};

@Override
public void start(Stage primaryStage) {
    log.fine("Started JavaFX, creating WebView...");
    stage = primaryStage;
    primaryStage.setScene(new Scene(webView = new WebView()));

    Worker<Void> worker = webView.getEngine().getLoadWorker();
    worker.stateProperty().addListener(stateListener);

    webView.getEngine().setOnError(e -> {
        thrown = e.getException();
    });

    // Prevents JavaFX from shutting down when hiding window, useful for calling capture(...) in succession
    Platform.setImplicitExit(false);

    initialized.countDown();
}

public static BufferedImage capture(String html)
throws InterruptedException,
       IOException {

    htmlFile = Files.createTempFile("snapshot-", ".html");
    Files.writeString(htmlFile, html);

    if (initialized.getCount() > 0) {
        new Thread(() -> Application.launch(SnapshotRaceCondition2.class)).start();
        initialized.await();
    }

    finished = new CountDownLatch(1);
    thrown = null;

    Platform.runLater(() -> {
        reloaded = false;
        stage.show(); // JDK-8087569: will not capture without showing stage
        stage.toBack();
        webView.getEngine().load(htmlFile.toUri().toString());
    });

    finished.await();

    if (thrown != null) {
        throw new IOException(thrown);
    }

    return capture;
}

reloaded yalnızca JavaFX uygulama iş parçacığında erişildiğinden geçici olarak bildirilmez.


1
Bu çok hoş bir yazım, özellikle iş parçacığı ve volatiledeğişkenleri çevreleyen kod geliştirmeleri . Ne yazık ki, WebEngine.reload()daha sonra aramak ve beklemek SUCCEEDEDişe yaramaz. HTML içeriğine bir sayaç yerleştirirsem, 0, 0, 1, 3, 3, 5bunun yerine 0, 1, 2, 3, 4, 5temel yarış koşulunu düzeltmediğini düşündürerek şunu alırım.
tresf

Alıntı: "kullanmak daha iyi [...] CountDownLatch". Bu bilginin bulunması kolay olmadığından ve ilk FX başlangıcı ile kodun hızına ve basitliğine yardımcı olduğu için yükseltme.
tresf

0

Yeniden boyutlandırma ve altta yatan anlık görüntü davranışını karşılamak için, (aşağıdaki) aşağıdaki çalışma çözümünü bulduk. Bu testlerin 2.000x (Windows, macOS ve Linux) çalıştırıldığını ve% 100 başarı ile rastgele WebView boyutları sağlandığını unutmayın.

İlk olarak, JavaFX geliştiricilerinden birine teklif vereceğim. Bu, özel (sponsorlu) bir hata raporundan alıntılanmıştır:

"FX AppThread'de yeniden boyutlandırmayı başlattığınızı ve SUCCEEDED durumuna ulaşıldıktan sonra yapıldığını varsayıyorum. Bu durumda bana şu anda 2 darbe beklemek (FX AppThread'i engellemeden) webkit uygulaması, JavaFX'te bazı boyutların değişmesine neden olmazsa, bu da webkit içinde yeniden boyutların değişmesine neden olabilecekse, değişikliklerini yapmak için yeterli zaman.

Nasıl JBS tartışma içine bu bilgileri beslemek düşünüyorum, ama "sadece web bileşeni kararlı olduğunda bir anlık görüntü almalısınız" yanıtı olacaktır eminim. Bu cevabı tahmin etmek için, bu yaklaşımın sizin için işe yarayıp yaramadığını görmek iyi olur. Veya başka sorunlara neden olduğu ortaya çıkarsa, bu sorunları düşünmek ve OpenJFX'in kendisinde düzeltilip düzeltilemeyeceğini / nasıl düzeltilebileceğini görmek iyi olur. "

  1. Varsayılan olarak, JavaFX 8, 600yükseklik tam ise varsayılan değerini kullanır 0. Kodu tekrar kullanımı, WebViewkullanması gereken setMinHeight(1), setPrefHeight(1)bu sorunu önlemek için. Bu, aşağıdaki kodda değil, ancak herkesin projesine uyarlanması için bahsetmeye değer.
  2. WebKit'in hazır olmasını sağlamak için bir animasyon zamanlayıcısının içinden tam olarak iki darbe bekleyin.
  3. Anlık görüntü boş hatasını önlemek için, nabzı da dinleyen anlık görüntü geri aramasını kullanın.
// without this runlater, the first capture is missed and all following captures are offset
Platform.runLater(new Runnable() {
    public void run() {
        // start a new animation timer which waits for exactly two pulses
        new AnimationTimer() {
            int frames = 0;

            @Override
            public void handle(long l) {
                // capture at exactly two frames
                if (++frames == 2) {
                    System.out.println("Attempting image capture");
                    webView.snapshot(new Callback<SnapshotResult,Void>() {
                        @Override
                        public Void call(SnapshotResult snapshotResult) {
                            capture.set(SwingFXUtils.fromFXImage(snapshotResult.getImage(), null));
                            unlatch();
                            return null;
                        }
                    }, null, null);

                    //stop timer after snapshot
                    stop();
                }
            }
        }.start();
    }
});
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.