简体   繁体   中英

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

I am developing IDE and I just stucked in the following problem:

I want to create TabPane, load into some place in the program (to VBox), then add TAB on use some addFile hanler and BUILD TAB content by code_workspace.fxml file (That all is working for now). But I just want to synchronize two scrollbars of the elements that are stored in the same FXML file. Concretely that is scrollbar of ScrollPane (code_line_counter) and TextArea (code_text_area).

I had one year of studying Java, so I welcome every advice you can give me. As you can see, my english isn't very good, so I suppose you will read a lot of information from a code:

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>

I tried a lot of moving with that code section:

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

But nothing help me. I tried also remove Platform.runLater(() -> {}); , but it also didn't help.

Guys, what is the right solution of this problem, please?

*ADDITION: There is very interesting behavioral - when I add first tab, it runs normally, but when I am adding second or more tab into program collection, in about the 70% of cases took me the following exception:

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)

Lookups (which are really the only way to access the scroll bar in a text area) only work on a node after that node has been rendered in the scene graph (basically, CSS styles need to be applied to the node first). So using them in a controller's initialize() method is tricky, because that method is necessarily executed before the FXML root is returned to the caller of the FXMLLoader.load() method (and so before it is added to the scene graph).

I think what is happening is that the Platform.runLater(...) is successful at start up, because the scene hasn't yet been rendered at all, so the Platform.runLater(...) schedules the runnable after pending UI processing (which includes rendering the first scene). In later invocations it probably just depends on when this happens to be executed within the current frame rendering cycle (so it appears somewhat random).

For your particular use case (displaying line numbers), I recommend you look at using the third party library RichTextFX by Tomas Mikula.

To do this more generally is pretty tricky. The safest way to ensure that a frame (or two) have been rendered before executing code is to use an AnimationTimer . This is a class whose handle() method is executed once every time a frame is rendered, while it is running. So the basic idea is to count frames, and once 1 (or 2, if you want to be really safe) frames have been rendered, then do the lookup (and stop the animation timer, since it's no longer needed). This is a bit of a hack (but then, using lookups in general is a bit of a hack).

Here is an example as a single class (you can use the same idea with FXML and a controller, I just did it this way for simplicity):

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

Again, though, for this specific use, I would look at RichTextFX .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM