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:UsersGreg BalagaeclipseCompanyapphs_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. Используя историю.back ()

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

то же, что и выше, без эффекта.

4. Реагирование на изменения стадии вместо

я также попытался вместо того, чтобы смотреть locationProperty of WebEngine, слушайте на chenges для stateProperty на Worker и запустите тот же код открытия, если 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);
        }
    }
});

решение

Ok мне удалось заставить его работать, сорвав 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);
        }
    }
});

8 ответов


существует другой метод для обработки этого.

вы можете добавить прослушиватель событий к элементам 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);
            }

где document является объектом DOM document. Убедитесь, что это сделано после завершения загрузки документа.


Я, наконец, нашел рабочее решение, которое сработало для меня:

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


это сработало для меня, поскольку мне пришлось в общем случае ловить любой якорь с target= "_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;
        }
    });

ответ@Avrom об использовании перехватчиков DOM предлагает лучшее решение, чем этот ответ в отношении вопроса: "JavaFX остановить открытие URL в WebView - открыть в браузере вместо этого".

этот ответ просто оставил для потомков.


используйте опцию 1 engine.load(oldValue) и оберните вызов load в .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 сообщает, что просто обертывает значение нагрузки в платформе.runLater не исправляет проблемы сбоя во всех случаях, поэтому полная замена объекта WebView новым WebView (как описывает Дрин в обновленном вопросе) может быть предпочтительным решением здесь (по крайней мере, пока не будет исправлена основная ошибка).


сбой jvm, который вы отмечаете в своем вопросе, является известной проблемой в JavaFX 2.2:

JDK-8087652 WebView аварийно завершает работу при вызове webEngine.загрузить (url) в webEngine.locationProperty () ChangeListener.


извините за выкапывание этой старой темы, но я нашел другое решение, которое я хотел поделиться с другими, кто борется с той же проблемой. Я нашел библиотеку, которая имеет хорошую обертку вокруг всей проблемы, см. Ее документы в github.

Edit: О, sry за то, что не сказал, что делает проект: Связанная библиотека содержит класс, который фактически реализовал весь код, обсуждаемый в этом потоке. Пользователь может просто создать новый экземпляр WebViewHyperlinkListener-интерфейс, который автоматически вызывается, когда что-то (ввод мыши, выход мыши, щелчок мыши) происходит со ссылкой. Как только обработчик завершается, он возвращает логическое значение: если обработчик возвращает true, то WebView не будет переходить на веб-страницу. Если обработчик возвращает false, это будет.


при обертывании d.обзор вызова в объект Runnable ошибка выполнения никогда не возникала снова. Странная вещь заключалась в том, что без упаковки ChangeListener был вызван второй раз через несколько секунд с тем же новым местоположением, и этот второй вызов разбил JVM.


Я нашел другое решение:

  1. зарегистрируйте CreatePopupHandler, который возвращает другой WebEngine, чем основной
  2. главный WebEngine отправляет load-call на вторичный WebEngine
  3. зарегистрируйте LocationChangeListener на вторичном WebEngine и поймайте изменение местоположения (включая адрес) и откройте его в нашем внешнем браузере
  4. окончательно очистите вверх вторичное WebEngine: стоп нагружая & разгружает URL-адрес

я реализовал его, поэтому вторичный WebEngine инициализируется ленивым. Он также может быть инициализирован в конструкторе. Оба имеют плюсы и минусы.

Примечание: это срабатывает только для ссылок, которые открываются в всплывающем окне. Обычно это происходит, когда A-элемент имеет целевой атрибут, который не является "_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;
    }

}

версия 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);
    }
}