同步两个 JavaFx WebView 的滚动条

Synchronize scrollbars of two JavaFx WebViews

我正在使用两个 WebView 来显示两个版本的 HTML 格式文本以供比较。两者显示相同数量的文本(相同的行数和对应的行始终具有相同的长度)。

当显示的文字超过节点大小时,WebView会出现滚动条。当然我想让这些滚动条同步滚动,这样一直显示相应的文字。

为了提供一个最小的、完整的和可验证的示例,我将代码缩减为:

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.GridPane;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

public class SynchronizedWebViewsTest extends Application {

  protected class DifferencePanel extends GridPane {
    private WebView actualPane;
    private WebView expectedPane;

    public DifferencePanel() {
      setPadding(new Insets(20, 20, 20, 20));
      actualPane = new WebView();
      expectedPane = new WebView();
      setResultPanes();
      addRow(0, actualPane, expectedPane);
    }

    public void setHtml(WebView webView) {
      Platform.runLater(() -> {
        webView.getEngine().loadContent(createHtml());
      });
    }

    public void synchronizeScrolls() {
      final ScrollBar actualScrollBarV = (ScrollBar)actualPane.lookup(".scroll-bar:vertical");
      final ScrollBar expectedScrollBarV = (ScrollBar)expectedPane.lookup(".scroll-bar:vertical");
      actualScrollBarV.valueProperty().bindBidirectional(expectedScrollBarV.valueProperty());
      final ScrollBar actualScrollBarH = (ScrollBar)actualPane.lookup(".scroll-bar:horizontal");
      final ScrollBar expectedScrollBarH = (ScrollBar)expectedPane.lookup(".scroll-bar:horizontal");
      actualScrollBarH.valueProperty().bindBidirectional(expectedScrollBarH.valueProperty());
    }

    private String createHtml() {
      StringBuilder sb = new StringBuilder(1000000);
      for (int i = 0; i < 100; i++) {
        sb.append(String.format("<nobr>%03d %2$s%2$s%2$s%2$s%2$s%2$s%2$s%2$s</nobr><br/>\n",
                                Integer.valueOf(i), "Lorem ipsum dolor sit amet "));
      }
      return sb.toString();
    }

    private void setResultPanes() {
      setHtml(actualPane);
      setHtml(expectedPane);
    }
  } // ---------------------------- end of DifferencePanel ----------------------------


  public static void main(String[] args){
    launch(args);
  }

  @Override
  public void start(Stage dummy)  throws Exception {
    Stage stage = new Stage();
    stage.setTitle(this.getClass().getSimpleName());
    DifferencePanel differencePanel = new DifferencePanel();
    Scene scene = new Scene(differencePanel);
    stage.setScene(scene);
    differencePanel.synchronizeScrolls();
    stage.showAndWait();
  }
}

我尝试使用添加监听器:

actualScrollBarV.onScrollFinishedProperty().addListener(event -> {
  System.out.println(event);
});

但是永远不会调用侦听器。

我使用的是 Java 1.8 版。0_92,但在 9.0.4 版中我得到了相同的结果。

谁能告诉我,我在这里遗漏了什么?

我会post发表评论,但遗憾的是我没有足够的声誉。

您是否尝试过以下解决方案?在值更改事件上创建侦听器,而不是绑定。

我无法使用 ScrollBar 方法。事实证明,实际上调用了侦听器(lambda 中的断点并不总是有效?)。设置其他 WebView 的滚动条值并没有让它倾斜改变滚动条或视口。 :-(

WebView 中发生了一些奇怪的事情;那可能是因为涉及到本地库...


但是,使用 WebView 的事件处理程序的方法是可行的。每个 WebView 的事件处理程序只是将所有事件镜像到另一个 WebView,使用同步字段 Boolean scrolling 来避免递归。

import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.Event;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

    public class SynchronizedWebViewsTest extends Application {

      protected class DifferencePanel extends GridPane {
        private Boolean scrolling = Boolean.FALSE;
        private WebView actualPane;
        private WebView expectedPane;

        public DifferencePanel() {
          setPadding(new Insets(20, 20, 20, 20));
          actualPane = new WebView();
          expectedPane = new WebView();
          setResultPanes();
          addRow(0, actualPane, expectedPane);
        }

        public void setHtml(WebView webView) {
          Platform.runLater(() -> {
            webView.getEngine().loadContent(createHtml());
          });
        }

        public void synchronizeScrolls() {
          wireViews(actualPane, expectedPane);
          wireViews(expectedPane, actualPane);
        }

        private void wireViews(WebView webView, WebView otherWebView) {
          webView.addEventHandler(Event.ANY, event -> {
            if (!scrolling.booleanValue()) {
              synchronized (scrolling) {
                scrolling = Boolean.TRUE;
                if (event instanceof MouseEvent) {
                  MouseEvent mouseEvent = (MouseEvent) event;
                  Point2D origin = webView.localToScreen(0, 0);
                  Point2D otherOrigin = otherWebView.localToScreen(0, 0);
                  double offsetX = otherOrigin.getX() - origin.getX();
                  double offsetY = otherOrigin.getY() - origin.getY();
                  double x = mouseEvent.getX();
                  double y = mouseEvent.getY();
                  double screenX = mouseEvent.getScreenX() + offsetX;
                  double screenY = mouseEvent.getScreenY() + offsetY;
                  MouseButton button = mouseEvent.getButton();
                  int clickCount = mouseEvent.getClickCount();
                  boolean shiftDown = mouseEvent.isShiftDown();
                  boolean controlDown = mouseEvent.isControlDown();
                  boolean altDown = mouseEvent.isAltDown();
                  boolean metaDown = mouseEvent.isMetaDown();
                  boolean primaryButtonDown = mouseEvent.isPrimaryButtonDown();
                  boolean middleButtonDown = mouseEvent.isMiddleButtonDown();
                  boolean secondaryButtonDown = mouseEvent.isSecondaryButtonDown();
                  boolean synthesized = mouseEvent.isSynthesized();
                  boolean popupTrigger = mouseEvent.isPopupTrigger();
                  boolean stillSincePress = mouseEvent.isStillSincePress();
                  MouseEvent otherMouseEvent =
                      new MouseEvent(otherWebView, otherWebView, mouseEvent.getEventType(), x, y, screenX,
                                     screenY, button, clickCount, shiftDown, controlDown, altDown, metaDown,
                                     primaryButtonDown, middleButtonDown, secondaryButtonDown, synthesized,
                                     popupTrigger, stillSincePress, null);
                  otherWebView.fireEvent(otherMouseEvent);
                }
                else {
                  otherWebView.fireEvent(event.copyFor(otherWebView, otherWebView));
                }
                scrolling = Boolean.FALSE;
              }
            }
          });
        }

        private String createHtml() {
          StringBuilder sb = new StringBuilder(1000000);
          for (int i = 0; i < 100; i++) {
            sb.append(String.format("<nobr>%03d %2$s%2$s%2$s%2$s%2$s%2$s%2$s%2$s</nobr><br/>\n",
                                    Integer.valueOf(i), "Lorem ipsum dolor sit amet "));
          }
          return sb.toString();
        }

        private void setResultPanes() {
          setHtml(actualPane);
          setHtml(expectedPane);
        }
      }


      public static void main(String[] args){
        launch(args);
      }

      @Override
      public void start(Stage dummy)  throws Exception {
        Stage stage = new Stage();
        stage.setTitle(this.getClass().getSimpleName());
        DifferencePanel differencePanel = new DifferencePanel();
        Scene scene = new Scene(differencePanel);
        stage.setScene(scene);
        differencePanel.synchronizeScrolls();
        stage.showAndWait();
      }
    }

这适用于我感兴趣的所有输入法:

  • 键盘:PageUp、PageDown、所有 4 个方向键、"space bar"(与 PageDown 相同)和 shift-"space bar"(与 PageUp 相同)、Home 和 End
  • 鼠标滚轮:RollDown 和 RollUp 以及 shift-RollUp(向左滚动)和 shift-RollDown(向右滚动)
  • 使用鼠标单击或拖动滚动条。
  • 使用鼠标 select 当前视口之外的文本。

镜像鼠标事件有一个额外的好处,即文本在两个 WebView 中得到 select编辑。