You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaFX自定义ComboBox皮肤的Add按钮设禁用触发StackOverflowError

解决JavaFX ComboBox自定义Skin中按钮禁用触发StackOverflowError的问题

问题重现

在JavaFX 19.0.2.1、Windows 10(22H2)环境下,通过ComboBoxListViewSkin给ComboBox添加了包含TextField和"+Add"按钮的自定义区域:

  • 按钮在TextField内容非空时启用,提交标签后设为禁用
  • 第一次点击按钮添加标签正常,但第二次及以后点击时,执行buttonAdd.setDisable(true)会触发StackOverflowError,错误日志显示与Windows平台的Accessible通知循环有关
  • 但通过TextField按Enter触发buttonAdd.fire()时无此问题

复现代码如下:

package org.example;

import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.control.skin.ComboBoxListViewSkin;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class App extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        ComboBox<String> comboBox = new ComboBox<>();
        ComboBoxListViewSkin<String> comboBoxListViewSkin = new ComboBoxListViewSkin<String>(comboBox) {
            private Button buttonAdd = new Button("+Add");
            private TextField textField = new TextField();
            private VBox pane = new VBox();
            {
                buttonAdd.setDisable(true);
                buttonAdd.setOnAction(event -> {
                    String text = textField.getText();
                    if (text.isBlank()) return;
                    comboBox.getItems().add(text);
                    textField.clear();
                    buttonAdd.setDisable(true);
                });
                textField.setOnKeyTyped(event -> {
                    buttonAdd.setDisable(textField.getText().isBlank());
                });
                textField.setOnAction(event -> buttonAdd.fire());
            }

            @Override
            public Node getPopupContent() {
                Node defaultContent = super.getPopupContent();
                // 每次调用都重新创建HBox并添加控件,导致UI刷新循环
                HBox hBox = new HBox();
                hBox.getChildren().addAll(buttonAdd, textField);
                HBox.setHgrow(textField, Priority.ALWAYS);
                defaultContent.setManaged(true);
                pane.getChildren().setAll(defaultContent, hBox);
                return pane;
            }
        };
        comboBox.setSkin(comboBoxListViewSkin);
        comboBox.setPrefWidth(400);
        comboBox.setPrefHeight(30);
        VBox vBox = new VBox(comboBox);

        Scene scene = new Scene(vBox, 400, 100);
        stage.setScene(scene);
        stage.show();
    }

    public static final void main(String[] args) {
        launch(args);
    }
}

错误日志片段:

Exception in thread "JavaFX Application Thread" java.lang.StackOverflowError
    at com.sun.glass.ui.Accessible$GetAttribute.run(Accessible.java:144)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    at com.sun.glass.ui.Accessible.lambda$getAttribute$0(Accessible.java:172)
    at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:424)
    at com.sun.glass.ui.Accessible.getAttribute(Accessible.java:169)
    at com.sun.glass.ui.win.WinAccessible.sendNotification(WinAccessible.java:273)
    at com.sun.glass.ui.win.WinAccessible.sendNotification(WinAccessible.java:281)
// ...

问题原因

核心问题在于getPopupContent()方法的实现:每次弹窗需要显示时,该方法都会重新创建HBox并将按钮、TextField添加进去,同时重置pane的子节点。当点击按钮修改其禁用状态时,UI状态变化会触发Skin重新计算布局,导致getPopupContent()被反复调用,进而触发Windows平台的Accessible通知循环,最终引发栈溢出。

而通过TextField按Enter触发buttonAdd.fire()时,事件处理逻辑没有触发Skin的重复重建,因此不会出现循环。

修复方案

将HBox的创建、控件添加和布局配置移到初始化块中,getPopupContent()只负责更新默认弹窗内容到容器中,避免每次弹窗显示都重建布局结构:

package org.example;

import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.control.skin.ComboBoxListViewSkin;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class App extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        ComboBox<String> comboBox = new ComboBox<>();
        ComboBoxListViewSkin<String> comboBoxListViewSkin = new ComboBoxListViewSkin<String>(comboBox) {
            private Button buttonAdd = new Button("+Add");
            private TextField textField = new TextField();
            private VBox pane = new VBox();
            private HBox hBox = new HBox(); // 提前创建HBox

            {
                buttonAdd.setDisable(true);
                // 初始化时一次性配置HBox和控件
                hBox.getChildren().addAll(buttonAdd, textField);
                HBox.setHgrow(textField, Priority.ALWAYS);
                pane.getChildren().add(hBox); // 先添加自定义区域

                buttonAdd.setOnAction(event -> {
                    String text = textField.getText();
                    if (text.isBlank()) return;
                    comboBox.getItems().add(text);
                    textField.clear();
                    buttonAdd.setDisable(true);
                });
                textField.setOnKeyTyped(event -> {
                    buttonAdd.setDisable(textField.getText().isBlank());
                });
                textField.setOnAction(event -> buttonAdd.fire());
            }

            @Override
            public Node getPopupContent() {
                Node defaultContent = super.getPopupContent();
                defaultContent.setManaged(true);
                // 只更新默认内容的位置,不重建HBox
                pane.getChildren().set(0, defaultContent);
                return pane;
            }
        };
        comboBox.setSkin(comboBoxListViewSkin);
        comboBox.setPrefWidth(400);
        comboBox.setPrefHeight(30);
        VBox vBox = new VBox(comboBox);

        Scene scene = new Scene(vBox, 400, 100);
        stage.setScene(scene);
        stage.show();
    }

    public static final void main(String[] args) {
        launch(args);
    }
}

验证效果

修改后,多次点击"+Add"按钮添加标签,按钮禁用状态正常切换,不会再触发StackOverflowError,功能完全符合预期。

内容的提问来源于stack exchange,提问作者cheily

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 15:36:03