O JavaFX docs estado que um WebView
está pronto quando Worker.State.SUCCEEDED
é atingido no entanto, a menos que você esperar um pouco (ou seja Animation
, Transition
, PauseTransition
, etc.), uma página em branco é processado.
Isso sugere que há um evento que ocorre dentro do WebView, preparando-o para uma captura, mas o que é?
Existem mais de 7.000 trechos de código no GitHub que usam,SwingFXUtils.fromFXImage
mas a maioria deles parece não estar relacionada WebView
, são interativos (humanos mascara a condição de corrida) ou usam transições arbitrárias (de 100 a 2.000ms).
Eu tentei:
Ouvindo
changed(...)
de dentro dasWebView
dimensões do (DoubleProperty
implementações de propriedades de altura e larguraObservableValue
, que podem monitorar essas coisas)- Não é viável. Às vezes, o valor parece mudar separadamente da rotina de pintura, levando ao conteúdo parcial.
Dizendo cegamente tudo e qualquer coisa
runLater(...)
no Thread do Aplicativo FX.- TechniquesMuitas técnicas usam isso, mas meus próprios testes de unidade (bem como ótimos comentários de outros desenvolvedores) explicam que os eventos já estão no thread certo e essa chamada é redundante. O melhor que consigo pensar é adicionar um atraso suficiente nas filas para que funcione para alguns.
Incluindo um ouvinte / gatilho DOM ou um ouvinte / gatilho JavaScript no
WebView
- JavaScript Ambos JavaScript e DOM parecem estar carregados corretamente quando
SUCCEEDED
são chamados, apesar da captura em branco. Ouvintes DOM / JavaScript não parecem ajudar.
- JavaScript Ambos JavaScript e DOM parecem estar carregados corretamente quando
Usando um
Animation
ouTransition
para "dormir" efetivamente sem bloquear o thread FX principal.- ⚠️ Essa abordagem funciona e, se o atraso for longo o suficiente, pode render até 100% dos testes de unidade, mas os tempos de transição parecem ser um momento futuro que estamos apenas adivinhando e com um design ruim. Para aplicativos de desempenho ou de missão crítica, isso força o programador a fazer uma troca entre velocidade ou confiabilidade, uma experiência potencialmente ruim para o usuário.
Quando é um bom momento para ligar 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 tela da página da web completa carregada no componente JavaFX WebView, não apenas da parte visível
- Posso capturar instantâneos de cena programaticamente?
- Captura de tela de página inteira, Java
- JavaFX 2.0+ WebView / WebEngine renderiza página da web em uma imagem
- Definir altura e largura do palco e da cena em javafx
- JavaFX: como redimensionar o estágio ao usar o webview
- Dimensionamento correto do Webview incorporado no 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
foi testado e não o corrige. Por favor, tente você mesmo se você não concorda. Eu ficaria feliz em estar errado, isso resolveria o problema.SUCCEEDED
estado (do qual o ouvinte dispara no encadeamento FX) é a técnica adequada. Se houver uma maneira de mostrar eventos na fila, eu ficaria feliz em tentar. Encontrei sugestões esparsas através de comentários nos fóruns da Oracle e algumas perguntas de SO queWebView
devem ser executadas em seu próprio encadeamento, por isso, depois de dias de teste, concentro a energia lá. Se essa suposição estiver errada, ótimo. Estou aberto a sugestões razoáveis que resolvam o problema sem tempos de espera arbitrários.loadContent
método ou ao carregar um URL de arquivo.Respostas:
Parece que este é um erro que ocorre ao usar os
loadContent
métodos do WebEngine . Também ocorre ao usarload
para carregar um arquivo local, mas, nesse caso, chamar reload () compensará isso.Além disso, como o Palco precisa ser exibido quando você tira uma captura instantânea, é necessário ligar
show()
antes de carregar o conteúdo. Como o conteúdo é carregado de forma assíncrona, é perfeitamente possível que ele seja carregado antes da instrução após a chamadaload
ouloadContent
terminar.A solução alternativa, então, é colocar o conteúdo em um arquivo e chamar o
reload()
método do WebEngine exatamente uma vez. Na segunda vez que o conteúdo é carregado, uma captura instantânea pode ser obtida com êxito de um ouvinte da propriedade de estado do trabalhador de carregamento.Normalmente, isso seria fácil:
Mas, como você está usando
static
tudo, precisará adicionar alguns campos:E você pode usá-los aqui:
E você precisará redefini-lo sempre que carregar o conteúdo:
Observe que existem maneiras melhores de executar o processamento multithread. Em vez de usar classes atômicas, você pode simplesmente usar os
volatile
campos:(os campos booleanos são falsos por padrão e os campos de objetos são nulos por padrão. Ao contrário dos programas C, essa é uma garantia garantida pelo Java; não existe memória não inicializada.)
Em vez de pesquisar em um loop as alterações feitas em outro encadeamento, é melhor usar a sincronização, um bloqueio ou uma classe de nível superior como CountDownLatch, que usa essas coisas internamente:
reloaded
não é declarado volátil porque é acessado apenas no encadeamento de aplicativo JavaFX.fonte
volatile
variáveis. Infelizmente, ligarWebEngine.reload()
e aguardar um subsequenteSUCCEEDED
não funciona. Se eu colocar um contador no conteúdo HTML, recebo: em0, 0, 1, 3, 3, 5
vez de0, 1, 2, 3, 4, 5
sugerir que ele realmente não corrige a condição de corrida subjacente.CountDownLatch
". Voto positivo porque essa informação não foi fácil de encontrar e ajuda a acelerar e a simplicidade do código com a inicialização inicial do FX.Para acomodar o redimensionamento, bem como o comportamento subjacente do instantâneo, eu (nós) apresentamos a seguinte solução de trabalho. Observe que esses testes foram executados 2.000x (Windows, macOS e Linux), fornecendo tamanhos aleatórios do WebView com 100% de sucesso.
Primeiro, citarei um dos desenvolvedores JavaFX. Isso é citado em um relatório de bug privado (patrocinado):
600
se height for exatamente0
. Código reutilizaçãoWebView
deve usarsetMinHeight(1)
,setPrefHeight(1)
para evitar este problema. Isso não está no código abaixo, mas vale a pena mencionar para qualquer pessoa que o adaptar ao seu projeto.fonte