Friday, December 29, 2017

How do I make JavaFX's webview render even when not visible due to being in another tab?

Leave a Comment

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.

3 Answers

Answers 1

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...

Answers 2

I would consider reloading page at the suitable moment using this command:

webView.getEngine().reload(); 

Also try to change parameter SnapshotParameters in method snapShot

If it would not work then I would consider storing Image in memory when WebView is being rendered on screen.

Answers 3

If you go by logical implementation of snapshot, only things that are visible on screen are taken as a snapshot. For taking snapshot of the web view, you can either make it automatically visible by clicking on the tab inside which the view is rendered just before taking the snapshot. Or you can manually click on the tab and take the screenshot. I think no API allows to take snapshot of hidden part as logically it will voilate the concept of hidden things. Code for taking snapshot is already available with you. You can click, or Load the tab like:

You can add a selectionChangedListener or you can do the load just before the snapshot.

addItemTab.setOnSelectionChanged(event -> loadTabBasedFXML(addItemTab, "/view/AddItem.fxml"));  private void loadTabBasedFXML(Tab tab, String fxmlPath) {         try {             AnchorPane anchorPane = FXMLLoader.load(this.getClass().getResource(fxmlPath));             tab.setContent(anchorPane);         } catch (IOException e) {         }     } 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment