JavaFX Spinner弹出Alert后持续自动增减问题及解决方案咨询
这个问题的根源很明确:当你点击Spinner的增减按钮并调用Alert.showAndWait()时,鼠标依然处于按下状态。JavaFX的Spinner组件默认会对长按按钮触发连续的增减调用——即使Alert挡住了界面,这个长按事件的逻辑还在后台持续运行,导致Alert关闭后,Spinner会疯狂执行增减操作,陷入循环。
下面给你两种可靠的解决方案,结合你的代码来调整:
方案1:拦截鼠标事件,控制首次触发逻辑
最直接的方法是绕过重写increment/decrement的思路,转而拦截Spinner按钮的鼠标按下事件,在首次触发时显示Alert,同时阻止默认的长按连续调用逻辑。
修改你的Spinner1类,添加鼠标事件过滤器:
public class Spinner1 extends TimeSpinner { private boolean warningDisplayed = false; public Spinner1() { // 获取Spinner的递增/递减按钮(通过CSS选择器) Node incrementBtn = lookup(".increment-arrow-button"); Node decrementBtn = lookup(".decrement-arrow-button"); // 给按钮绑定鼠标事件处理逻辑 setupButtonEventFilter(incrementBtn, true); setupButtonEventFilter(decrementBtn, false); } private void setupButtonEventFilter(Node button, boolean isIncrement) { button.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> { if (!warningDisplayed) { // 标记警告已显示,避免重复弹出 warningDisplayed = true; // 弹出Alert Alert alert = new Alert(AlertType.WARNING, SPINNER_1_MESSAGE); alert.setHeaderText(null); alert.showAndWait(); // 手动执行一次增减操作(因为我们拦截了这次鼠标事件) if (isIncrement) { super.increment(1); } else { super.decrement(1); } // 消费事件,阻止Spinner默认的长按连续调用逻辑 event.consume(); } // 如果警告已显示,让事件正常传递,Spinner默认的长按逻辑可以正常工作 }); } // 移除原有的displayPopUpOrNot方法,因为逻辑已经移到鼠标事件里了 }
同时,修改你的TimeSpinner类,去掉increment/decrement方法里的displayPopUpOrNot()调用:
public abstract class TimeSpinner extends Spinner<LocalTime> { @Override public void decrement(int steps) { try { getValueFactory().setValue(LocalTime.parse(getEditor().getText(), DateTimeFormatter.ofPattern(UtilitiesBean.FORMAT_TIMEONLY_12HR, Locale.ENGLISH))); super.decrement(steps); } catch (DateTimeParseException e) { getEditor().setText(UtilitiesBean.INSTANCE.formatLocalTime(LocalTime.now(), UtilitiesBean.FORMAT_TIMEONLY_12HR)); getValueFactory().setValue(LocalTime.now()); } } @Override public void increment(int steps) { try { getValueFactory().setValue(LocalTime.parse(getEditor().getText(), DateTimeFormatter.ofPattern(UtilitiesBean.FORMAT_TIMEONLY_12HR, Locale.ENGLISH))); super.increment(steps); } catch (DateTimeParseException e) { getEditor().setText(UtilitiesBean.INSTANCE.formatLocalTime(LocalTime.now(), UtilitiesBean.FORMAT_TIMEONLY_12HR)); getValueFactory().setValue(LocalTime.now()); } } }
这个方案的优势是:
- 完全避免了长按触发的连续调用问题
- 首次点击后,后续的长按/点击操作都能正常使用Spinner的默认逻辑
方案2:临时禁用Spinner,避免事件传递
如果你不想修改鼠标事件,也可以在显示Alert时临时禁用Spinner,阻止后续事件触发,Alert关闭后再恢复并执行一次操作:
修改Spinner1的displayPopUpOrNot方法(注意需要修改方法参数来区分递增/递减):
public class Spinner1 extends TimeSpinner { private boolean warningDisplayed = false; @Override public void decrement(int steps) { displayPopUpOrNot(false, steps); } @Override public void increment(int steps) { displayPopUpOrNot(true, steps); } protected void displayPopUpOrNot(boolean isIncrement, int steps) { if (!warningDisplayed) { warningDisplayed = true; // 临时禁用Spinner,阻止后续事件 setDisable(true); Alert alert = new Alert(AlertType.WARNING, SPINNER_1_MESSAGE); alert.setHeaderText(null); alert.showAndWait().ifPresent(btnType -> { // 恢复Spinner可用状态 setDisable(false); // 执行本次的增减操作 if (isIncrement) { super.increment(steps); } else { super.decrement(steps); } }); } else { // 警告已显示,直接执行默认逻辑 if (isIncrement) { super.increment(steps); } else { super.decrement(steps); } } } }
这个方案的缺点是:如果用户在Alert弹出期间一直按住鼠标,Spinner恢复后可能还会触发一次连续调用,但比你的原始代码已经好很多。
为什么你的原始代码会无限循环?
当你调用alert.showAndWait()时,线程会阻塞等待Alert关闭,但Spinner的按钮长按逻辑是在JavaFX的事件队列里持续触发的——这些触发事件会在Alert关闭后一次性执行,导致你看到的“无限循环”现象。拦截事件或临时禁用Spinner,本质上都是切断了这些后续事件的传递。
内容的提问来源于stack exchange,提问作者Bob

