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。
我正在开发 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。