在 JavaFX 中删除窗格后如何调整手风琴的大小
How to resize accordion after removing a pane in JavaFX
我正在处理一个小型 JavaFX 项目。在我的一个场景中,我想动态添加和删除我实现的自定义组件,它分别从 TitledPane 扩展到 Accordion。这一切都很好,但是从 Accordion 中删除窗格后,该死的东西不会立即调整大小 ,但只有在我单击 gui 上的某个地方之后。我准备了下面的 GIF 图像来向您展示问题。
有人能告诉我为什么手风琴只在我点击 gui 界面上的某处后才调整大小,而不是立即调整吗?我的意思是自动调整大小似乎不是问题,它只是不会触发...有人可以告诉我为什么会这样吗?也许这是显而易见的,但我对 JavaFX 不是很熟悉,所以我真的被困在这里了。我还观察到其他组件的类似行为,所以我可能在这里遗漏了一些根本性的东西。
更新
好的,我为您创建了一个最小示例来重现我的问题。您可以在 GitHub javafx-demo 上克隆存储库并亲自尝试。这样做我注意到,手风琴只有在我点击它时才会调整大小,而不是当我点击 gui 上的其他任何地方时。
更新 1
我进一步简化了示例。您可以在上面的 GitHub 存储库中找到示例,或者查看下面的代码:
应用程序
public class App extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(App.class.getResource("parentView.fxml"));
Scene scene = new Scene(fxmlLoader.load(), 640, 480);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
父控制器
public class ParentController {
@FXML
private Accordion accordion;
public void onAddAction() {
var itemControl = new ItemControl();
EventHandler<ActionEvent> removeEventHandler = event -> {
accordion.getPanes().remove(itemControl);
};
itemControl.setOnRemoveProperty(removeEventHandler);
accordion.getPanes().add(itemControl);
}
}
父视图
<StackPane xmlns="http://javafx.com/javafx/16"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="org.example.ParentController">
<Group StackPane.alignment="CENTER">
<VBox>
<Accordion fx:id="accordion"/>
<Button onAction="#onAddAction" text="Add"/>
</VBox>
</Group>
</StackPane>
ItemControl
public class ItemControl extends TitledPane {
private final UUID id = UUID.randomUUID();
private final ObjectProperty<EventHandler<ActionEvent>> onRemoveProperty = new SimpleObjectProperty<>();
@FXML
private Button removeButton;
public ItemControl() {
FXMLLoader fxmlLoader = new FXMLLoader(ItemControl.class.getResource("itemControl.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException e) {
e.printStackTrace();
}
}
@FXML
public void initialize() {
removeButton.onActionProperty().bind(onRemoveProperty);
}
public void setOnRemoveProperty(EventHandler<ActionEvent> onRemoveProperty) {
this.onRemoveProperty.set(onRemoveProperty);
}
// equals and hashCode omitted for brevity (id instance variable is used as identifier)
}
ItemControl FXML
<fx:root type="javafx.scene.control.TitledPane" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
<VBox>
<Button fx:id="removeButton" text="Remove"/>
</VBox>
</fx:root>
该行为是 AccordionSkin 中的错误。技术原因是它保留了对当前和先前扩展窗格的内部引用 - 两者都用于计算 min/pref 高度 - 在删除扩展窗格时未正确更新。如果窗格不再是手风琴的一部分,解决方法是使这些引用无效,f.i。从皮肤的侦听器到窗格列表。
没有干净的方法来解决这个问题,因为所有涉及的 fields/methods 都是私有的 - 但是,如果允许我们弄脏,我们可以通过反射来解决这个错误。
基础知识:
- 扩展 AccordionSkin 并让我们的手风琴使用扩展版本
- 在皮肤中,在返回 super
之前覆盖 computeMin/Pref/Height 到 check/fix 的引用
- 检查:窗格应包含在手风琴的窗格中
- 修复:如果不是,将引用设置为空
备注:
- 对内部字段的反射访问需要在运行时打开包
- 通常要注意:tweaking/relying 内部实现高度依赖于版本并且 might/will 最终会中断
- FXUtils 是我用于反射的本地实用程序class,您必须将其替换为您自己的实现
代码:
public class SimpleLayoutAccordionOnRemove extends Application {
/**
* AccordionSkin that hacks the broken layout after remove of expanded pane.
*/
public static class HackedAccordionSkin extends AccordionSkin {
public HackedAccordionSkin(Accordion control) {
super(control);
}
@Override
protected double computeMinHeight(double width, double topInset, double rightInset,
double bottomInset, double leftInset) {
checkPaneFields();
return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
}
@Override
protected double computePrefHeight(double width, double topInset, double rightInset,
double bottomInset, double leftInset) {
checkPaneFields();
return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
}
private void checkPaneFields() {
checkPaneField("previousPane");
checkPaneField("expandedPane");
}
/**
* Check if the pane referenced by the field with the given name is contained
* in the accordion's panes and sets it to null if not.
*/
private void checkPaneField(String fieldName) {
TitledPane prev = (TitledPane) FXUtils.invokeGetFieldValue(AccordionSkin.class, this, fieldName);
if (!getSkinnable().getPanes().contains(prev)) {
FXUtils.invokeSetFieldValue(AccordionSkin.class, this, fieldName, null);
}
}
}
private Parent createContent() {
Accordion accordion = new Accordion() {
@Override
protected Skin<?> createDefaultSkin() {
return new HackedAccordionSkin(this);
}
};
Button add = new Button("add");
add.setOnAction(e -> {
addTitledPane(accordion);
});
VBox accBox = new VBox(accordion, add);
StackPane content = new StackPane(new Group(accBox));
return content;
}
int count;
private void addTitledPane(Accordion accordion) {
TitledPane pane = new TitledPane();
pane.setText("Pane " + count++);
Button remove = new Button("remove");
remove.setOnAction(e -> {
accordion.getPanes().remove(pane);
});
VBox box = new VBox(remove);
pane.setContent(box);
accordion.getPanes().add(pane);
}
@Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent(), 600, 400));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
我正在处理一个小型 JavaFX 项目。在我的一个场景中,我想动态添加和删除我实现的自定义组件,它分别从 TitledPane 扩展到 Accordion。这一切都很好,但是从 Accordion 中删除窗格后,该死的东西不会立即调整大小 ,但只有在我单击 gui 上的某个地方之后。我准备了下面的 GIF 图像来向您展示问题。
有人能告诉我为什么手风琴只在我点击 gui 界面上的某处后才调整大小,而不是立即调整吗?我的意思是自动调整大小似乎不是问题,它只是不会触发...有人可以告诉我为什么会这样吗?也许这是显而易见的,但我对 JavaFX 不是很熟悉,所以我真的被困在这里了。我还观察到其他组件的类似行为,所以我可能在这里遗漏了一些根本性的东西。
更新
好的,我为您创建了一个最小示例来重现我的问题。您可以在 GitHub javafx-demo 上克隆存储库并亲自尝试。这样做我注意到,手风琴只有在我点击它时才会调整大小,而不是当我点击 gui 上的其他任何地方时。
更新 1
我进一步简化了示例。您可以在上面的 GitHub 存储库中找到示例,或者查看下面的代码:
应用程序
public class App extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(App.class.getResource("parentView.fxml"));
Scene scene = new Scene(fxmlLoader.load(), 640, 480);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
父控制器
public class ParentController {
@FXML
private Accordion accordion;
public void onAddAction() {
var itemControl = new ItemControl();
EventHandler<ActionEvent> removeEventHandler = event -> {
accordion.getPanes().remove(itemControl);
};
itemControl.setOnRemoveProperty(removeEventHandler);
accordion.getPanes().add(itemControl);
}
}
父视图
<StackPane xmlns="http://javafx.com/javafx/16"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="org.example.ParentController">
<Group StackPane.alignment="CENTER">
<VBox>
<Accordion fx:id="accordion"/>
<Button onAction="#onAddAction" text="Add"/>
</VBox>
</Group>
</StackPane>
ItemControl
public class ItemControl extends TitledPane {
private final UUID id = UUID.randomUUID();
private final ObjectProperty<EventHandler<ActionEvent>> onRemoveProperty = new SimpleObjectProperty<>();
@FXML
private Button removeButton;
public ItemControl() {
FXMLLoader fxmlLoader = new FXMLLoader(ItemControl.class.getResource("itemControl.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException e) {
e.printStackTrace();
}
}
@FXML
public void initialize() {
removeButton.onActionProperty().bind(onRemoveProperty);
}
public void setOnRemoveProperty(EventHandler<ActionEvent> onRemoveProperty) {
this.onRemoveProperty.set(onRemoveProperty);
}
// equals and hashCode omitted for brevity (id instance variable is used as identifier)
}
ItemControl FXML
<fx:root type="javafx.scene.control.TitledPane" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
<VBox>
<Button fx:id="removeButton" text="Remove"/>
</VBox>
</fx:root>
该行为是 AccordionSkin 中的错误。技术原因是它保留了对当前和先前扩展窗格的内部引用 - 两者都用于计算 min/pref 高度 - 在删除扩展窗格时未正确更新。如果窗格不再是手风琴的一部分,解决方法是使这些引用无效,f.i。从皮肤的侦听器到窗格列表。
没有干净的方法来解决这个问题,因为所有涉及的 fields/methods 都是私有的 - 但是,如果允许我们弄脏,我们可以通过反射来解决这个错误。
基础知识:
- 扩展 AccordionSkin 并让我们的手风琴使用扩展版本
- 在皮肤中,在返回 super 之前覆盖 computeMin/Pref/Height 到 check/fix 的引用
- 检查:窗格应包含在手风琴的窗格中
- 修复:如果不是,将引用设置为空
备注:
- 对内部字段的反射访问需要在运行时打开包
- 通常要注意:tweaking/relying 内部实现高度依赖于版本并且 might/will 最终会中断
- FXUtils 是我用于反射的本地实用程序class,您必须将其替换为您自己的实现
代码:
public class SimpleLayoutAccordionOnRemove extends Application {
/**
* AccordionSkin that hacks the broken layout after remove of expanded pane.
*/
public static class HackedAccordionSkin extends AccordionSkin {
public HackedAccordionSkin(Accordion control) {
super(control);
}
@Override
protected double computeMinHeight(double width, double topInset, double rightInset,
double bottomInset, double leftInset) {
checkPaneFields();
return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
}
@Override
protected double computePrefHeight(double width, double topInset, double rightInset,
double bottomInset, double leftInset) {
checkPaneFields();
return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
}
private void checkPaneFields() {
checkPaneField("previousPane");
checkPaneField("expandedPane");
}
/**
* Check if the pane referenced by the field with the given name is contained
* in the accordion's panes and sets it to null if not.
*/
private void checkPaneField(String fieldName) {
TitledPane prev = (TitledPane) FXUtils.invokeGetFieldValue(AccordionSkin.class, this, fieldName);
if (!getSkinnable().getPanes().contains(prev)) {
FXUtils.invokeSetFieldValue(AccordionSkin.class, this, fieldName, null);
}
}
}
private Parent createContent() {
Accordion accordion = new Accordion() {
@Override
protected Skin<?> createDefaultSkin() {
return new HackedAccordionSkin(this);
}
};
Button add = new Button("add");
add.setOnAction(e -> {
addTitledPane(accordion);
});
VBox accBox = new VBox(accordion, add);
StackPane content = new StackPane(new Group(accBox));
return content;
}
int count;
private void addTitledPane(Accordion accordion) {
TitledPane pane = new TitledPane();
pane.setText("Pane " + count++);
Button remove = new Button("remove");
remove.setOnAction(e -> {
accordion.getPanes().remove(pane);
});
VBox box = new VBox(remove);
pane.setContent(box);
accordion.getPanes().add(pane);
}
@Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent(), 600, 400));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}