Java Swing实现点击按钮切换JPopupMenu显隐(类JMenu行为)的优化问题
需求
开发Java Swing应用时需要实现类似JMenu的交互效果:点击按钮弹出JPopupMenu,再次点击同一按钮则隐藏菜单。
现有问题
初始实现逻辑存在缺陷:第二次点击按钮时,弹窗会被Swing底层逻辑自动关闭,导致无法直接通过判断弹窗可见性完成切换逻辑。原有依赖EventQueue.getCurrentEvent()判断事件来源的方案依赖底层事件队列实现,稳定性不足。
初始实现代码
import java.awt.BorderLayout; import java.awt.Component; import java.awt.event.ActionEvent; import javax.swing.AbstractAction; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; @SuppressWarnings("serial") public class PopupAction extends AbstractAction { private final JPopupMenu popup; public PopupAction(String text, JPopupMenu popup) { super(text); this.popup = popup; } @Override public void actionPerformed(ActionEvent e) { if (e.getSource() instanceof Component && !popup.isVisible()) { Component c = (Component) e.getSource(); popup.show(c, 0, c.getHeight()); } } public static void main(String[] args) { SwingUtilities.invokeLater(PopupAction::createTestUI); } private static void createTestUI() { JFrame frm = new JFrame("Test popup"); JPopupMenu popup = new JPopupMenu(); popup.add("Item 1"); popup.add("Item 2"); popup.add("Item 3"); JPanel p = new JPanel(); p.add(new JButton(new PopupAction("Items", popup))); frm.add(p, BorderLayout.NORTH); frm.setSize(500, 300); frm.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); frm.setLocationRelativeTo(null); frm.setVisible(true); } }
原有稳定性不足的实现方案
import java.awt.AWTEvent; import java.awt.BorderLayout; import java.awt.Component; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.MouseEvent; import javax.swing.AbstractAction; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; @SuppressWarnings("serial") public class PopupAction extends AbstractAction { private final JPopupMenu popup; private boolean ignoreNextAction; private final PopupMenuListener pml = new PopupMenuListener() { @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { // do nothing } @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { onClose(); } @Override public void popupMenuCanceled(PopupMenuEvent e) { onClose(); } private void onClose() { AWTEvent e = EventQueue.getCurrentEvent(); if (e instanceof MouseEvent && ((MouseEvent) e).getComponent() instanceof JButton) { MouseEvent me = (MouseEvent) e; JButton b = (JButton) me.getComponent(); if (PopupAction.this.equals(b.getAction()) && me.getID() == MouseEvent.MOUSE_PRESSED) { ignoreNextAction = true; } } } }; public PopupAction(String text, JPopupMenu popup) { super(text); this.popup = popup; popup.addPopupMenuListener(pml); } @Override public void actionPerformed(ActionEvent e) { if (e.getSource() instanceof Component && !ignoreNextAction) { Component c = (Component) e.getSource(); popup.show(c, 0, c.getHeight()); } ignoreNextAction = false; } public static void main(String[] args) { SwingUtilities.invokeLater(PopupAction::createTestUI); } private static void createTestUI() { JFrame frm = new JFrame("Test popup"); JPopupMenu popup = new JPopupMenu(); popup.add("Item 1"); popup.add("Item 2"); popup.add("Item 3"); JPanel p = new JPanel(); p.add(new JButton(new PopupAction("Items", popup))); frm.add(p, BorderLayout.NORTH); frm.setSize(500, 300); frm.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); frm.setLocationRelativeTo(null); frm.setVisible(true); } }
可靠实现方案
采用JToggleButton本身自带选中状态切换能力,结合PopupMenuListener实现状态同步,完全不依赖事件队列的底层判断,稳定性极高:
import java.awt.BorderLayout; import java.awt.event.ActionEvent; import javax.swing.AbstractAction; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JToggleButton; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; @SuppressWarnings("serial") public class TogglePopupAction extends AbstractAction { private final JPopupMenu popup; private JToggleButton triggerBtn; // 防止状态同步循环标记 private boolean syncingState = false; private final PopupMenuListener popupListener = new PopupMenuListener() { @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) {} @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { // 同步按钮选中状态 if (triggerBtn != null && triggerBtn.isSelected() && !syncingState) { syncingState = true; triggerBtn.setSelected(false); syncingState = false; } } @Override public void popupMenuCanceled(PopupMenuEvent e) { popupMenuWillBecomeInvisible(e); } }; public TogglePopupAction(String text, JPopupMenu popup) { super(text); this.popup = popup; popup.addPopupMenuListener(popupListener); } @Override public void actionPerformed(ActionEvent e) { if (!(e.getSource() instanceof JToggleButton) || syncingState) { return; } triggerBtn = (JToggleButton) e.getSource(); syncingState = true; if (triggerBtn.isSelected()) { // 选中状态显示弹窗 popup.show(triggerBtn, 0, triggerBtn.getHeight()); } else { // 取消选中状态隐藏弹窗 popup.setVisible(false); } syncingState = false; } public static void main(String[] args) { SwingUtilities.invokeLater(TogglePopupAction::createTestUI); } private static void createTestUI() { JFrame frm = new JFrame("Test popup"); JPopupMenu popup = new JPopupMenu(); popup.add("Item 1"); popup.add("Item 2"); popup.add("Item 3"); JPanel p = new JPanel(); JToggleButton btn = new JToggleButton(new TogglePopupAction("Items", popup)); // 去掉默认的选中高亮,保持和普通按钮一致的视觉效果 btn.setFocusPainted(false); p.add(btn); frm.add(p, BorderLayout.NORTH); frm.setSize(500, 300); frm.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); frm.setLocationRelativeTo(null); frm.setVisible(true); } }
方案优势
- 完全基于Swing公开API实现,不依赖底层事件队列的内部逻辑,兼容性和稳定性高
- 交互逻辑和视觉状态完全同步,点击按钮、点击外部区域、按ESC关闭弹窗都会正确同步按钮的选中状态
- 可以通过修改
JToggleButton的UI属性,保持和普通JButton完全一致的视觉效果,用户无感知
内容的提问来源于stack exchange,提问作者Sergiy Medvynskyy
相关产品推荐
相关产品推荐

