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

Java Swing实现点击按钮切换JPopupMenu显隐(类JMenu行为)的优化问题

Java Swing 实现按钮点击切换JPopupMenu显隐的可靠方案

需求

开发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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.23 17:15:07