Как сделать рендеринг веб-браузера JavaFX, даже если он невидим из-за того, что он находится на другой вкладке?

Я загружаю веб-сайт в 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() является ложным (все остальные факторы в операторе return являются истинными). Этот метод находится на 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 был обновлен, но нет, это то же самое.

Ответ 1

Воспроизведение проблемы Пабло здесь: https://github.com/johanwitters/stackoverflow-javafx-webview.

Пабло предложил переопределить 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();
        }
    }
}

Этот фрагмент вызывается из WebViewSample, который открывает 2 вкладки. Один с кнопкой "моментальный снимок", другой - с WebView. Как отметил Пабло, вкладка с WebView должна быть второй вкладкой для воспроизведения.

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;
    }
}

Мне не удалось исправить проблему Пабло, но, надеюсь, может помочь предложение javassist.

Я уверен: Продолжение следует...

Ответ 2

Я бы подумал о перезагрузке страницы в подходящий момент, используя следующую команду:

webView.getEngine().reload();

Также попробуйте изменить параметр SnapshotParameters в методе snapShot

Если это не сработает, я бы подумал о сохранении изображения в памяти, когда WebView отображается на экране.

Ответ 3

Если вы выполняете логическую реализацию моментального снимка, в качестве моментального снимка принимаются только то, что видно на экране. Чтобы сделать снимок веб-представления, вы можете сделать его автоматически видимым, щелкнув вкладку, внутри которой визуализируется представление, перед тем, как сделать снимок. Или вы можете вручную щелкнуть по вкладке и сделать снимок экрана. Я думаю, что ни один 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) {
        }
    }