即使由于在另一个选项卡中而不可见,如何使 JavaFX 的 webview 呈现?

Posted

技术标签:

【中文标题】即使由于在另一个选项卡中而不可见,如何使 JavaFX 的 webview 呈现?【英文标题】:How do I make JavaFX's webview render even when not visible due to being in another tab? 【发布时间】:2018-06-03 12:20:39 【问题描述】:

我正在 JavaFX WebView 上加载一个网站,一段时间之后,我会截取如下内容:

WritableImage image = webView.snapshot(null, null);

如果我正在查看运行良好的 WebView,但如果它被隐藏在一个不在前台的选项卡中(我不确定其他隐藏它的情况),那么屏幕截图是适当的网站,但完全空白。

即使不可见,如何强制 WebView 呈现?

在此期间,webView.isVisible() 为真。

我发现WebView 中有一个名为isTreeReallyVisible() 的方法,目前它包含:

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;

当 WebView 通过在非前台选项卡中隐藏时,impl_isTreeVisible() 为假(返回语句中的所有其他因素都为真)。该方法在Node 上,如下所示:

/**
 * @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;

我本可以覆盖 impl_treeVisibleProperty() 以提供我自己的实现,但 WebView 是最终的,所以我不能继承它。

另一个与最小化(图标化)或隐藏选项卡完全不同的情况是完全隐藏舞台(如在托盘栏中运行)。在该模式下,即使我可以进行渲染,WebView 也不会调整大小。我打电话给webView.resize(),然后截取屏幕截图,屏幕截图大小合适,但实际呈现的页面与WebView 之前的大小相同。

在显示和隐藏阶段调试这种大小调整行为,我发现最终我们到达Node.addToSceneDirtyList(),其中包含:

private void addToSceneDirtyList() 
    Scene s = getScene();
    if (s != null) 
        s.addToDirtyList(this);
        if (getSubScene() != null) 
            getSubScene().setDirty(this);
        
    

在隐藏模式下,getScene() 返回null,这与显示时不同。这意味着永远不会调用s.addToDirtyList(this)。我不确定这是否是它没有正确调整大小的原因。

这里有一个错误,一个非常古老的错误:https://bugs.openjdk.java.net/browse/JDK-8087569,但我认为这不是全部问题。

我正在使用 Java 1.8.0_151 执行此操作。我尝试了 9.0.1 看看它的行为是否会有所不同,因为据我了解 WebKit 已升级,但不,它是相同的。

【问题讨论】:

我不记得选项卡窗格的皮肤是如何实现的,但在您描述的场景中,Web 视图可能不在场景中。在这种情况下,快照将不起作用(但您可以在拍摄快照时暂时将 Web 视图添加到场景中)。如果它仍然是场景的一部分,我不知道...... @James_D 不知道...世界末日。 看看有没有帮助***.com/questions/40642573/… 【参考方案1】:

在这里重现 Pablo 的问题:https://github.com/johanwitters/***-javafx-webview。

Pablo 建议重写 WebView 并调整一些方法。这是行不通的,因为它是一个最终类和一个私有成员。作为替代方案,我使用 javassist 重命名方法并将代码替换为我希望它执行的代码。我已经“替换”了handleStagePulse方法的内容,如下所示。

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();
        
    

这个 sn-p 是从打开 2 个选项卡的 WebViewSample 调用的。一个带有“快照”按钮,另一个带有 WebView。正如 Pablo 指出的那样,带有 WebView 的选项卡必须是能够重现的第二个选项卡。

package com.johanw.***;

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;
    

我没有成功解决 Pablo 的问题,但希望使用 javassist 的建议可能会有所帮助。

我确定:待续……

【讨论】:

除了上述之外,我想补充一点,如果你想覆盖最后的方法,这里有一种方法:***.com/questions/748362/… 当然....... Web 视图在视图之外不会被渲染。您是否尝试将浏览器放在第二个选项卡中并且您从未选择它它仍然会输出图像吗? 我还没有测试你的代码,但是你的例子和我正在做的事情之间的一个很大的不同,我应该在我的问题中指出,我没有显示标签在截屏之前。如果您在第二个选项卡上的 Web 视图上加载页面并且让第一个选项卡具有按钮并且从不显示第二个选项卡,那么您更接近我的情况。 感谢巴勃罗的反馈。我已经交换了标签,确实可以重现您的问题。我引入了 javassist 来更改 WebView 类,以便该方法运行以下代码,而不是原始代码。它没有用。然而! javafx.scene.Scene.impl_setAllowPGAccess(true);最终 com.sun.javafx.sg.prism.NGWebView peer = impl_getPeer(); peer.update(); // 创建新的渲染队列 impl_markDirty(com.sun.javafx.scene.DirtyBits.WEBVIEW_VIEW); javafx.scene.Scene.impl_setAllowPGAccess(false); 【参考方案2】:

我会考虑在适当的时候使用这个命令重新加载页面:

webView.getEngine().reload();

同时尝试在方法snapShot中更改参数SnapshotParameters

如果它不起作用,那么我会考虑在屏幕上呈现 WebView 时将 Image 存储在内存中。

【讨论】:

什么时候合适? WebView 可能永远不会出现在屏幕上。【参考方案3】:

如果您通过快照的逻辑实现,只有在屏幕上可见的东西才会被视为快照。 要拍摄 Web 视图的快照,您可以通过单击在拍摄快照之前呈现视图的选项卡使其自动可见。或者您可以手动单击选项卡并截取屏幕截图。 我认为没有 API 允许拍摄隐藏部分的快照,因为从逻辑上讲它会破坏隐藏事物的概念。 您已经可以使用拍摄快照的代码。您可以单击或加载选项卡,如:

你可以添加一个 selectionChangedListener 或者你可以在快照之前进行加载。

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) 
        
    

【讨论】:

您可以查看***.com/help/how-to-answer以获得更多了解。 我认为所有期望作为明确答案的细节都在那里。在指向评论之前要具体。 您不能快照隐藏元素的断言是错误的。如果它不在场景中,您甚至可以捕获(除非可见属性明确设置为 false)。出于某种原因,Web 视图在这里是一个例外,这就是问题所在。

以上是关于即使由于在另一个选项卡中而不可见,如何使 JavaFX 的 webview 呈现?的主要内容,如果未能解决你的问题,请参考以下文章

如何在一个选项卡中隐藏现有属性并在另一个选项卡中显示它

如何使 std::vector 的 operator[] 编译在 DEBUG 中而不是在 RELEASE 中进行边界检查

根据数据表内容使选项卡可见

如何在新选项卡中打开 vue 组件而不通过 vue 路由器加载页面?

在另一个选项卡中显示工具栏 crud 表

在另一个工作簿选项卡中搜索单元格值并打印数据