JavaFX - 如何同步 FXML 文件中声明的两个元素的滚动条

JavaFX - how to synchronize scrollbars of two elements declared in FXML file

我正在开发 IDE,但我遇到了以下问题:

我想创建 TabPane,加载到程序中的某个位置(到 VBox),然后在使用一些 addFile 处理程序时添加 TAB,并通过 code_workspace.fxml 文件构建 TAB 内容(目前一切正常) .但我只想同步存储在同一个 FXML 文件中的元素的两个滚动条。具体就是ScrollPane(code_line_counter)和TextArea(code_text_area)的滚动条。

我学习了一年Java,所以我欢迎你给我的每一个建议。如您所见,我的英语不是很好,所以我想您会从代码中阅读很多信息:

FileManager.java

package sample;

import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;

import java.net.URL;
import java.util.ResourceBundle;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class FileManager extends TabPane implements Initializable {

    @FXML private ScrollPane code_line_counter;
    @FXML private TextArea code_text_area;
    @FXML private Label code_line_counter_label;

    private int created_tabs;

    public FileManager()
    {
        super();
        super.setTabClosingPolicy(TabPane.TabClosingPolicy.ALL_TABS);
        this.created_tabs = 0;
    }

    @Override
    public void initialize(URL location, ResourceBundle resources)
    {

        code_text_area.textProperty().addListener((observable, oldValue, newValue) -> {

            Matcher matcher_old_value = Pattern.compile("\n").matcher(oldValue);
            Matcher matcher_new_value = Pattern.compile("\n").matcher(newValue);

            int old_line_count = 0;
            int new_line_count = 0;

            while (matcher_old_value.find()) {
                old_line_count++;
            }

            while (matcher_new_value.find()) {
                new_line_count++;
            }

            if (new_line_count != old_line_count) {

                code_line_counter_label.setText("1");
                for (int i = 2; i < new_line_count + 2; i++)
                    code_line_counter_label.setText(code_line_counter_label.getText() + "\n" + i);

            }

        });

        Platform.runLater(() -> {

            ScrollBar s1 = (ScrollBar) code_line_counter.lookup(".scroll-bar:vertical");
            ScrollBar s2 = (ScrollBar) code_text_area.lookup(".scroll-bar:vertical");
            s2.valueProperty().bindBidirectional(s1.valueProperty());

        });
        }

    public boolean addFile (StackPane work_space, VBox welcome_screen, NotificationManager notification_manager)
    {    
        Tab new_tab = new Tab("Untitled " + (this.created_tabs + 1));

        try {
            FXMLLoader fxml_loader = new FXMLLoader(getClass().getResource("elements/code_workspace.fxml"));
            new_tab.setContent(fxml_loader.load());
        } catch (Exception e) {
            Notification fxml_load_error = new Notification("icons/notifications/default_warning.png");
            notification_manager.addNotification(fxml_load_error);
            return false;
        }

        new_tab.setOnClosed(event -> {
            if (super.getTabs().isEmpty()) {
                this.created_tabs = 0;
                work_space.getChildren().clear();
                work_space.getChildren().add(welcome_screen);
            }
        });

        super.getTabs().add(new_tab);
        super.getSelectionModel().select(new_tab);
        this.created_tabs++;

        return true;
    }

}

code_workspace.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import java.net.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<VBox xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.FileManager">
    <children>
        <HBox minHeight="-Infinity" styleClass="search-bar" VBox.vgrow="NEVER" />
      <HBox VBox.vgrow="ALWAYS">
         <children>
            <ScrollPane id="code_line_counter" fx:id="code_line_counter" fitToWidth="true" focusTraversable="false" hbarPolicy="NEVER" minWidth="-Infinity" vbarPolicy="NEVER" HBox.hgrow="NEVER">
               <content>
                  <Label fx:id="code_line_counter_label" alignment="TOP_LEFT" focusTraversable="false" nodeOrientation="RIGHT_TO_LEFT" text="1" />
               </content>
            </ScrollPane>
              <TextArea fx:id="code_text_area" focusTraversable="false" promptText="Let's code!" HBox.hgrow="ALWAYS" />
         </children>
      </HBox>
    </children>
   <stylesheets>
      <URL value="@../styles/ezies_theme.css" />
      <URL value="@../styles/sunis_styles.css" />
   </stylesheets>
</VBox>

我尝试了很多移动该代码部分的方法:

ScrollBar s1 = (ScrollBar) code_line_counter.lookup(".scroll-bar:vertical");
            ScrollBar s2 = (ScrollBar) code_text_area.lookup(".scroll-bar:vertical");
            s2.valueProperty().bindBidirectional(s1.valueProperty());

但没有任何帮助。我也试过删除 Platform.runLater(() -> {});,但也没有用。

各位大侠,请问这个问题的正确解法是什么?

*加法: 有一个非常有趣的行为 - 当我添加第一个选项卡时,它运行正常,但是当我将第二个或更多选项卡添加到程序集合中时,在大约 70% 的情况下出现以下异常:

Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
    at sample.FileManager.lambda$initialize(FileManager.java:75)
    at sample.FileManager$$Lambda4/1491808102.run(Unknown Source)
    at com.sun.javafx.application.PlatformImpl.lambda$null0(PlatformImpl.java:295)
    at com.sun.javafx.application.PlatformImpl$$Lambda/1472939626.run(Unknown Source)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.application.PlatformImpl.lambda$runLater1(PlatformImpl.java:294)
    at com.sun.javafx.application.PlatformImpl$$Lambda/1870453520.run(Unknown Source)
    at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.lambda$null5(WinApplication.java:101)
    at com.sun.glass.ui.win.WinApplication$$Lambda/123208387.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:745)

查找(这实际上是访问文本区域中滚动条的唯一方法)仅在该节点在场景图中呈现后才在该节点上工作(基本上,CSS 样式需要首先应用于节点)。所以在控制器的 initialize() 方法中使用它们是棘手的,因为该方法必须在 FXML 根返回给 FXMLLoader.load() 方法的调用者之前执行(因此在它被添加到场景图中之前) .

我认为发生的事情是 Platform.runLater(...) 在启动时成功,因为场景还没有被渲染,所以 Platform.runLater(...) 在等待 UI 处理(包括渲染第一个场景)。在以后的调用中,它可能仅取决于在当前帧渲染周期内何时执行(因此它看起来有些随机)。

对于您的特定用例(显示行号),我建议您查看使用 Tomas Mikula 的第三方库 RichTextFX

更普遍地执行此操作非常棘手。确保在执行代码之前渲染一帧(或两帧)的最安全方法是使用 AnimationTimer。这是一个class,它的handle()方法每渲染一帧就执行一次,而它是运行。所以基本的想法是计算帧数,一旦渲染了 1 个(或 2 个,如果你真的想安全的话)帧,然后进行查找(并停止动画计时器,因为它不再需要)。这有点 hack(但是,通常使用查找有点 hack)。

这是一个单独的示例 class(您可以对 FXML 和控制器使用相同的想法,为了简单起见,我只是这样做的):

import static java.util.stream.Collectors.toList;

import java.util.Random;
import java.util.stream.IntStream;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.Stage;

public class SynchronizedTextAreaScrolling extends Application {

    private static final Random RNG = new Random();

    @Override
    public void start(Stage primaryStage) {
        TabPane tabs = new TabPane();
        Tab tab = new Tab("Tab 1");
        tab.setContent(createTextAreas());
        tabs.getTabs().add(tab);

        Button addTab = new Button("New");
        addTab.setOnAction(e -> {
            Tab newTab = new Tab("Tab "+(tabs.getTabs().size()+1));
            newTab.setContent(createTextAreas());
            tabs.getTabs().add(newTab);
            tabs.getSelectionModel().select(newTab);
        });

        BorderPane root = new BorderPane(tabs, null, null, addTab, null);
        primaryStage.setScene(new Scene(root, 600, 400));
        primaryStage.show();
    }

    private HBox createTextAreas() {
        TextArea lineNumbers = new TextArea();
        int numLines = RNG.nextInt(20) + 20 ;
        lineNumbers.setText(String.join("\n", IntStream.rangeClosed(1, numLines).mapToObj(Integer::toString).collect(toList())));

        TextArea text = new TextArea();
        text.setText(String.join("\n", IntStream.rangeClosed(1, numLines).mapToObj(i -> "Line "+i).collect(toList())));

        lineNumbers.setMinWidth(40);
        lineNumbers.setPrefWidth(60);

        HBox.setHgrow(lineNumbers, Priority.NEVER);
        HBox.setHgrow(text, Priority.ALWAYS);

        // An AnimationTimer, whose handle(...) method will be invoked once
        // on each frame pulse (i.e. each rendering of the scene graph)
        AnimationTimer timer = new AnimationTimer() {

            // count number of frames rendered since the timer was started:
            private int frameCount = 0 ;

            @Override
            public void handle(long now) {
                frameCount++ ;

                // wait for the second frame. This should ensure that
                // the text areas have been rendered to the scene and so
                // they have had CSS applied. This allows us to use
                // lookups on them
                if (frameCount >= 2) {
                    ScrollBar sb1 = (ScrollBar) lineNumbers.lookup(".scroll-bar:vertical");
                    ScrollBar sb2 = (ScrollBar) text.lookup(".scroll-bar:vertical");
                    sb1.valueProperty().bindBidirectional(sb2.valueProperty());

                    // this animation timer is no longer needed, so stop it:
                    stop();
                }
            }
        };
        timer.start();

        return new HBox(lineNumbers, text);
    }

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

同样,对于这个特定用途,我会查看 RichTextFX