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 que WebView
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.