java JavaFX 停止在 WebView 中打开 URL - 在浏览器中打开
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/15555510/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
JavaFX stop opening URL in WebView - open in browser instead
提问by Dreen
The embedded WebView browser I am using needs special handling for particular URLs, to open them in the native default browser instead of WebView. The actual browsing part works fine but I need to stop the WebView from displaying that page as well. I can think of several ways to do it but none of them work. Here is my code:
我使用的嵌入式 WebView 浏览器需要对特定 URL 进行特殊处理,以便在本机默认浏览器而不是 WebView 中打开它们。实际浏览部分工作正常,但我也需要停止 WebView 显示该页面。我可以想到几种方法来做到这一点,但没有一个有效。这是我的代码:
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. Funnily enough, the page opens fine in the native browser.
这会杀死 JVM。有趣的是,页面在本机浏览器中打开得很好。
# 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. 对 Stage 的变化做出反应
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
. There was no difference in result from previous method (apart from not actually being able to use #1).
我也尝试过,而不是寻找locationProperty
of WebEngine
,而是监听 chenges for stateProperty
ofWorker
并触发相同的打开代码 if newState == State.SCHEDULED
。结果与以前的方法没有区别(除了实际上无法使用#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);
}
}
});
回答by Avrom
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. Make sure this is done after the document has finished loading.
其中 document 是 DOM 文档对象。确保在文档完成加载后完成此操作。
回答by MatWein
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));
回答by Michael Powers
This worked for me as I had to generically trap any anchor with 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 (e.g. :hover).
这对我有用,因为我必须用 target="_blank" 来捕获任何锚点。我不得不通过向 DOM 询问指针下的所有元素(例如:hover)来解决 PopupFeatures 回调完全没有有用的上下文这一事实。
// 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;
}
});
回答by jewelsea
@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.runLateras a workaround to prevent the jvm crash.
使用选项 1engine.load(oldValue)
并将加载调用包装在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 报告说,仅将负载值包装在 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 在 webEngine.locationProperty() ChangeListener 中调用 webEngine.load(url) 时崩溃。
回答by vatbub
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. Once the handler terminates, it returns a boolean: If the handler returns true
, the WebView will not navigate to the linked web page. If the handler returns false
, it will.
编辑:哦,请不要告诉我该项目是做什么的:链接库包含一个类,该类实际上实现了该线程中讨论的所有代码。用户可以简单地创建一个新的WebViewHyperlinkListener
-interface实例,当链接发生某些事情(鼠标进入、鼠标退出、鼠标单击)时,它会自动调用。一旦处理程序终止,它会返回一个布尔值:如果处理程序返回true
,WebView 将不会导航到链接的网页。如果处理程序返回false
,它会。
回答by Jan Bene?
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);
}
}
回答by JCamacho
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。
回答by Frederic Leitenberger
I found another solution:
我找到了另一个解决方案:
- Register a CreatePopupHandler that returns a different WebEngine than the main one
- The main WebEngine sends the load-call to the secondary WebEngine
- Register a LocationChangeListener on the secondary WebEngine and catch the location change (including the address) and open it in our external browser
- Finally clean up the secondary WebEngine: stop loading & unload the URL
- 注册一个 CreatePopupHandler,它返回一个不同于主要的 WebEngine
- 主 WebEngine 将加载调用发送到辅助 WebEngine
- 在辅助 WebEngine 上注册一个 LocationChangeListener 并捕获位置更改(包括地址)并在我们的外部浏览器中打开它
- 最后清理二级WebEngine:停止加载和卸载URL
I implemented it so the secondary WebEngine is initialized lazy. It may also be initialized in the constructor. Both has pros and contras.
我实现了它,因此辅助 WebEngine 被初始化为惰性。它也可以在构造函数中初始化。两者各有优缺点。
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;
}
}
回答by Uwe
When wrapping the d.browse call into a Runnable Object the runtime error never occured again. 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.
将 d.browse 调用包装到 Runnable 对象中时,运行时错误再也没有发生。奇怪的是,如果没有包装,在几秒钟后使用相同的新位置第二次调用 ChangeListener 并且第二次调用使 JVM 崩溃。