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

}