如何强制创建控件皮肤以便我可以使用 getParent?
How to force control skin to be created so I can use getParent?
在下面的最小示例中,当打印出 tabs
和 tabs2
的 parents 时,它们都为空。
从this question开始我明白了,因为虽然两个TabPanes已经添加到SplitPane中,但由于TabPanes的皮肤还没有创建,getScene
和getParent
将 return 为空。
所以问题是,我怎样才能在我试图打印出来的代码中访问他们的 parents?我想我需要强制创建皮肤?
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.SplitPane;
import javafx.scene.control.TabPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class MainApp extends Application {
@Override
public void start(Stage stage) throws Exception {
Button button = new Button("click");
StackPane root = new StackPane(button);
button.setOnAction( event -> {
SplitPane sp = new SplitPane();
TabPane tabs1 = new TabPane();
TabPane tabs2 = new TabPane();
sp.getItems().addAll(tabs1, tabs2);
root.getChildren().add(sp);
System.out.println(tabs1.getParent());
System.out.println(tabs2.getParent());
});
Scene scene = new Scene(root);
stage.setTitle("JavaFX and Gradle");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
您可以考虑尝试通过保留对父节点的引用来更改您的逻辑,而不是尝试依赖节点呈现。这样你就不需要依赖节点渲染了。
以下是我尝试过的更改,它按预期工作。
#改变1:
将下面的实例变量添加到 DetachableTabPane.java,让我们知道这是在哪个拆分窗格中设置的。这样你就不需要遍历所有节点来找到父 SplitPane。
private TabSplitPane parentSplitPane;
#改2:
创建一个自定义 SplitPane 并将其自身注册到其子项。因此,您不必担心每次将 DetachableTabPane 添加到 SplitPane 时都要设置它。
class TabSplitPane extends SplitPane {
public TabSplitPane() {
getItems().addListener((ListChangeListener) e -> {
if (e.next()) {
e.getAddedSubList().stream().filter(o -> o instanceof DetachableTabPane).forEach(tp -> ((DetachableTabPane) tp).parentSplitPane = TabSplitPane.this);
e.getRemoved().stream().filter(o -> o instanceof DetachableTabPane).forEach(tp -> ((DetachableTabPane) tp).parentSplitPane = null);
}
});
}
}
#改变3:
更新 placeTab 方法如下。通过这种方式,您可以直接处理与 DetachableTabPane 相关联的 SplitPane 实例,而不必担心何时呈现节点。
public void placeTab(Tab tab, Pos pos) {
boolean addToLast = pos == Pos.CENTER_RIGHT || pos == Pos.BOTTOM_CENTER;
DetachableTabPane dt = detachableTabPaneFactory.create(this);
dt.getTabs().add(tab);
Orientation requestedOrientation = Orientation.HORIZONTAL;
if (pos == Pos.BOTTOM_CENTER || pos == Pos.TOP_CENTER) {
requestedOrientation = Orientation.VERTICAL;
}
TabSplitPane targetSplitPane = parentSplitPane;
// If there is no splitPane parent... Create one!!
if (targetSplitPane == null) {
targetSplitPane = new TabSplitPane();
targetSplitPane.setOrientation(requestedOrientation);
Pane parent = (Pane) getParent();
int indexInParent = parent.getChildren().indexOf(DetachableTabPane.this);
parent.getChildren().remove(DetachableTabPane.this);
if (addToLast) {
targetSplitPane.getItems().addAll(DetachableTabPane.this, dt);
} else {
targetSplitPane.getItems().addAll(dt, DetachableTabPane.this);
}
parent.getChildren().add(indexInParent, targetSplitPane);
}
// If the orientation is changed... create a new split pane.
else if (targetSplitPane.getOrientation() != requestedOrientation) {
TabSplitPane parent = targetSplitPane;
int indexInParent = parent.getItems().indexOf(DetachableTabPane.this);
parent.getItems().remove(DetachableTabPane.this);
targetSplitPane = new TabSplitPane();
targetSplitPane.setOrientation(requestedOrientation);
if (addToLast) {
targetSplitPane.getItems().addAll(DetachableTabPane.this, dt);
} else {
targetSplitPane.getItems().addAll(dt, DetachableTabPane.this);
}
parent.getItems().add(indexInParent, targetSplitPane);
} else {
if (addToLast) {
parentSplitPane.getItems().add(dt);
} else {
int indexInParent = targetSplitPane.getItems().indexOf(DetachableTabPane.this);
parentSplitPane.getItems().add(indexInParent, dt);
}
}
}
Applying CSS and generating a layout pass 将强制创建由 CSS 或控件的皮肤管理的任何节点。
如果将布局传递应用程序应用于场景图的根,则它可以用于整个场景图。或者如果应用于场景图的子树,则仅更改(脏)的部分。
这是一个基于您问题中的代码的示例:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class MainApp extends Application {
@Override
public void start(Stage stage) throws Exception {
Button button = new Button("click");
StackPane root = new StackPane(button);
button.setOnAction(event -> {
SplitPane sp = new SplitPane();
TabPane tabs1 = new TabPane();
TabPane tabs2 = new TabPane();
sp.getItems().addAll(tabs1, tabs2);
root.getChildren().add(sp);
// force a layout pass.
sp.applyCss();
sp.layout();
System.out.println(tabs1.getParent());
System.out.println(tabs2.getParent());
});
Scene scene = new Scene(root);
stage.setTitle("JavaFX and Gradle");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
通过这些更改,输出选项卡窗格的父节点的代码打印出一个非空值(对封闭的拆分窗格 skin 的引用)。请务必注意,它不是对封闭的 SplitPane 控件实例的引用,而仅仅是控件(皮肤)的视觉表示。这是一个重要的区别。它回答了您的问题,但答案可能并不像您想象的那样直接适用于您的应用程序。
关于这种方法的建议
此处的其余部分是关于 API 在 JavaFX 中使用的背景信息和一般建议,因此如果与您的特定情况无关,请忽略。
一般来说,我建议单独保留重要项目之间关系的引用和结构,而不是依赖节点的 parent/child 关系。这样您就可以在更高级别(例如控件 类 本身)处理事物,而不是处理皮肤的底层节点和在皮肤内创建的节点(这是脆弱的关系,可能会在 JavaFX 版本和皮肤实现之间发生变化,破坏了你的代码)。
对于控件,它们通常有 API 来描述它们与场景图无关的关系。在可能的情况下使用这些比查询 parent/child 关系之类的场景图关系更可取。例如,您可以使用 [splitPane.getItems()] 调用获取拆分窗格的所有子项。或者,使用 [tabPane.getTabs()] 调用的 TabPane 中的所有选项卡。请注意,tab isn't even a node, so using the scene graph to try to find or manage it isn't appropriate. Similar object relationships outside of the scene graph are maintained for other controls, like menu items. Complex controls such as color pickers 可以包含具有弹出窗口的组合框,甚至在不同的弹出窗口中具有节点 windows,具有与父节点完全不同的场景图。
通常,如果您使用 FXML,重要的引用会被注入到控制器内的应用程序代码中,因此您不需要做额外的工作。如果您正在动态更改现有场景图的结构(这听起来像是您正在做的),那么您可能需要做更多的工作并创建一些自定义数据结构来管理引用并将它们与场景中的内容同步图。
在下面的最小示例中,当打印出 tabs
和 tabs2
的 parents 时,它们都为空。
从this question开始我明白了,因为虽然两个TabPanes已经添加到SplitPane中,但由于TabPanes的皮肤还没有创建,getScene
和getParent
将 return 为空。
所以问题是,我怎样才能在我试图打印出来的代码中访问他们的 parents?我想我需要强制创建皮肤?
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.SplitPane;
import javafx.scene.control.TabPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class MainApp extends Application {
@Override
public void start(Stage stage) throws Exception {
Button button = new Button("click");
StackPane root = new StackPane(button);
button.setOnAction( event -> {
SplitPane sp = new SplitPane();
TabPane tabs1 = new TabPane();
TabPane tabs2 = new TabPane();
sp.getItems().addAll(tabs1, tabs2);
root.getChildren().add(sp);
System.out.println(tabs1.getParent());
System.out.println(tabs2.getParent());
});
Scene scene = new Scene(root);
stage.setTitle("JavaFX and Gradle");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
您可以考虑尝试通过保留对父节点的引用来更改您的逻辑,而不是尝试依赖节点呈现。这样你就不需要依赖节点渲染了。
以下是我尝试过的更改,它按预期工作。
#改变1:
将下面的实例变量添加到 DetachableTabPane.java,让我们知道这是在哪个拆分窗格中设置的。这样你就不需要遍历所有节点来找到父 SplitPane。
private TabSplitPane parentSplitPane;
#改2:
创建一个自定义 SplitPane 并将其自身注册到其子项。因此,您不必担心每次将 DetachableTabPane 添加到 SplitPane 时都要设置它。
class TabSplitPane extends SplitPane {
public TabSplitPane() {
getItems().addListener((ListChangeListener) e -> {
if (e.next()) {
e.getAddedSubList().stream().filter(o -> o instanceof DetachableTabPane).forEach(tp -> ((DetachableTabPane) tp).parentSplitPane = TabSplitPane.this);
e.getRemoved().stream().filter(o -> o instanceof DetachableTabPane).forEach(tp -> ((DetachableTabPane) tp).parentSplitPane = null);
}
});
}
}
#改变3:
更新 placeTab 方法如下。通过这种方式,您可以直接处理与 DetachableTabPane 相关联的 SplitPane 实例,而不必担心何时呈现节点。
public void placeTab(Tab tab, Pos pos) {
boolean addToLast = pos == Pos.CENTER_RIGHT || pos == Pos.BOTTOM_CENTER;
DetachableTabPane dt = detachableTabPaneFactory.create(this);
dt.getTabs().add(tab);
Orientation requestedOrientation = Orientation.HORIZONTAL;
if (pos == Pos.BOTTOM_CENTER || pos == Pos.TOP_CENTER) {
requestedOrientation = Orientation.VERTICAL;
}
TabSplitPane targetSplitPane = parentSplitPane;
// If there is no splitPane parent... Create one!!
if (targetSplitPane == null) {
targetSplitPane = new TabSplitPane();
targetSplitPane.setOrientation(requestedOrientation);
Pane parent = (Pane) getParent();
int indexInParent = parent.getChildren().indexOf(DetachableTabPane.this);
parent.getChildren().remove(DetachableTabPane.this);
if (addToLast) {
targetSplitPane.getItems().addAll(DetachableTabPane.this, dt);
} else {
targetSplitPane.getItems().addAll(dt, DetachableTabPane.this);
}
parent.getChildren().add(indexInParent, targetSplitPane);
}
// If the orientation is changed... create a new split pane.
else if (targetSplitPane.getOrientation() != requestedOrientation) {
TabSplitPane parent = targetSplitPane;
int indexInParent = parent.getItems().indexOf(DetachableTabPane.this);
parent.getItems().remove(DetachableTabPane.this);
targetSplitPane = new TabSplitPane();
targetSplitPane.setOrientation(requestedOrientation);
if (addToLast) {
targetSplitPane.getItems().addAll(DetachableTabPane.this, dt);
} else {
targetSplitPane.getItems().addAll(dt, DetachableTabPane.this);
}
parent.getItems().add(indexInParent, targetSplitPane);
} else {
if (addToLast) {
parentSplitPane.getItems().add(dt);
} else {
int indexInParent = targetSplitPane.getItems().indexOf(DetachableTabPane.this);
parentSplitPane.getItems().add(indexInParent, dt);
}
}
}
Applying CSS and generating a layout pass 将强制创建由 CSS 或控件的皮肤管理的任何节点。
如果将布局传递应用程序应用于场景图的根,则它可以用于整个场景图。或者如果应用于场景图的子树,则仅更改(脏)的部分。
这是一个基于您问题中的代码的示例:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class MainApp extends Application {
@Override
public void start(Stage stage) throws Exception {
Button button = new Button("click");
StackPane root = new StackPane(button);
button.setOnAction(event -> {
SplitPane sp = new SplitPane();
TabPane tabs1 = new TabPane();
TabPane tabs2 = new TabPane();
sp.getItems().addAll(tabs1, tabs2);
root.getChildren().add(sp);
// force a layout pass.
sp.applyCss();
sp.layout();
System.out.println(tabs1.getParent());
System.out.println(tabs2.getParent());
});
Scene scene = new Scene(root);
stage.setTitle("JavaFX and Gradle");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
通过这些更改,输出选项卡窗格的父节点的代码打印出一个非空值(对封闭的拆分窗格 skin 的引用)。请务必注意,它不是对封闭的 SplitPane 控件实例的引用,而仅仅是控件(皮肤)的视觉表示。这是一个重要的区别。它回答了您的问题,但答案可能并不像您想象的那样直接适用于您的应用程序。
关于这种方法的建议
此处的其余部分是关于 API 在 JavaFX 中使用的背景信息和一般建议,因此如果与您的特定情况无关,请忽略。
一般来说,我建议单独保留重要项目之间关系的引用和结构,而不是依赖节点的 parent/child 关系。这样您就可以在更高级别(例如控件 类 本身)处理事物,而不是处理皮肤的底层节点和在皮肤内创建的节点(这是脆弱的关系,可能会在 JavaFX 版本和皮肤实现之间发生变化,破坏了你的代码)。
对于控件,它们通常有 API 来描述它们与场景图无关的关系。在可能的情况下使用这些比查询 parent/child 关系之类的场景图关系更可取。例如,您可以使用 [splitPane.getItems()] 调用获取拆分窗格的所有子项。或者,使用 [tabPane.getTabs()] 调用的 TabPane 中的所有选项卡。请注意,tab isn't even a node, so using the scene graph to try to find or manage it isn't appropriate. Similar object relationships outside of the scene graph are maintained for other controls, like menu items. Complex controls such as color pickers 可以包含具有弹出窗口的组合框,甚至在不同的弹出窗口中具有节点 windows,具有与父节点完全不同的场景图。
通常,如果您使用 FXML,重要的引用会被注入到控制器内的应用程序代码中,因此您不需要做额外的工作。如果您正在动态更改现有场景图的结构(这听起来像是您正在做的),那么您可能需要做更多的工作并创建一些自定义数据结构来管理引用并将它们与场景中的内容同步图。