JavaFX - 如何创建(不可见)WebView 的快照/屏幕截图

Posted

技术标签:

【中文标题】JavaFX - 如何创建(不可见)WebView 的快照/屏幕截图【英文标题】:JavaFX - How to create SnapShot/Screenshot of (invisble) WebView 【发布时间】:2015-07-19 05:09:08 【问题描述】:

我想从 JavaFX(8) 中的 WebView 创建快照/屏幕截图/图像。此 WebView 不需要可见(在我的情况下)。我的问题: 当 WebView不可见(或未添加到任何可见容器)时,是否可以(以任何方式)从 WebView 创建屏幕截图/图像?

查看我的示例代码,当 WebView(或其父 ScrollPane)为 visible=false, 屏幕截图不起作用(分别为空/空白)。

示例代码:

package test;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.SnapshotResult;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javafx.util.Duration;

public class JavaFXApplication extends Application

   @Override
   public void start(Stage primaryStage)
   
      ImageView webviewPreviewImage = new ImageView();
      Label waitLabel = new Label("Please wait...");
      WebView webView = new WebView();
      webView.setMaxHeight(480d);
      webView.setMinHeight(480d);
      webView.setMaxWidth(640d);
      webView.setMinWidth(640d);
      webView.setZoom(0.4);

      ScrollPane scrollpane = new ScrollPane(webView);
      scrollpane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
      scrollpane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
      scrollpane.setMaxWidth(0); //WORKAROUND: hide the WebView/ScrollPane
      scrollpane.setMaxHeight(0); //WORKAROUND: hide the WebView/ScrollPane
      scrollpane.setMinWidth(0); //WORKAROUND: hide the WebView/ScrollPane
      scrollpane.setMinHeight(0); //WORKAROUND: hide the WebView/ScrollPane

      //scrollpane.setVisible(false); //when WebView is invisible,  SnapShot doesn't work!

       webView.getEngine().getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>() 
       
          @Override
          public void changed(ObservableValue ov, Worker.State oldState, Worker.State newState) 
          
              if (newState == Worker.State.SUCCEEDED) 
              
                 //When SUCCEEDED is called, the WebPage may not has fully finished rendering!
                 //so, wait for few seceonds before making the screenshot...
                 Timeline timeline = new Timeline(new KeyFrame(
                         Duration.millis(1500),
                         ae -> takeSnapshot()));
                 timeline.play();
              
           

           private KeyFrame takeSnapshot()
           
              webView.snapshot((SnapshotResult param) ->
              
                 webviewPreviewImage.setImage(param.getImage());
                 webviewPreviewImage.setFitHeight(240d);
                 webviewPreviewImage.setFitWidth(320d);
                 webviewPreviewImage.setPreserveRatio(true);
                 waitLabel.setVisible(false);
                 return null;
              , null, null);
              return null;
          
      );
      webView.getEngine().load("http://www.bing.com");

      VBox root = new VBox();
      root.setAlignment(Pos.CENTER);
      root.setSpacing(10d);
      root.getChildren().add(waitLabel);
      root.getChildren().add(scrollpane);
      root.getChildren().add(webviewPreviewImage);

      Scene scene = new Scene(root, 800, 600);
      primaryStage.setScene(scene);
      primaryStage.show();
  

  public static void main(String[] args)
  
     launch(args);
  

【问题讨论】:

您要在不显示应用程序或场景的情况下拍摄快照吗?? 我的应用程序正在显示(并且因此有一个场景)。但是这个 WebView 不需要是它的一部分。 这是一些未解决的请求功能,例如 Jira 上的 issue。我已经测试了 8u60b13(新的 webkit 实现),但没有成功。我认为你有一个很好的解决方法。 【参考方案1】:

WebView 类包含以下功能:

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;

只要函数返回false,渲染就会被阻塞。所以让它渲染可能非常棘手。通常,对于快照,您根本不必将节点放在场景中。

【讨论】:

虽然这不是一个解决方案,但很高兴看到一个解决方案解释了为什么解决方案可能(很容易)不可能。谢谢!【参考方案2】:

经过大量搜索和拼凑几块后,我发现了一个示例 SO-poster 在 oracle 论坛上发布,它的问题是 webview 的大小是固定的,并且我的 css 在 html 中使用需要 (不在 JavaFX 中):

overflow-x: hidden;
overflow-y: hidden;

隐藏最后一个滚动条。

所以我想出了以下快照方法(带有动画的应用程序作为您的应用程序的示例):

package application;

import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

import javafx.animation.Animation;
import javafx.animation.PauseTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.effect.GaussianBlur;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.web.WebView;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;

public class WebViewSnapshot extends Application 

    BorderPane rootPane;
    TranslateTransition animation;

    @Override
    public void start(Stage primaryStage) 

        Rectangle rect = new Rectangle(50, 50, 50, 50);
        rect.setFill(Color.CORAL);

        animation = createAnimation(rect);

        Button snapshotButton = new Button("Take snapshot");

        Pane pane = new Pane(rect);
        pane.setMinSize(600, 150);

        rootPane = new BorderPane(pane, null, null, snapshotButton, new Label("This is the main scene"));

        snapshotButton.setOnAction(e -> 
            // html file being shown in webview
            File htmlFile = new File ("generated/template.html");
            // the resulting snapshot png file
            File aboutFile = new File ("generated/about.png");
            generate(htmlFile, aboutFile, 1280, 720);
        );

        BorderPane.setAlignment(snapshotButton, Pos.CENTER);
        BorderPane.setMargin(snapshotButton, new Insets(5));
        Scene scene = new Scene(rootPane);
        primaryStage.setScene(scene);
        primaryStage.show();
    

    private TranslateTransition createAnimation(Rectangle rect) 
        TranslateTransition animation = new TranslateTransition(Duration.seconds(1), rect);
        animation.setByX(400);
        animation.setCycleCount(Animation.INDEFINITE);
        animation.setAutoReverse(true);
        animation.play();
        return animation;
    

    public void generate(File htmlFile, File outputFile, double width, double height) 
        animation.pause();
        // rootPane is the root of original scene in an FXML controller you get this when you assign it an id
        rootPane.setEffect(new GaussianBlur());
        Stage primaryStage = (Stage)rootPane.getScene().getWindow();
        // creating separate webview holding same html content as in original scene
        WebView webView = new WebView();
        // with the size I want the snapshot
        webView.setPrefSize(width, height);
        AnchorPane snapshotRoot = new AnchorPane(webView);
        webView.getEngine().load(htmlFile.toURI().toString());
        Stage popupStage = new Stage(StageStyle.TRANSPARENT);
        popupStage.initOwner(primaryStage);
        popupStage.initModality(Modality.APPLICATION_MODAL);
        // this popup doesn't really show anything size = 1x1, it just holds the snapshot-webview
        popupStage.setScene(new Scene(snapshotRoot, 1, 1));
        // pausing to make sure the webview/picture is completely rendered
        PauseTransition pt = new PauseTransition(Duration.seconds(2));
        pt.setOnFinished(new EventHandler<ActionEvent>() 
            @Override public void handle(ActionEvent event) 
                WritableImage image = webView.snapshot(null, null);
                // writing a png to outputFile
                // writing a JPG like this will result in a pink JPG, see other posts
                // if somebody can scrape me simple code to convert it ARGB to RGB or something
                String format = "png";
                try 
                    ImageIO.write(SwingFXUtils.fromFXImage(image, null), format, outputFile);
                 catch (IOException e) 
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                 finally 
                    rootPane.setEffect(null);
                    popupStage.hide();
                    animation.play();
                
            
        );
        // pausing, after pause onFinished event will take + write snapshot
        pt.play();
        // GO!
        popupStage.show();
    

    public static void main(String[] args) 
        launch(args);
    

【讨论】:

【参考方案3】:

这里回答了为什么不可见的 WebView 根本不呈现:https://***.com/a/52977547/1534698,绕过该问题的代码在这里https://***.com/a/58047583/1534698。

但是这里提供了一个干净的解决方案来解决 WebView 在说“我准备好了”时可能无法拍摄快照的问题:https://***.com/a/60746994/1534698。

这是一个总结,因为实际上有两个问题阻碍了 WebView 的简单快照。我必须合并这两个解决方案才能获得一个有效的干净解决方案。

【讨论】:

以上是关于JavaFX - 如何创建(不可见)WebView 的快照/屏幕截图的主要内容,如果未能解决你的问题,请参考以下文章

Java 画布不可见

JavaFX 项目出错:ImageIO 和 BufferedImage 不可见

关于JavaFx的WebView调用Echarts点线不对应的问题

JavaFX : 使用 JavaFX 嵌入浏览器而不是可用的 webview

JavaFx:打开另外一个窗体并加载WebView

JavaFX WebView:透明WebView保持绘制旧内容