Коли WebView готовий до знімка ()?


9

У JavaFX DOCS стан , що WebViewготова , коли Worker.State.SUCCEEDEDбуде досягнуто , однак, якщо ви будете чекати деякий час (тобто Animation, Transition, PauseTransitionі т.д.), порожня сторінка відображається.

Це говорить про те, що всередині WebView є подія, яка готує її до захоплення, але що це?

У GitHubSwingFXUtils.fromFXImage є понад 7000 фрагментів коду, які використовують, але більшість з них, здається, не пов'язані WebView, є інтерактивними (людина маскує перегонний стан) або використовують довільні переходи (десь від 100 мс до 2000 мс).

Я спробував:

  • Прослуховування в changed(...)межах WebView"розмірів" ( DoublePropertyреалізовані властивості висоти та ширини ObservableValue, які можуть контролювати ці речі)

    • Ot Не життєздатний. Іноді значення, здається, змінюється окремо від звичайної фарби, що призводить до часткового вмісту.
  • Сліпо розповідаючи що-небудь про все runLater(...)на FX Application Thread.

    • Any Багато методів використовують це, але мої власні тестові одиниці (а також деякі чудові відгуки інших розробників) пояснюють, що події часто вже знаходяться в потрібній нитці, і цей виклик є зайвим. Найкраще, що я можу придумати, - це лише достатня кількість затримок через чергування, яка для деяких працює.
  • Додавання слухача / тригера DOM або слухача / тригера JavaScript до WebView

    • SeemБуде JavaScript і DOM, здається, завантажуються належним чином при виклику, SUCCEEDEDнезважаючи на пусте захоплення. Слухачі DOM / JavaScript, схоже, не допомагають.
  • Використання Animationабо Transitionдля ефективного "сну" без блокування основного потоку FX.

    • Цей підхід працює, і якщо затримка буде достатньо довгою, може принести до 100% одиничних тестів, але час переходу, здається, є деяким майбутнім моментом, про який ми просто здогадуємось і поганий дизайн. Для виконавців або критично важливих програм це змушує програміста здійснити компроміс між швидкістю або надійністю, і потенційно поганим досвідом для користувача.

Коли гарний час зателефонувати WebView.snapshot(...)?

Використання:

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.
 */

Фрагмент коду:

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

Пов'язані:


Platform.runLater не є зайвим. Можуть бути очікувані події, необхідні веб-перегляду для завершення його візуалізації. Platform.runLater - це перше, що я спробував би.
VGR

Перегони, а також одиничні тести дозволяють припустити, що події не очікуються, а трапляються в окремому потоці. Platform.runLaterбув протестований і не виправляє. Спробуйте самі, якщо ви не згодні. Я був би радий помилитися, це закрило б питання.
tresf

Крім того, офіційні документи встановлюють SUCCEEDEDстан (про який слухач спрацьовує на потоці FX) - це правильна методика. Якщо є спосіб показати події, що стоять у черзі, я буду в захваті від спробувати. Я знайшов рідкісні пропозиції через коментарі на форумах Oracle та деякі питання ТА, які WebViewповинні запускатись у власну тему, тому після днів тестування я зосереджуюсь на енергії. Якщо це припущення неправильне, чудово. Я відкритий до будь-яких розумних пропозицій, які вирішують проблему без довільних термінів очікування.
tresf

Я написав власний дуже короткий тест і зміг успішно отримати знімок WebView у слухача штатного працівника. Але ваша програма дає мені порожню сторінку. Я все ще намагаюся зрозуміти різницю.
VGR

Здається, це відбувається лише при використанні loadContentметоду або при завантаженні URL-адреси файлу.
VGR

Відповіді:


1

Здається, це помилка, яка виникає при використанні loadContentметодів WebEngine . Він також виникає при використанні loadдля завантаження локального файлу, але в цьому випадку виклик reload () компенсує це.

Крім того, оскільки етап повинен бути показаний під час зйомки, вам потрібно зателефонувати show()перед завантаженням вмісту. Оскільки вміст завантажується асинхронно, цілком можливо, що він буде завантажений перед оператором після виклику loadабо loadContentзавершення.

Вирішення цього завдання полягає в тому, щоб розмістити вміст у файлі та викликати reload()метод WebEngine рівно один раз. Другий раз, коли вміст завантажується, знімок можна успішно зробити у слухача стану власності працівника вантажу.

Зазвичай це буде просто:

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

Але оскільки ви використовуєте staticдля всього, вам доведеться додати кілька полів:

private static boolean reloaded;
private static volatile Path htmlFile;

І ви можете використовувати їх тут:

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

І тоді вам доведеться скидати його щоразу, коли ви завантажуєте вміст:

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

Зауважте, що є кращі способи виконання багатопотокової обробки. Замість використання атомних класів ви можете просто використовувати volatileполя:

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

(Булеві поля за замовчуванням є помилковими, а об'єктні поля за замовчуванням - нульовими. На відміну від програм C, це важка гарантія, зроблена Java; немає такої речі, як неініціалізована пам'ять.)

Замість опитування в циклі для змін, внесених в інший потік, краще використовувати синхронізацію, блокування або клас вищого рівня, як CountDownLatch, який використовує ці речі внутрішньо:

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 не оголошується мінливим, оскільки він доступний лише в потоці додатків JavaFX.


1
Це дуже приємне написання, особливо вдосконалення коду, що стосується потоку та volatileзмінних. На жаль, дзвонити WebEngine.reload()і чекати наступного SUCCEEDEDне виходить. Якщо я розміщую лічильник у вмісті HTML, я отримую: 0, 0, 1, 3, 3, 5замість того 0, 1, 2, 3, 4, 5, щоб припустити, що він фактично не фіксує основні умови гонки.
tresf

Цитата: "краще використовувати [...] CountDownLatch". Оновлення, оскільки цю інформацію було не просто знайти, і вона допомагає швидкості та простоті коду при початковому запуску FX.
tresf

0

Для розміщення розміру та базової поведінки знімка я (ми) придумав наступне робоче рішення. Зауважте, ці тести були виконані 2 000x (Windows, macOS та Linux), забезпечуючи випадкові розміри WebView зі 100% успіхом.

По-перше, я цитую один з розробників JavaFX. Це цитується з приватного (спонсорованого) звіту про помилку:

"Я припускаю, що ви ініціюєте зміну розміру на FX AppThread, і це робиться після досягнення стану SUCCEEDED. У такому випадку мені здається, що в цей момент, очікуючи 2 імпульси (не блокуючи FX AppThread), слід дати впровадженню webkit достатньо часу, щоб внести свої зміни, якщо тільки це не призведе до зміни деяких параметрів у JavaFX, що може призвести знову до зміни розмірів всередині webkit.

Я роздумую над тим, як подати цю інформацію під час обговорення в JBS, але я впевнений, що знайдеться відповідь, що "ви повинні робити знімок лише тоді, коли веб-компонент стабільний". Тож, щоб передбачити цю відповідь, було б добре переконатися, чи працює цей підхід для вас. Або, якщо виявиться, що це викликає інші проблеми, було б добре подумати над цими проблемами і подивитися, чи / як їх можна виправити в самому OpenJFX ".

  1. За замовчуванням JavaFX 8 використовує типовий, 600якщо висота точно така 0. Код повторного використання WebViewслід використовувати setMinHeight(1), setPrefHeight(1)щоб уникнути цієї проблеми. Це не в коді нижче, але варто згадати для кожного, хто адаптував його до свого проекту.
  2. Щоб забезпечити готовність WebKit, зачекайте рівно два імпульси зсередини таймера анімації.
  3. Щоб запобігти помилкові помилки знімка, використовуйте зворотний виклик знімка, який також слухає імпульс.
// 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();
    }
});
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.