I'm loading a website on a JavaFX WebView and after a while taking a screenshot with something like:
WritableImage image = webView.snapshot(null, null);
If I'm looking at that WebView that works fine, but if it's hidden by being in a tab that is not in the foreground (I'm not sure about other cases of hiding it), then, the screenshot is of the appropriate site, but entirely blank.
How can I force the WebView to render even if not visible?
During this time, webView.isVisible()
is true.
I found there's a method in WebView
called isTreeReallyVisible()
and currently it contains:
private boolean isTreeReallyVisible() {
if (getScene() == null) {
return false;
}
final Window window = getScene().getWindow();
if (window == null) {
return false;
}
boolean iconified = (window instanceof Stage) ? ((Stage)window).isIconified() : false;
return impl_isTreeVisible()
&& window.isShowing()
&& window.getWidth() > 0
&& window.getHeight() > 0
&& !iconified;
}
When the WebView is hidden by being in a non-foreground tab, impl_isTreeVisible()
is false (all other factors in the return statement are true). That method is on Node
and looks like this:
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@Deprecated
public final boolean impl_isTreeVisible() {
return impl_treeVisibleProperty().get();
}
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@Deprecated
protected final BooleanExpression impl_treeVisibleProperty() {
if (treeVisibleRO == null) {
treeVisibleRO = new TreeVisiblePropertyReadOnly();
}
return treeVisibleRO;
}
I could have overriden impl_treeVisibleProperty()
to provide my own implementation, but WebView
is final, so, I cannot inherit from it.
Another completely different situation to being minimized (iconified) or on a hidden tab is to have the stage completely hidden (as in, running in the tray bar). When in that mode, even if I can get rendering to happen, the WebView
doesn't resize. I call webView.resize()
and then take a screenshot and the screenshot is of the appropriate size but the actual rendered page is of whatever size the WebView
was before.
Debugging this sizing behavior in shown and hidden stages, I found that eventually we get to Node.addToSceneDirtyList()
that contains:
private void addToSceneDirtyList() {
Scene s = getScene();
if (s != null) {
s.addToDirtyList(this);
if (getSubScene() != null) {
getSubScene().setDirty(this);
}
}
}
When in hidden mode, getScene()
returns null
, unlike what happens when it's being show. That means that s.addToDirtyList(this)
is never called. I'm not sure if this is the reason why it doesn't get properly resized.
There's a bug about this, a very old one, here: https://bugs.openjdk.java.net/browse/JDK-8087569 but I don't think that's the whole issue.
I'm doing this with Java 1.8.0_151. I tried 9.0.1 to see if it would behave differently as it is my understanding that WebKit was upgraded, but no, it's the same.
You can access the DOM of the web page displayed inside a JavaFX WebView by calling the WebEngine getDocument() method. Here is an example of accessing the DOM of a WebView : Document document = webEngine. getDocument();
WebView is a Node that manages a WebEngine and displays its content. The associated WebEngine is created automatically at construction time and cannot be changed afterwards. WebView handles mouse and some keyboard events, and manages scrolling automatically, so there's no need to put it into a ScrollPane .
It supports Cascading Style Sheets (CSS), JavaScript, Document Object Model (DOM), and HTML5. The embedded browser enables you to perform the following tasks in your JavaFX applications: Render HTML content from local and remote URLs.
Reproducing Pablo's problem here: https://github.com/johanwitters/stackoverflow-javafx-webview.
Pablo suggested to override WebView and adjust some methods. That doesn't work given it's a final class and a private member. As an alternative, I've used javassist to rename a method and replace the code with the code that I want it to execute. I've "replaced" the contents of method handleStagePulse, as shown below.
public class WebViewChanges {
// public static String MY_WEBVIEW_CLASSNAME = WebView.class.getName();
public WebView newWebView() {
createSubclass();
return new WebView();
}
// https://www.ibm.com/developerworks/library/j-dyn0916/index.html
boolean created = false;
private void createSubclass() {
if (created) return;
created = true;
try
{
String methodName = "handleStagePulse";
// get the super class
CtClass webViewClass = ClassPool.getDefault().get("javafx.scene.web.WebView");
// get the method you want to override
CtMethod handleStagePulseMethod = webViewClass.getDeclaredMethod(methodName);
// Rename the previous handleStagePulse method
String newName = methodName+"Old";
handleStagePulseMethod.setName(newName);
// mnew.setBody(body.toString());
CtMethod newMethod = CtNewMethod.copy(handleStagePulseMethod, methodName, webViewClass, null);
String body = "{" +
" " + Scene.class.getName() + ".impl_setAllowPGAccess(true);\n" +
" " + "final " + NGWebView.class.getName() + " peer = impl_getPeer();\n" +
" " + "peer.update(); // creates new render queues\n" +
// " " + "if (page.isRepaintPending()) {\n" +
" " + " impl_markDirty(" + DirtyBits.class.getName() + ".WEBVIEW_VIEW);\n" +
// " " + "}\n" +
" " + Scene.class.getName() + ".impl_setAllowPGAccess(false);\n" +
"}\n";
System.out.println(body);
newMethod.setBody(body);
webViewClass.addMethod(newMethod);
CtMethod isTreeReallyVisibleMethod = webViewClass.getDeclaredMethod("isTreeReallyVisible");
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
This snippet is called from the WebViewSample which opens 2 tabs. One with a "snapshot" button, another with the WebView. As Pablo pointed out, the tab with the WebView needs to be the second tab to be able to reproduce.
package com.johanw.stackoverflow;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javax.imageio.ImageIO;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
public class WebViewSample extends Application {
private Scene scene;
private TheBrowser theBrowser;
private void setLabel(Label label) {
label.setText("" + theBrowser.browser.isVisible());
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Tabs");
Group root = new Group();
Scene scene = new Scene(root, 400, 250, Color.WHITE);
TabPane tabPane = new TabPane();
BorderPane borderPane = new BorderPane();
theBrowser = new TheBrowser();
{
Tab tab = new Tab();
tab.setText("Other tab");
HBox hbox0 = new HBox();
{
Button button = new Button("Screenshot");
button.addEventHandler(MouseEvent.MOUSE_PRESSED,
new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent e) {
WritableImage image = theBrowser.getBrowser().snapshot(null, null);
File file = new File("test.png");
RenderedImage renderedImage = SwingFXUtils.fromFXImage(image, null);
try {
ImageIO.write(
renderedImage,
"png",
file);
} catch (IOException e1) {
e1.printStackTrace();
}
}
});
hbox0.getChildren().add(button);
hbox0.setAlignment(Pos.CENTER);
}
HBox hbox1 = new HBox();
Label visibleLabel = new Label("");
{
hbox1.getChildren().add(new Label("webView.isVisible() = "));
hbox1.getChildren().add(visibleLabel);
hbox1.setAlignment(Pos.CENTER);
setLabel(visibleLabel);
}
HBox hbox2 = new HBox();
{
Button button = new Button("Refresh");
button.addEventHandler(MouseEvent.MOUSE_PRESSED,
new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent e) {
setLabel(visibleLabel);
}
});
hbox2.getChildren().add(button);
hbox2.setAlignment(Pos.CENTER);
}
VBox vbox = new VBox();
vbox.getChildren().addAll(hbox0);
vbox.getChildren().addAll(hbox1);
vbox.getChildren().addAll(hbox2);
tab.setContent(vbox);
tabPane.getTabs().add(tab);
}
{
Tab tab = new Tab();
tab.setText("Browser tab");
HBox hbox = new HBox();
hbox.getChildren().add(theBrowser);
hbox.setAlignment(Pos.CENTER);
tab.setContent(hbox);
tabPane.getTabs().add(tab);
}
// bind to take available space
borderPane.prefHeightProperty().bind(scene.heightProperty());
borderPane.prefWidthProperty().bind(scene.widthProperty());
borderPane.setCenter(tabPane);
root.getChildren().add(borderPane);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args){
launch(args);
}
}
class TheBrowser extends Region {
final WebView browser;
final WebEngine webEngine;
public TheBrowser() {
browser = new WebViewChanges().newWebView();
webEngine = browser.getEngine();
getStyleClass().add("browser");
webEngine.load("http://www.google.com");
getChildren().add(browser);
}
private Node createSpacer() {
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
return spacer;
}
@Override protected void layoutChildren() {
double w = getWidth();
double h = getHeight();
layoutInArea(browser,0,0,w,h,0, HPos.CENTER, VPos.CENTER);
}
@Override protected double computePrefWidth(double height) {
return 750;
}
@Override protected double computePrefHeight(double width) {
return 500;
}
public WebView getBrowser() {
return browser;
}
public WebEngine getWebEngine() {
return webEngine;
}
}
I've not succeeded in fixing Pablo's problem, but hopefully the suggestion to use javassist might help.
I'm sure: To be continued...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With