L'état du JavaFX que WebView
est prêt quand Worker.State.SUCCEEDED
est atteint cependant, à moins que vous attendez un certain temps (c. -à Animation
, Transition
, PauseTransition
, etc.), une page vierge est rendue.
Cela suggère qu'il y a un événement qui se produit à l'intérieur du WebView le préparant pour une capture, mais qu'est-ce que c'est?
Il y a plus de 7000 extraits de code sur GitHub qui utilisentSwingFXUtils.fromFXImage
mais la plupart d'entre eux semblent être sans rapport avec WebView
, sont interactifs (l'homme masque la condition de concurrence) ou utilisent des transitions arbitraires (de 100 à 2000 ms).
J'ai essayé:
Écoute à
changed(...)
partirWebView
des dimensions du (DoubleProperty
implémentation des propriétés de hauteur et de largeurObservableValue
, qui peuvent surveiller ces choses)- 🚫Non viable. Parfois, la valeur semble changer indépendamment de la routine de peinture, conduisant à un contenu partiel.
Dire aveuglément tout et n'importe quoi
runLater(...)
sur le fil d'application FX.- Any De nombreuses techniques utilisent cela, mais mes propres tests unitaires (ainsi que d'excellents commentaires d'autres développeurs) expliquent que les événements sont souvent déjà sur le bon thread, et cet appel est redondant. Le mieux que je puisse penser est d'ajouter juste assez de retard dans la mise en file d'attente pour que cela fonctionne pour certains.
Ajouter un écouteur / déclencheur DOM ou un écouteur / déclencheur JavaScript au
WebView
- JavaScript Les deux JavaScript et le DOM semblent être chargés correctement lorsqu'ils
SUCCEEDED
sont appelés malgré la capture vierge. Les écouteurs DOM / JavaScript ne semblent pas aider.
- JavaScript Les deux JavaScript et le DOM semblent être chargés correctement lorsqu'ils
Utiliser un
Animation
ouTransition
pour "dormir" efficacement sans bloquer le thread FX principal.- ⚠️ Cette approche fonctionne et si le retard est suffisamment long, peut produire jusqu'à 100% des tests unitaires, mais les temps de transition semblent être un moment futur que nous devinons juste et une mauvaise conception. Pour les applications performantes ou critiques, cela oblige le programmeur à faire un compromis entre la vitesse ou la fiabilité, une expérience potentiellement mauvaise pour l'utilisateur.
Quel est le bon moment pour appeler WebView.snapshot(...)
?
Usage:
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.
*/
Extrait de code:
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();
}
}
En relation:
- Capture d'écran de la page Web complète chargée dans le composant JavaFX WebView, non seulement partie visible
- Puis-je capturer un instantané de scène par programme?
- Capture d'écran de la page entière, Java
- Page Web de rendu JavaFX 2.0+ WebView / WebEngine en image
- Définir la hauteur et la largeur de la scène et de la scène dans javafx
- JavaFX: comment redimensionner la scène lors de l'utilisation de WebView
- Dimensionnement correct de Webview intégré dans 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
a été testé et ne le résout pas. Veuillez l'essayer par vous-même si vous n'êtes pas d'accord. Je serais heureux de me tromper, cela résoudrait le problème.SUCCEEDED
état (dont l'auditeur se déclenche sur le thread FX) est la bonne technique. S'il y a un moyen de montrer les événements en file d'attente, je serais ravi d'essayer. J'ai trouvé des suggestions clairsemées à travers des commentaires sur les forums Oracle et quelques questions SO quiWebView
doivent fonctionner dans son propre fil de conception, donc après des jours de test, je concentre l'énergie là-bas. Si cette hypothèse est fausse, tant mieux. Je suis ouvert à toutes suggestions raisonnables pour résoudre le problème sans temps d'attente arbitraires.loadContent
méthode ou lors du chargement d'une URL de fichier.Réponses:
Il semble que ce soit un bug qui se produit lors de l'utilisation des
loadContent
méthodes de WebEngine . Cela se produit également lors de l'utilisationload
pour charger un fichier local, mais dans ce cas, l'appel à reload () le compensera.De plus, comme la scène doit s'afficher lorsque vous prenez un instantané, vous devez appeler
show()
avant de charger le contenu. Étant donné que le contenu est chargé de manière asynchrone, il est tout à fait possible qu'il soit chargé avant l'instruction suivant l'appel àload
ou seloadContent
termine.La solution de contournement consiste alors à placer le contenu dans un fichier et à appeler la
reload()
méthode WebEngine exactement une fois. La deuxième fois que le contenu est chargé, un instantané peut être pris avec succès à partir d'un écouteur de la propriété d'état du travailleur de chargement.Normalement, ce serait facile:
Mais parce que vous utilisez
static
pour tout, vous devrez ajouter quelques champs:Et vous pouvez les utiliser ici:
Et puis vous devrez le réinitialiser à chaque fois que vous chargez du contenu:
Notez qu'il existe de meilleures façons d'effectuer un traitement multithread. Au lieu d'utiliser des classes atomiques, vous pouvez simplement utiliser des
volatile
champs:(les champs booléens sont faux par défaut, et les champs objet sont nuls par défaut. Contrairement aux programmes C, c'est une garantie solide faite par Java; il n'y a pas de mémoire non initialisée.)
Au lieu d'interroger dans une boucle les modifications apportées dans un autre thread, il est préférable d'utiliser la synchronisation, un verrou ou une classe de niveau supérieur comme CountDownLatch qui utilise ces choses en interne:
reloaded
n'est pas déclaré volatile car il n'est accessible que dans le thread d'application JavaFX.la source
volatile
variables. Malheureusement, appelerWebEngine.reload()
et attendre une suiteSUCCEEDED
ne fonctionne pas. Si je place un compteur dans le contenu HTML, je reçois:0, 0, 1, 3, 3, 5
au lieu de0, 1, 2, 3, 4, 5
, suggérant qu'il ne résout pas réellement la condition de concurrence sous-jacente.CountDownLatch
". Vote ascendant car ces informations n'étaient pas faciles à trouver et aident à accélérer et à simplifier le code lors du démarrage initial de FX.Pour tenir compte du redimensionnement ainsi que du comportement de l'instantané sous-jacent, j'ai (nous avons) proposé la solution de travail suivante. Notez que ces tests ont été exécutés 2000x (Windows, macOS et Linux) fournissant des tailles WebView aléatoires avec un succès de 100%.
Tout d'abord, je vais citer l'un des développeurs JavaFX. Ceci est extrait d'un rapport de bogue privé (sponsorisé):
600
si la hauteur est exactement0
. Le recyclage du codeWebView
devrait utilisersetMinHeight(1)
,setPrefHeight(1)
pour éviter ce problème. Ce n'est pas dans le code ci-dessous, mais mérite d'être mentionné pour quiconque l'adapte à son projet.la source