WebView 什么时候准备好快照()?
When is a WebView ready for a snapshot()?
JavaFX 文档指出 WebView
is ready when Worker.State.SUCCEEDED
is reached 但是,除非您稍等片刻(即 Animation
、Transition
、PauseTransition
等),否则会出现空白页被渲染。
这表明 WebView 内部发生了一个事件,准备好进行捕获,但它是什么?
有 over 7,000 code snippets on GitHub which use SwingFXUtils.fromFXImage
,但其中大多数似乎与 WebView
无关,是交互式的(人类掩盖了竞争条件)或使用任意过渡(从 100 毫秒到 2,000 毫秒不等)。
我试过:
从 WebView
的维度中监听 changed(...)
(高度和宽度属性 DoubleProperty
实现 ObservableValue
,它可以监视这些东西)
- 不可行。有时,该值似乎与绘制例程分开更改,导致部分内容。
在 FX 应用程序线程上盲目地告诉 runLater(...)
任何事情。
- 许多技术都使用这个,但我自己的单元测试(以及其他开发人员的一些很好的反馈)解释说事件通常已经在正确的线程上,这个调用是多余的。我能想到的最好的方法是通过排队增加对某些人有效的延迟。
在WebView
中添加DOMlistener/trigger或JavaScriptlistener/trigger
- 当
SUCCEEDED
被调用时,JavaScript 和 DOM 似乎都被正确加载,尽管是空白捕获。 DOM/JavaScript 听众似乎没有帮助。
使用 Animation
或 Transition
有效地 "sleep" 而不会阻塞主 FX 线程。
- ⚠️ 这种方法有效,如果延迟足够长,可以产生高达 100% 的单元测试,但转换时间似乎是 some future moment that we're just guessing 和糟糕的设计。对于高性能或关键任务应用程序,这迫使程序员在速度或可靠性之间做出权衡,这对用户来说都是一种潜在的糟糕体验。
什么时候打电话比较好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();
}
}
相关:
- Screenshot of the full web page loaded into JavaFX WebView component, not only visible part
- Whole page screenshot, Java
- JavaFX 2.0+ WebView /WebEngine render web page to an image
- Set Height and Width of Stage and Scene in javafx
- JavaFX:how to resize the stage when using webview
- 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
这似乎是使用 WebEngine 的 loadContent
方法时出现的错误。当使用 load
加载本地文件时也会发生这种情况,但在这种情况下,调用 reload() 将对其进行补偿。
另外,由于快照时需要显示舞台,因此需要在加载内容之前调用show()
。由于内容是异步加载的,因此完全有可能在调用 load
或 loadContent
之后的语句完成之前加载内容。
然后,解决方法是将内容放在一个文件中,并只调用 WebEngine 的 reload()
方法一次。第二次加载内容时,可以从load worker状态的监听器中成功抓拍快照属性.
通常情况下,这很容易:
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
字段,而不是使用原子 classes:
private static volatile boolean started;
private static volatile boolean finished = true;
private static volatile Throwable thrown;
private static volatile BufferedImage capture;
(boolean字段默认为false,object字段默认为null。与C程序不同,这是Java做出的硬性保证;不存在未初始化的内存。)
与其在循环中轮询另一个线程中所做的更改,不如使用同步、锁或更高级别 class,如 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
未声明为 volatile,因为它仅在 JavaFX 应用程序线程中访问。
为了适应调整大小以及底层快照行为,我(我们)提出了以下工作解决方案。请注意,这些测试是 运行 2,000x(Windows、macOS 和 Linux)提供随机 WebView 大小且 100% 成功。
首先,我将引用一位 JavaFX 开发人员的话。这是从私人(赞助)错误报告中引用的:
"I assume you initiate the resizing on the FX AppThread, and that it is done after the SUCCEEDED state is reached. In that case, it seems to me that at that moment, waiting 2 pulses (without blocking the FX AppThread) should give the webkit implementation enough time to make its changes, unless this results in some dimensions being changed in JavaFX, which may result again in dimensions being changed inside webkit.
I'm thinking on how to feed this info into the discussion in JBS, but I'm pretty sure there will be the answer that "you should take a snapshot only when the webcomponent is stable". So in order to anticipate this answer, it would be good to see if this approach work for you. Or, if it turns out to cause other problems, it would be good to think about these problems, and see if/how they can be fixed in OpenJFX itself."
- 默认情况下,如果高度恰好为
0
,则 JavaFX 8 使用默认值 600
。
重用WebView
的代码应该使用setMinHeight(1)
、setPrefHeight(1)
来避免这个问题。这不在下面的代码中,但值得一提的是任何将其改编为他们的项目的人。
- 为了适应 WebKit 的就绪状态,请等待来自动画计时器内部的恰好两个脉冲。
- 要防止快照空白错误,请利用快照回调,它也会侦听脉冲。
// 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();
}
});
JavaFX 文档指出 WebView
is ready when Worker.State.SUCCEEDED
is reached 但是,除非您稍等片刻(即 Animation
、Transition
、PauseTransition
等),否则会出现空白页被渲染。
这表明 WebView 内部发生了一个事件,准备好进行捕获,但它是什么?
有 over 7,000 code snippets on GitHub which use SwingFXUtils.fromFXImage
,但其中大多数似乎与 WebView
无关,是交互式的(人类掩盖了竞争条件)或使用任意过渡(从 100 毫秒到 2,000 毫秒不等)。
我试过:
从
WebView
的维度中监听changed(...)
(高度和宽度属性DoubleProperty
实现ObservableValue
,它可以监视这些东西)- 不可行。有时,该值似乎与绘制例程分开更改,导致部分内容。
在 FX 应用程序线程上盲目地告诉
runLater(...)
任何事情。- 许多技术都使用这个,但我自己的单元测试(以及其他开发人员的一些很好的反馈)解释说事件通常已经在正确的线程上,这个调用是多余的。我能想到的最好的方法是通过排队增加对某些人有效的延迟。
在
中添加DOMlistener/trigger或JavaScriptlistener/triggerWebView
- 当
SUCCEEDED
被调用时,JavaScript 和 DOM 似乎都被正确加载,尽管是空白捕获。 DOM/JavaScript 听众似乎没有帮助。
- 当
使用
Animation
或Transition
有效地 "sleep" 而不会阻塞主 FX 线程。- ⚠️ 这种方法有效,如果延迟足够长,可以产生高达 100% 的单元测试,但转换时间似乎是 some future moment that we're just guessing 和糟糕的设计。对于高性能或关键任务应用程序,这迫使程序员在速度或可靠性之间做出权衡,这对用户来说都是一种潜在的糟糕体验。
什么时候打电话比较好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();
}
}
相关:
- Screenshot of the full web page loaded into JavaFX WebView component, not only visible part
- Whole page screenshot, Java
- JavaFX 2.0+ WebView /WebEngine render web page to an image
- Set Height and Width of Stage and Scene in javafx
- JavaFX:how to resize the stage when using webview
- 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
这似乎是使用 WebEngine 的 loadContent
方法时出现的错误。当使用 load
加载本地文件时也会发生这种情况,但在这种情况下,调用 reload() 将对其进行补偿。
另外,由于快照时需要显示舞台,因此需要在加载内容之前调用show()
。由于内容是异步加载的,因此完全有可能在调用 load
或 loadContent
之后的语句完成之前加载内容。
然后,解决方法是将内容放在一个文件中,并只调用 WebEngine 的 reload()
方法一次。第二次加载内容时,可以从load worker状态的监听器中成功抓拍快照属性.
通常情况下,这很容易:
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
字段,而不是使用原子 classes:
private static volatile boolean started;
private static volatile boolean finished = true;
private static volatile Throwable thrown;
private static volatile BufferedImage capture;
(boolean字段默认为false,object字段默认为null。与C程序不同,这是Java做出的硬性保证;不存在未初始化的内存。)
与其在循环中轮询另一个线程中所做的更改,不如使用同步、锁或更高级别 class,如 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
未声明为 volatile,因为它仅在 JavaFX 应用程序线程中访问。
为了适应调整大小以及底层快照行为,我(我们)提出了以下工作解决方案。请注意,这些测试是 运行 2,000x(Windows、macOS 和 Linux)提供随机 WebView 大小且 100% 成功。
首先,我将引用一位 JavaFX 开发人员的话。这是从私人(赞助)错误报告中引用的:
"I assume you initiate the resizing on the FX AppThread, and that it is done after the SUCCEEDED state is reached. In that case, it seems to me that at that moment, waiting 2 pulses (without blocking the FX AppThread) should give the webkit implementation enough time to make its changes, unless this results in some dimensions being changed in JavaFX, which may result again in dimensions being changed inside webkit.
I'm thinking on how to feed this info into the discussion in JBS, but I'm pretty sure there will be the answer that "you should take a snapshot only when the webcomponent is stable". So in order to anticipate this answer, it would be good to see if this approach work for you. Or, if it turns out to cause other problems, it would be good to think about these problems, and see if/how they can be fixed in OpenJFX itself."
- 默认情况下,如果高度恰好为
0
,则 JavaFX 8 使用默认值600
。 重用WebView
的代码应该使用setMinHeight(1)
、setPrefHeight(1)
来避免这个问题。这不在下面的代码中,但值得一提的是任何将其改编为他们的项目的人。 - 为了适应 WebKit 的就绪状态,请等待来自动画计时器内部的恰好两个脉冲。
- 要防止快照空白错误,请利用快照回调,它也会侦听脉冲。
// 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();
}
});