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

时间:2015-05-20 18:05:04

标签: java javafx scrollbar bind fxml

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

我想创建TabPane,加载到程序中的某个地方(到VBox),然后通过code_workspace.fxml文件添加TAB使用一些addFile hanler和BUILD 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(() -> {});,但它也没有帮助。

伙计们,请问这个问题的正确解决方案是什么?

* ADDITION: 有一个非常有趣的行为 - 当我添加第一个标签时,它运行正常,但是当我在程序集中添加第二个或更多标签时,大约70%的情况下我带来了以下异常:

Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
    at sample.FileManager.lambda$initialize$1(FileManager.java:75)
    at sample.FileManager$$Lambda$294/1491808102.run(Unknown Source)
    at com.sun.javafx.application.PlatformImpl.lambda$null$170(PlatformImpl.java:295)
    at com.sun.javafx.application.PlatformImpl$$Lambda$50/1472939626.run(Unknown Source)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.application.PlatformImpl.lambda$runLater$171(PlatformImpl.java:294)
    at com.sun.javafx.application.PlatformImpl$$Lambda$49/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$null$145(WinApplication.java:101)
    at com.sun.glass.ui.win.WinApplication$$Lambda$38/123208387.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:745)

1 个答案:

答案 0 :(得分:2)

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

我认为正在发生的事情是Platform.runLater(...)在启动时成功,因为场景尚未完全渲染,因此Platform.runLater(...)在挂起的UI处理后调度runnable(包括渲染第一个场景)。在以后的调用中,它可能只取决于何时在当前帧渲染周期内执行此操作(因此它看起来有点随机)。

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

要做到这一点,更普遍是非常棘手的。确保在执行代码之前渲染帧(或两个)的最安全方法是使用AnimationTimer。这是一个类,handle()方法在每次渲染帧时执行一次,同时它正在运行。所以基本的想法是计算帧数,一旦1(或2,如果你想要真正安全)帧已被渲染,然后进行查找(并停止动画计时器,因为它不再需要)。这有点像黑客攻击(但是,使用查找通常是一种黑客攻击)。

以下是一个单独的类示例(您可以使用与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