简体   繁体   English

JavaFX停止在WebView中打开URL-而是在浏览器中打开

[英]JavaFX stop opening URL in WebView - open in browser instead

The embedded WebView browser I am using needs special handling for particular URLs, to open them in the native default browser instead of WebView. 我使用的嵌入式WebView浏览器需要对特定URL进行特殊处理,才能在本机默认浏览器而不是WebView中打开它们。 The actual browsing part works fine but I need to stop the WebView from displaying that page as well. 实际的浏览部分工作正常,但我还需要停止WebView显示该页面。 I can think of several ways to do it but none of them work. 我可以想到几种方法,但没有一种有效。 Here is my code: 这是我的代码:

this.wv.getEngine().locationProperty().addListener(new ChangeListener<String>() {
    @Override
    public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue)
    {
        Desktop d = Desktop.getDesktop();
        try
        {
            URI address = new URI(observable.getValue());
            if ((address.getQuery() + "").indexOf("_openmodal=true") > -1)
            {
                // wv.getEngine().load(oldValue); // 1
                // wv.getEngine().getLoadWorker().cancel(); // 2
                // wv.getEngine().executeScript("history.back()"); // 3
                d.browse(address);
            }
        }
        catch (IOException | URISyntaxException e)
        {
            displayError(e);
        }
    }
});

A bit more info about what happens in each of three cases 有关三种情况下发生的情况的更多信息

1. Loading the previous address 1.加载先前的地址

wv.getEngine().load(oldValue);

This kills the JVM. 这会杀死JVM。 Funnily enough, the page opens fine in the native browser. 有趣的是,该页面在本机浏览器中可以正常打开。

# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x000000005b8fef38, pid=7440, tid=8000
#
# JRE version: 7.0_09-b05
# Java VM: Java HotSpot(TM) 64-Bit Server VM (23.5-b02 mixed mode windows-amd64 compressed oops)
# Problematic frame:
# C  [jfxwebkit.dll+0x2fef38]  Java_com_sun_webpane_platform_BackForwardList_bflItemGetIcon+0x184f58
#
# Failed to write core dump. Minidumps are not enabled by default on client versions of Windows
#
# An error report file with more information is saved as:
# C:\Users\Greg Balaga\eclipse\Companyapp\hs_err_pid7440.log
#
# If you would like to submit a bug report, please visit:
#   http://bugreport.sun.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.

2. Cancelling the worker 2.取消工人

wv.getEngine().getLoadWorker().cancel();

Does nothing, the page loads in both the WebView and native browser. 不执行任何操作,页面将同时加载到WebView和本机浏览器中。

3. Using history.back() 3.使用history.back()

wv.getEngine().executeScript("history.back()");

Same as above, no effect. 与上面相同,没有效果。

4. Reacting to Stage changes instead 4.应对阶段变更

I have also tried to instead of looking the locationProperty of WebEngine , listen on chenges for stateProperty of the Worker and fire the same opening code if newState == State.SCHEDULED . 我也试过,而不是寻找的locationPropertyWebEngine ,监听chenges为stateProperty的的Worker和火如果同一开放代码newState == State.SCHEDULED There was no difference in result from previous method (apart from not actually being able to use #1). 与以前的方法相比,结果没有差异(除了实际上无法使用#1之外)。


Update 更新资料

The code I'm using now still crashes the JVM: 我现在使用的代码仍然会使JVM崩溃:

this.wv.getEngine().locationProperty().addListener(new ChangeListener<String>() {
    @Override
    public void changed(ObservableValue<? extends String> observable, final String oldValue, String newValue)
    {
        Desktop d = Desktop.getDesktop();
        try
        {
            URI address = new URI(newValue);
            if ((address.getQuery() + "").indexOf("_openmodal=true") > -1)
            {
                Platform.runLater(new Runnable() {
                    @Override
                    public void run()
                    {
                        wv.getEngine().load(oldValue);
                    }
                });
                d.browse(address);
            }
        }
        catch (IOException | URISyntaxException e)
        {
            displayError(e);
        }
    }
});

Workaround 解决方法

Ok I managed to make it work by tearing down the webview and rebuilding it. 好的,我设法通过拆掉webview并重建它来使其工作。

this.wv.getEngine().locationProperty().addListener(new ChangeListener<String>() {
    @Override
    public void changed(ObservableValue<? extends String> observable, final String oldValue, String newValue)
    {
        Desktop d = Desktop.getDesktop();
        try
        {
            URI address = new URI(newValue);
            if ((address.getQuery() + "").indexOf("_openmodal=true") > -1)
            {
                Platform.runLater(new Runnable() {
                    @Override
                    public void run()
                    {
                        grid_layout.getChildren().remove(wv);
                        wv = new WebView();
                        grid_layout.add(wv, 0, 1);
                        wv.getEngine().load(oldValue);
                    }
                });
                d.browse(address);
            }
        }
        catch (IOException | URISyntaxException e)
        {
            displayError(e);
        }
    }
});

There is another method for handling this. 还有另一种处理方法。

You can add an event listener to the DOM elements and intercept it that way. 您可以将事件侦听器添加到DOM元素,然后以这种方式进行拦截。

Example: 例:

NodeList nodeList = document.getElementsByTagName("a");
            for (int i = 0; i < nodeList.getLength(); i++)
            {
                Node node= nodeList.item(i);
                EventTarget eventTarget = (EventTarget) node;
                eventTarget.addEventListener("click", new EventListener()
                {
                    @Override
                    public void handleEvent(Event evt)
                    {
                        EventTarget target = evt.getCurrentTarget();
                        HTMLAnchorElement anchorElement = (HTMLAnchorElement) target;
                        String href = anchorElement.getHref();
                        //handle opening URL outside JavaFX WebView
                        System.out.println(href);
                        evt.preventDefault();
                    }
                }, false);
            }

Where document is the DOM document object. 其中document是DOM文档对象。 Make sure this is done after the document has finished loading. 确保在文档加载完成后完成此操作。

I finally found a working solution that worked for me: 我终于找到了对我有用的可行解决方案:

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.scene.web.WebView;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.html.HTMLAnchorElement;

import java.awt.*;
import java.net.URI;

public class HyperLinkRedirectListener implements ChangeListener<Worker.State>, EventListener
{
    private static final String CLICK_EVENT = "click";
    private static final String ANCHOR_TAG = "a";

    private final WebView webView;

    public HyperLinkRedirectListener(WebView webView)
    {
        this.webView = webView;
    }

    @Override
    public void changed(ObservableValue<? extends Worker.State> observable, Worker.State oldValue, Worker.State newValue)
    {
        if (Worker.State.SUCCEEDED.equals(newValue))
        {
            Document document = webView.getEngine().getDocument();
            NodeList anchors = document.getElementsByTagName(ANCHOR_TAG);
            for (int i = 0; i < anchors.getLength(); i++)
            {
                Node node = anchors.item(i);
                EventTarget eventTarget = (EventTarget) node;
                eventTarget.addEventListener(CLICK_EVENT, this, false);
            }
        }
    }

    @Override
    public void handleEvent(Event event)
    {
        HTMLAnchorElement anchorElement = (HTMLAnchorElement) event.getCurrentTarget();
        String href = anchorElement.getHref();

        if (Desktop.isDesktopSupported())
        {
            openLinkInSystemBrowser(href);
        } else
        {
            // LOGGER.warn("OS does not support desktop operations like browsing. Cannot open link '{}'.", href);
        }

        event.preventDefault();
    }

    private void openLinkInSystemBrowser(String url)
    {
        // LOGGER.debug("Opening link '{}' in default system browser.", url);

        try
        {
            URI uri = new URI(url);
            Desktop.getDesktop().browse(uri);
        } catch (Throwable e)
        {
            // LOGGER.error("Error on opening link '{}' in system browser.", url);
        }
    }
}

Usage: 用法:

webView.getEngine().getLoadWorker().stateProperty().addListener(new HyperLinkRedirectListener(webView));

This worked for me as I had to generically trap any anchor with target="_blank". 这对我来说很有效,因为我必须使用target =“ _ blank”捕获所有锚点。 I had to work around the fact that the PopupFeatures callback has absolutely no useful context by asking the DOM for all elements under the pointer (eg :hover). 我不得不解决以下事实:PopupFeatures回调绝对没有有用的上下文,方法是向DOM请求指针下的所有元素(例如:hover)。

// intercept target=_blank hyperlinks
webView.getEngine().setCreatePopupHandler(
    new Callback<PopupFeatures, WebEngine>() {
        @Override
        public WebEngine call(PopupFeatures config) {
            // grab the last hyperlink that has :hover pseudoclass
            Object o = webView
                    .getEngine()
                    .executeScript(
                            "var list = document.querySelectorAll( ':hover' );"
                                    + "for (i=list.length-1; i>-1; i--) "
                                    + "{ if ( list.item(i).getAttribute('href') ) "
                                    + "{ list.item(i).getAttribute('href'); break; } }");

            // open in native browser
            try {
                if (o != null) {
                    Desktop.getDesktop().browse(
                            new URI(o.toString()));
                } else {
                    log.error("No result from uri detector: " + o);
                }
            } catch (IOException e) {
                log.error("Unexpected error obtaining uri: " + o, e);
            } catch (URISyntaxException e) {
                log.error("Could not interpret uri: " + o, e);
            }

            // prevent from opening in webView
            return null;
        }
    });

@Avrom's answer of using DOM interceptors offers a better solution than this answer with regards to the question: "JavaFX stop opening URL in WebView - open in browser instead". 关于以下问题,@ Avrom使用DOM拦截器的答案提供了比该答案更好的解决方案:“ JavaFX停止在WebView中打开URL-而是在浏览器中打开”。

This answer just left for posterity. 这个答案留给后代。


Use option 1 engine.load(oldValue) and wrap the load call in Platform.runLater as a workaround to prevent the jvm crash. 使用选项1 engine.load(oldValue)并将load调用包装在Platform.runLater中,作为防止jvm崩溃的解决方法。

import javafx.application.*;
import javafx.beans.value.*;
import javafx.scene.Scene;
import javafx.scene.web.*;
import javafx.stage.Stage;

public class GoogleBlock extends Application {
  public static void main(String[] args) throws Exception { launch(args); }

  @Override public void start(final Stage stage) throws Exception {
    final WebView webView = new WebView();
    final WebEngine engine = webView.getEngine();
    engine.load("http://www.google.com");
    engine.locationProperty().addListener(new ChangeListener<String>() {
      @Override public void changed(ObservableValue<? extends String> ov, final String oldLoc, final String loc) {
        if (!loc.contains("google.com")) {
          Platform.runLater(new Runnable() {
            @Override public void run() {
              engine.load(oldLoc);
            }
          });
        }
      }
    });

    stage.setScene(new Scene(webView));
    stage.show();
  }
}

Update 更新资料

Although the above solution works OK for me in the supplied GoogleBlock sample application under jdk7u15, win7, Dreen reports that just wrapping the load value in Platform.runLater does not fix crash issues in all cases, so the complete replacement of the WebView object with a new WebView (as Dreen outlines in the updated question), might be the preferred solution here (at least until the underlying bug is fixed). 尽管上述解决方案在jdk7u15,win7下提供的GoogleBlock示例应用程序中对我来说可以正常运行,但Dreen报告说,仅将load值包装在Platform.runLater中并不能解决所有情况下的崩溃问题,因此将WebView对象完全替换为新的WebView(如Dreen在更新的问题中概述的那样)可能是此处的首选解决方案(至少在修复了基本错误之前)。


The jvm crash you note in your question is a known issue in JavaFX 2.2: 您在问题中注意到的jvm崩溃是JavaFX 2.2中的一个已知问题:

JDK-8087652 WebView crashes on calling webEngine.load(url) in a webEngine.locationProperty() ChangeListener . JDK-8087652调用webEngine.locationProperty()ChangeListener中的webEngine.load(url)时,WebView崩溃

Sorry for digging out this old thread but I found another solution that I wanted to share with others who struggle with the same problem. 很抱歉挖掘出这个旧线程,但是我找到了另一个解决方案,我想与其他遇到相同问题的人分享。 I found a library that has a nice wrapper around the entire issue, see its docs at github . 我发现一个库在整个问题上都有很好的包装,请参阅github上的文档

Edit: Oh, sry for not telling what the project does: The linked library contains a class that actually implemented all of the code discussed in this thread. 编辑:哦,很抱歉不告诉您该项目做什么:链接库包含一个实际上实现了该线程中讨论的所有代码的类。 The user can simply create a new instance of the WebViewHyperlinkListener -interface that gets automatically called when something (mouse enter, mouse quit, mouse click) happens with the link. 用户可以简单地创建WebViewHyperlinkListener的新实例,当链接发生某些事件(鼠标进入,鼠标退出,鼠标单击)时,将自动调用该实例。 Once the handler terminates, it returns a boolean: If the handler returns true , the WebView will not navigate to the linked web page. 处理程序终止后,将返回一个布尔值:如果处理程序返回true ,则WebView将不会导航到链接的网页。 If the handler returns false , it will. 如果处理程序返回false ,它将返回。

When wrapping the d.browse call into a Runnable Object the runtime error never occured again. 将d.browse调用包装到Runnable对象中时,再也不会发生运行时错误。 The strange thing was without that wrapping the ChangeListener was called a second time after some seconds with the same new location and this second call crashed the JVM. 奇怪的是,没有在几秒钟后使用相同的新位置再次调用ChangeListener,并且第二次调用使JVM崩溃。

I found another solution: 我找到了另一个解决方案:

  1. Register a CreatePopupHandler that returns a different WebEngine than the main one 注册一个CreatePopupHandler,它返回与主WebEngine不同的WebEngine。
  2. The main WebEngine sends the load-call to the secondary WebEngine 主WebEngine将加载调用发送到辅助WebEngine
  3. Register a LocationChangeListener on the secondary WebEngine and catch the location change (including the address) and open it in our external browser 在辅助WebEngine上注册一个LocationChangeListener并捕获位置更改(包括地址)并在我们的外部浏览器中将其打开
  4. Finally clean up the secondary WebEngine: stop loading & unload the URL 最后清理辅助WebEngine:停止加载和卸载URL

I implemented it so the secondary WebEngine is initialized lazy. 我实现了它,以便辅助WebEngine初始化为惰性。 It may also be initialized in the constructor. 也可以在构造函数中对其进行初始化。 Both has pros and contras. 两者都有利弊。

Note: This only triggers for Links which open as a popup. 注意:这只会触发以弹出窗口形式打开的链接。 This usually is the case when an a-element has a target-attribute that is not "_self" or with JS: window.open(...) . 当a元素的目标属性不是“ _self”或使用JS: window.open(...)时,通常是这种情况。

Here is the Magic ... 这是魔术...

Register it like this: 像这样注册:

engine.setCreatePopupHandler(new BrowserPopupHandler());

The core class: 核心课程:

public static class BrowserPopupHandler implements Callback<PopupFeatures, WebEngine>
{

    private WebEngine popupHandlerEngine;

    public WebEngine call(PopupFeatures popupFeatures)
    {
        // by returning null here the action would be canceled
        // by returning a different WebEngine (than the main one where we register our listener) the load-call will go to that one
        // we return a different WebEngine here and register a location change listener on it (see blow)
        return getPopupHandler();
    }

    private WebEngine getPopupHandler()
    {
        if (popupHandlerEngine == null) // lazy init - so we only initialize it when needed ...
        {
            synchronized (this) // double checked synchronization
            {
                if (popupHandlerEngine == null)
                {
                    popupHandlerEngine = initEngine();
                }
            }
        }
        return popupHandlerEngine;
    }

    private WebEngine initEngine()
    {
        final WebEngine popupHandlerEngine = new WebEngine();

        // this change listener will trigger when our secondary popupHandlerEngine starts to load the url ...
        popupHandlerEngine.locationProperty().addListener(new ChangeListener<String>()
        {

            public void changed(ObservableValue<? extends String> observable, String oldValue, String location)
            {
                if (!location.isEmpty())
                {
                    Platform.runLater(new Runnable()
                    {

                        public void run()
                        {
                            popupHandlerEngine.loadContent(""); // stop loading and unload the url
                            // -> does this internally: popupHandlerEngine.getLoadWorker().cancelAndReset();
                        }

                    });

                    try
                    {
                        // Open URL in Browser:
                        Desktop desktop = Desktop.getDesktop();
                        if (desktop.isSupported(Desktop.Action.BROWSE))
                        {
                            URI uri = new URI(location);
                            desktop.browse(uri);
                        }
                        else
                        {
                            System.out.println("Could not load URL: " + location);
                        }
                    }
                    catch (Exception e)
                    {
                        e.printStackTrace();
                    }
                }
            }

        });
        return popupHandlerEngine;
    }

}

2017 version - still very hacky but much more concise: 2017版-仍然非常hacky,但更加简洁:

class AboutDialog extends Dialog {
    private final Controller controller;
    private final String url;

    AboutDialog() {
        super();

        this.controller = Controller.getInstance();

        this.setTitle(controller.getProperty("about_title"));
        this.setHeaderText(null);

        this.url = getClass().getResource("/about_dialog.html").toExternalForm();

        this.setWebView();

        this.getDialogPane().getButtonTypes().add(new ButtonType(controller.getProperty("close"), ButtonBar.ButtonData.CANCEL_CLOSE));
        this.getDialogPane().setPrefWidth(600);
    }

    private void setWebView() {
        final WebView webView = new WebView();
        webView.getEngine().load(url);

        webView.getEngine().locationProperty().addListener((observable, oldValue, newValue) -> {
            controller.getMainFxApp().getHostServices().showDocument(newValue);
            Platform.runLater(this::setWebView);
        });

        this.getDialogPane().setContent(webView);
    }
}

Your "update" section is actually really close to what I got to work, no workaround needed: 您的“更新”部分实际上与我的工作非常接近,不需要任何解决方法:

wv.getEngine().locationProperty().addListener(new ChangeListener<String>() {    
            public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue)
            {
                if((address.getQuery() + "").indexOf("_openmodal=true") > -1) {
                    Platform.runLater(new Runnable() {
                        public void run() {
                            try {
                                Desktop.getDesktop().browse(new URI(newValue));
                            } catch (IOException e) { //Decide how to handle:
                                //Can't find default program handler for link.
                            } catch (URISyntaxException e) {
                                //Bad syntax on link.
                            }
                            wv.getEngine().reload();
                        }
                    });
                }
            }

        });

What I like about this method is that it accounts for a URL coming from more than just a hyperlink element as well as links to things like emails or local files. 我喜欢这种方法的地方在于,它不仅包含超链接元素而且还包含指向电子邮件或本地文件之类的链接的URL。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM