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
相关产品推荐
相关产品推荐

