但是,JavaFX文档声明,除非您等待一段时间(即Animation、Transition、PauseTransition等),否则将呈现一个空白页。
这表明在WebView中有一个事件正在为捕获做准备,但它是什么呢?
虽然有SwingFXUtils.fromFXImage,但它们中的大多数似乎要么与WebView无关,要么是交互式的(人类掩盖了种族状况),要么使用任意的过渡(从100‘s到2000’s)。
我试过:
changed(...)的维度监听WebView(高度和宽度属性DoubleProperty实现ObservableValue,它可以监视这些事情)- Not viable. Sometimes, the value seems to change separate from the paint routine, leading to partial content.
runLater(...)任何事情和一切。- Many techniques use this, but my own unit tests (as well as some great feedback from other developers) explain that events are often already on the right thread, and this call is redundant. The best I can think of is adds just enough of a delay through queuing that it works for some.
WebView- Both JavaScript and the DOM seem to be loaded properly when `SUCCEEDED` is called despite the blank capture. DOM/JavaScript listeners don't seem to help.
Animation或Transition有效地“睡眠”而不阻塞主FX线程。- ⚠️ This approach works and if the delay is long enough, can yield up to 100% of unit tests, but the Transition times seem to be [some future moment that we're just guessing](https://stackoverflow.com/a/30723223/3196753) and bad design. For performant or mission-critical applications, this forces the programmer to make a tradeoff between speed or reliability, both a potentially bad experience to the user.
何时是调用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();
}
}相关信息:
发布于 2020-03-18 20:23:50
为了适应调整大小以及底层快照行为,我(我们)提出了以下工作解决方案。注意,这些测试运行了2000 x (Windows、macOS和Linux),提供了随机的WebView大小,成功率为100%。
首先,我将引用一个JavaFX开发人员。这是引用自一份私人(赞助的) bug报告:
“我假设您在FX AppThread上启动调整大小,并且是在达到成功状态之后进行的。在这种情况下,在我看来,等待2个脉冲(不阻塞FX AppThread)应该会给webkit实现足够的时间来进行更改,除非这会导致JavaFX中的某些维度发生变化,这可能导致webkit中的维度再次发生变化。 我正在考虑如何将这些信息输入到JBS中的讨论中,但我非常肯定会有这样的答案:“只有当webcomponent稳定时,您才应该快照”。因此,为了预测这个答案,最好看看这个方法是否适合你。或者,如果结果会导致其他问题,最好考虑一下这些问题,看看是否/如何在OpenJFX本身中修复这些问题。“
600,如果高度恰好是0。重用WebView的代码应该使用setMinHeight(1),setPrefHeight(1)来避免这个问题。这并不在下面的代码中,但是对于任何修改它以适应他们的项目的人来说,都是值得一提的。// 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();
}
});发布于 2020-01-19 17:37:26
这似乎是使用WebEngine的loadContent方法时发生的错误。在使用load加载本地文件时也会发生这种情况,但在这种情况下,调用重装()将对其进行补偿。
另外,由于需要在拍摄快照时显示阶段,所以需要在加载内容之前调用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字段:
private static volatile boolean started;
private static volatile boolean finished = true;
private static volatile Throwable thrown;
private static volatile BufferedImage capture;(默认情况下,布尔字段为false,而对象字段默认为空。与C程序不同的是,这是Java的硬保证;没有未初始化的内存。)
与其在循环中轮询在另一个线程中所做的更改,不如使用同步、锁或像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不被声明为易失性,因为它仅在JavaFX应用程序线程中被访问。
https://stackoverflow.com/questions/59803411
复制相似问题