JavaFX прекращает открытие URL-адреса в WebView - вместо этого откроется в браузере
Встроенный браузер 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);
}
}
});
Немного больше информации о том, что происходит в каждом из трех случаев
1. Загрузка предыдущего адреса
wv.getEngine().load(oldValue);
Это убивает 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. Отмена работника
wv.getEngine().getLoadWorker().cancel();
Ничего, страница загружается как в WebView, так и в родном браузере.
3. Использование history.back()
wv.getEngine().executeScript("history.back()");
То же, что и выше, никакого эффекта.
4. Реакция на смену этапов вместо
Я также пытался вместо того, чтобы искать locationProperty
из WebEngine
, слушать stateProperty
для stateProperty
Worker
и stateProperty
тот же начальный код, если newState == State.SCHEDULED
. Не было никакой разницы в результате предыдущего метода (кроме того, что он фактически не мог использовать № 1).
Обновить
Код, который я использую, по-прежнему вызывает сбои 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);
}
}
});
Временное решение
Хорошо, мне удалось заставить его работать, срывая веб-просмотр и перестраивая его.
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);
}
}
});
Ответы
Ответ 1
Существует еще один способ для этого.
Вы можете добавить прослушиватель событий к элементам DOM и перехватить его таким образом.
Пример:
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);
}
Где документ является объектом документа DOM. Убедитесь, что это сделано после завершения загрузки документа.
Ответ 2
Наконец, я нашел рабочее решение, которое сработало для меня:
public class HyperlinkRedirectListener implements ChangeListener<Worker.State>, EventListener {
private static final Logger LOGGER = LoggerFactory.getLogger(HyperlinkRedirectListener.class);
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 State> observable, State oldValue, State newValue) {
if (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);
}
}
}
webView.getEngine(). getLoadWorker(). stateProperty(). addListener (новый HyperlinkRedirectListener (webView));
Ответ 3
Это работало для меня, поскольку мне приходилось в основном ловить любой якорь с целью = "_ blank". Мне пришлось обойти тот факт, что обратный вызов 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;
}
});
Ответ 4
Ответ @Avrom использования DOM-перехватчиков предлагает лучшее решение, чем этот ответ в связи с вопросом: "JavaFX перестает открывать URL-адрес в WebView - вместо этого открывать в браузере".
Этот ответ только что вышел для потомков.
Используйте опцию 1 engine.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();
}
}
Обновить
Хотя приведенное выше решение работает нормально для меня в прилагаемом образце приложения GoogleBlock в jdk7u15, win7, Dreen сообщает, что просто обертывание значения нагрузки в Platform.runLater не устраняет проблемы с аварийными ситуациями во всех случаях, поэтому полная замена объекта WebView новый WebView (как начертания Дринга в обновленном вопросе), может быть предпочтительным решением здесь (по крайней мере, до устранения основной ошибки).
Сбой jvm, который вы отмечаете в своем вопросе, является известной проблемой в JavaFX 2.2:
JDK-8087652 Сбой WebView при вызове webEngine.load(url) в webEngine.locationProperty() ChangeListener.
Ответ 5
Извините за то, что выкалываете этот старый поток, но я нашел другое решение, которое я хотел бы поделиться с другими, которые борются с той же проблемой. Я нашел библиотеку с красивой оболочкой по всей проблеме, см. Ее документы в github.
Редактировать: О, ладно, за то, что вы не сказали, что делает проект. Связанная библиотека содержит класс, который фактически реализовал весь код, обсуждаемый в этом потоке. Пользователь может просто создать новый экземпляр WebViewHyperlinkListener
-interface, который автоматически вызывается, когда что-то (ввод мыши, выключение мыши, щелчок мыши) происходит со ссылкой. Как только обработчик завершается, он возвращает логическое значение: Если обработчик возвращает true
, WebView не будет перемещаться по связанной веб-странице. Если обработчик вернет false
, это произойдет.
Ответ 6
При переносе вызова d.browse в Runnable Object ошибка времени выполнения никогда не повторялась. Странная вещь была без того, чтобы обернуть ChangeListener был вызван второй раз через несколько секунд с тем же самым новым местоположением, и этот второй вызов разбил JVM.
Ответ 7
Я нашел другое решение:
- Зарегистрируйте CreatePopupHandler, который возвращает другой WebEngine, чем основной
- Основной WebEngine отправляет вызов нагрузки второму WebEngine
- Зарегистрируйте LocationChangeListener на вторичном WebEngine и поймайте изменение местоположения (включая адрес) и откройте его в нашем внешнем браузере.
- Наконец, очистите вторичный WebEngine: прекратите загружать и выгружать URL-адрес
Я реализовал его, чтобы вторичный WebEngine инициализировался ленивым. Он также может быть инициализирован в конструкторе. У обоих есть плюсы и минусы.
Примечание. Это вызывает только ссылки, которые открываются как всплывающие окна. Обычно это тот случай, когда a-элемент имеет атрибут target, который не является "_self" или с JS: window.open(...)
.
Вот волшебство...
Зарегистрируйте его так:
engine.setCreatePopupHandler(new BrowserPopupHandler());
Основной класс:
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;
}
}
Ответ 8
Версия 2017 года - все еще очень хаки, но гораздо более кратким:
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);
}
}