El JavaFX documentos de estado que una WebView
está listo cuando Worker.State.SUCCEEDED
se alcanza sin embargo, a menos que esperar un tiempo (es decir Animation
, Transition
, PauseTransition
, etc.), una página en blanco se procesa.
Esto sugiere que hay un evento que ocurre dentro de WebView y lo prepara para una captura, pero ¿qué es?
Hay más de 7,000 fragmentos de código en GitHub que usan,SwingFXUtils.fromFXImage
pero la mayoría de ellos parecen no estar relacionados WebView
, son interactivos (las máscaras humanas son la condición de la carrera) o usan Transiciones arbitrarias (de 100ms a 2,000ms).
He intentado:
Escuchando
changed(...)
desde dentro de lasWebView
dimensiones (DoubleProperty
implementos de propiedades de alto y anchoObservableValue
, que pueden monitorear estas cosas)- ViableNo viable. A veces, el valor parece cambiar por separado de la rutina de pintura, lo que lleva a un contenido parcial.
Contar ciegamente cualquier cosa y todo
runLater(...)
en el hilo de la aplicación FX.- 🚫 Muchas técnicas usan esto, pero mis propias pruebas unitarias (así como algunos excelentes comentarios de otros desarrolladores) explican que los eventos a menudo ya están en el hilo correcto, y esta llamada es redundante. Lo mejor que se me ocurre es que agrega un retraso suficiente a través de la cola para que funcione para algunos.
Agregar un escucha / disparador DOM o un escucha / disparador JavaScript al
WebView
- JavaScript Tanto JavaScript como DOM parecen estar cargados correctamente cuando
SUCCEEDED
se llama a pesar de la captura en blanco. Los oyentes DOM / JavaScript no parecen ayudar.
- JavaScript Tanto JavaScript como DOM parecen estar cargados correctamente cuando
Usando
Animation
oTransition
para "dormir" efectivamente sin bloquear el hilo principal de FX.- ⚠️ Este enfoque funciona y si el retraso es lo suficientemente largo, puede producir hasta el 100% de las pruebas unitarias, pero los tiempos de transición parecen ser un momento futuro que solo estamos adivinando y con un mal diseño. Para aplicaciones de rendimiento o de misión crítica, esto obliga al programador a hacer una compensación entre velocidad o confiabilidad, una experiencia potencialmente mala para el usuario.
Cuando es un buen momento para llamar WebView.snapshot(...)
?
Uso:
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.
*/
Fragmento de código:
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();
}
}
Relacionado:
- Captura de pantalla de la página web completa cargada en el componente JavaFX WebView, no solo parte visible
- ¿Puedo capturar instantáneas de la escena mediante programación?
- Captura de pantalla de toda la página, Java
- JavaFX 2.0+ WebView / WebEngine renderiza la página web a una imagen
- Establecer la altura y el ancho del escenario y la escena en javafx
- JavaFX: cómo cambiar el tamaño del escenario cuando se usa webview
- Dimensionamiento correcto de Webview incrustado en Tabelcell
- https://docs.oracle.com/javase/8/javafx/embedded-browser-tutorial/add-browser.htm#CEGDIBBI
- http://docs.oracle.com/javafx/2/swing/swing-fx-interoperability.htm#CHDIEEJE
- https://bugs.openjdk.java.net/browse/JDK-8126854
- https://bugs.openjdk.java.net/browse/JDK-8087569
Platform.runLater
fue probado y no lo soluciona. Por favor, pruébelo usted mismo si no está de acuerdo. Me alegraría estar equivocado, cerraría el problema.SUCCEEDED
estado (del cual el oyente dispara en el hilo FX) es la técnica adecuada. Si hay una manera de mostrar los eventos en cola, me encantaría intentarlo. He encontrado sugerencias dispersas a través de comentarios en los foros de Oracle y algunas preguntas SO queWebView
deben ejecutarse en su propio hilo por diseño, por lo que después de días de pruebas, estoy enfocando la energía allí. Si esa suposición es incorrecta, genial. Estoy abierto a cualquier sugerencia razonable que solucione el problema sin tiempos de espera arbitrarios.loadContent
método o al cargar una URL de archivo.Respuestas:
Parece que este es un error que ocurre cuando se usan los
loadContent
métodos de WebEngine . También ocurre cuando se usaload
para cargar un archivo local, pero en ese caso, llamar a reload () lo compensará.Además, dado que el escenario debe mostrarse cuando toma una instantánea, debe llamar
show()
antes de cargar el contenido. Dado que el contenido se carga de forma asíncrona, es completamente posible que se cargue antes de que finalice la declaración que sigue a la llamadaload
oloadContent
finaliza.La solución, entonces, es colocar el contenido en un archivo y llamar al
reload()
método de WebEngine exactamente una vez. La segunda vez que se carga el contenido, se puede tomar una instantánea con éxito de un oyente de la propiedad de estado del trabajador de carga.Normalmente, esto sería fácil:
Pero como lo está utilizando
static
para todo, deberá agregar algunos campos:Y puedes usarlos aquí:
Y luego deberá restablecerlo cada vez que cargue contenido:
Tenga en cuenta que hay mejores formas de realizar un procesamiento multiproceso. En lugar de usar clases atómicas, simplemente puede usar
volatile
campos:(los campos booleanos son falsos de forma predeterminada, y los campos de objeto son nulos de forma predeterminada. A diferencia de los programas en C, esta es una garantía sólida hecha por Java; no existe una memoria no inicializada).
En lugar de sondear en un bucle los cambios realizados en otro subproceso, es mejor usar sincronización, un bloqueo o una clase de nivel superior como CountDownLatch que usa esas cosas internamente:
reloaded
no se declara volátil porque solo se accede a él en el hilo de la aplicación JavaFX.fuente
volatile
variables. Desafortunadamente, llamarWebEngine.reload()
y esperar un posteriorSUCCEEDED
no funciona. Si coloco un contador en el contenido HTML, recibo: en0, 0, 1, 3, 3, 5
lugar de0, 1, 2, 3, 4, 5
, sugiriendo que en realidad no corrige la condición de carrera subyacente.CountDownLatch
". Votación positiva porque esta información no fue fácil de encontrar y ayuda a acelerar y simplificar el código con el inicio inicial de FX.Para acomodar el cambio de tamaño y el comportamiento subyacente de la instantánea, yo (hemos) ideado la siguiente solución de trabajo. Tenga en cuenta que estas pruebas se ejecutaron 2,000x (Windows, macOS y Linux) proporcionando tamaños aleatorios de WebView con un 100% de éxito.
Primero, citaré a uno de los desarrolladores de JavaFX. Esto se cita de un informe de error privado (patrocinado):
600
si la altura es exactamente0
. Código reutilizaciónWebView
debe utilizarsetMinHeight(1)
,setPrefHeight(1)
para evitar este problema. Esto no está en el código a continuación, pero vale la pena mencionarlo para cualquiera que lo adapte a su proyecto.fuente