首页 > 解决方案 > 带有复选框的 Swing JMenu

问题描述

我需要在 JMenu 中显示一个复选框(模仿 JCheckBoxMenuItem 的功能,但在父级别)。这个功能似乎不是 Swing API 的一部分,但我想我可以通过覆盖 JMenu API 来让它工作。

这就是我希望它通过我在 Paint 中拼凑的 Mockup 的样子:

https://i.stack.imgur.com/hd7XB.png

但我没有成功让复选框出现,我不知道如何强制这种行为。

这是一个代码示例,显示了我正在尝试做的事情。右键单击框架中的任意位置:

import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JPopupMenu;
import javax.swing.JToggleButton;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;

public class PopupMenuExample extends JFrame {
  JPopupMenu popupMenu;

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {

      @Override
      public void run() {

        for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
          if ("Nimbus".equals(info.getName())) {
            try {
              UIManager.setLookAndFeel(info.getClassName());
            } catch (Exception e) {
              e.printStackTrace();
            }
            break;
          }
        }

        PopupMenuExample frame = new PopupMenuExample ();

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.setSize(400, 400);
        frame.setVisible(true);
      }
    });

  }

  public PopupMenuExample () {

    popupMenu = new JPopupMenu();

    popupMenu.add(createMenu("Displayed Items"));
    this.addMouseListener(new MouseAdapter() {
      public void mouseReleased(MouseEvent Me) {
        if (Me.isPopupTrigger()) {
          popupMenu.show(Me.getComponent(), Me.getX(), Me.getY());
        }
      }
    });

  }

  public JMenu createMenu(String title) {
    JCheckBoxMenu m = new JCheckBoxMenu(title, true);

    JCheckBoxMenuItem tractorsMenuItem = new JCheckBoxMenuItem("Tractor", true);

    JCheckBoxMenu carsMenu = new JCheckBoxMenu("Cars", true);

    JCheckBoxMenuItem chevy = new JCheckBoxMenuItem("Chevy", true);
    JCheckBoxMenuItem fusion = new JCheckBoxMenuItem("Ford", true);
    JCheckBoxMenuItem tesla = new JCheckBoxMenuItem("Tesla", true);

    carsMenu.add(chevy);
    carsMenu.add(fusion);
    carsMenu.add(tesla);

    JCheckBoxMenuItem tankMenuItem = new JCheckBoxMenuItem("Tank", true);

    m.add(tractorsMenuItem);
    m.add(carsMenu);
    m.add(tankMenuItem);


    return m;
  }


  // Trying to mimic how JCheckBoxMenuItem works:
  // https://github.com/Himansu-Nayak/java7-sourcecode/blob/master/javax/swing/JCheckBoxMenuItem.java
  public static class JCheckBoxMenu extends JMenu {


    public JCheckBoxMenu(String text, boolean selected) {
      super(text);
      setModel(new JToggleButton.ToggleButtonModel());
      setSelected(selected);
      setFocusable(false);

    }
  }


}

标签: javaswing

解决方案


我必须同意@Andrew Thompson 关于将 aJMenu用于工作的原因,原因如下:

  1. 目前尚不清楚菜单旁边的复选标记的实际含义。(同样的问题实际上也适用于带有复选框的树)。
  2. 使用菜单结构的导航不是最佳的,例如,如果您不小心点击了错误的位置,菜单就会关闭。也与大多数 lafs 点击 aJMenuItem将实际上关闭整个JPopupMenu。如果您想切换多个项目,则不理想。

使用 aJTree来管理状态的建议解决方案是我采用的解决方案。因为JPopupMenus 无法聚焦,所以需要创建弹出窗口并显式处理。在解释过程中,我将引用我的答案末尾可以找到的每个实现类。

因为即使使用显示复选标记的 a ,它的确切含义也JTree可能模棱两可值(叶子只允许有or状态)。SELECTEDDELSELECTEDINDETERMINATEINDETERMINATESELECTEDDESELECTED

首先,我们需要树的模型。TristateNode这是自动管理子节点和父节点状态的简单实现。如果具有子节点的节点被选中/取消选中,所有下降节点将自动被选中/取消选中(TristateNodeTristateState)。

现在我们需要为树提供渲染器和编辑器。渲染器使用一个图标来显示当前TristateState节点。我目前使用纯色,但你可以做任何你喜欢的事情。编辑器实际上并没有做任何事情。每次发送编辑请求时,它都会迭代目标节点的状态,并告诉树不要编辑。这样我们就不必提供额外的编辑组件。( CheckboxTreeCheckBoxTreeRendererCheckBoxTreeEditor) TristateIcon

现在对于我们显示弹出窗口以显示树的部分。因为让弹出窗口始终可见可能很方便,所以我将在弹出窗口中添加一个“Pin”按钮,将其迁移到完整的JDialog. 如果弹出/对话框当前可见,我们只需将显示的组件切换为新组件(TreePopupManagerTreePopupContent)。

对于演示,我创建了一个简单的面板,其中包含 4 个可以使用鼠标选择的区域。通过使用鼠标右键,弹出窗口将显示在鼠标位置。单击其他任何地方将隐藏弹出窗口。如果弹出窗口当前被固定,则聚焦任何组件将自动更新弹出窗口内容。

这是它所需的所有代码:

import java.awt.*;
import java.awt.event.*;
import java.util.EventObject;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.CellEditorListener;
import javax.swing.tree.*;

public class Test {

    public static void main(final String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame();
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setContentPane(createContentPane());
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }

    private static JComponent createContentPane() {
        return new DemoContent();
    }

    private static void drawRect(final Graphics g, final int x, final int y, final int w, final int h) {
        g.fillRect(x, y, w, 1);
        g.fillRect(x, y, 1, h);
        g.fillRect(x + w - 1, y, 1, h);
        g.fillRect(x, y + h - 1, w, 1);
    }

    private static class TreePopupManager {

        private static final TreePopupManager INSTANCE = new TreePopupManager();

        private final TreePopupContent contentPanel = new TreePopupContent(this::pin);
        private Popup popup;
        private Component owner;
        private Point lastPos;
        private JDialog dialog;

        public TreePopupManager() {
            Toolkit.getDefaultToolkit().addAWTEventListener(e -> {
                if (popup != null) {
                    if (e instanceof FocusEvent) {
                        Component oppositeComp = ((FocusEvent) e).getOppositeComponent();
                        Component comp = ((FocusEvent) e).getComponent();
                        int id = e.getID();
                        if (id == FocusEvent.FOCUS_LOST) {
                            if (oppositeComp == null
                                || !SwingUtilities.isDescendingFrom(oppositeComp, contentPanel)) {
                                hidePopup();
                            }
                        } else if (id == FocusEvent.FOCUS_GAINED) {
                            if (comp == null
                                || !SwingUtilities.isDescendingFrom(comp, contentPanel)) {
                                hidePopup();
                            }
                        }
                    } else if (e instanceof MouseEvent && e.getID() == MouseEvent.MOUSE_PRESSED) {
                        Component component = ((MouseEvent) e).getComponent();
                        if (!SwingUtilities.isDescendingFrom(component, contentPanel)) {
                            hidePopup();
                        }
                    }
                }
            }, AWTEvent.FOCUS_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK);
        }

        private void pin() {
            hidePopup();
            if (dialog == null) {
                dialog = new JDialog(owner != null ? SwingUtilities.getWindowAncestor(owner) : null);
                dialog.setContentPane(contentPanel);
                contentPanel.setPinned(true);
                dialog.pack();
                dialog.setLocation(lastPos);
                dialog.setVisible(true);
            }
        }

        private void hidePopup() {
            if (popup != null) {
                popup.hide();
                popup = null;
            }
        }

        private void hideDialog() {
            if (dialog != null) {
                dialog.setVisible(false);
                dialog.dispose();
                dialog = null;
            }
        }

        public static TreePopupManager getInstance() {
            return INSTANCE;
        }

        public boolean isPopupVisible() {
            return popup != null || dialog != null;
        }

        public void showPopup(final Component parent, final JTree content, final Point p, final String title) {
            contentPanel.setTitle(title);
            contentPanel.setContent(content);
            if (dialog != null && !dialog.isVisible()) {
                hideDialog();
            }
            if (popup == null && dialog == null) {
                contentPanel.setPinned(false);
                lastPos = p;
                owner = parent;
                popup = PopupFactory.getSharedInstance().getPopup(parent, contentPanel, p.x, p.y);
                popup.show();
            }
        }
    }

    private static class TreePopupContent extends JPanel {

        private final JPanel view = new JPanel(new BorderLayout());
        private final JLabel label = new JLabel();
        private final JButton pinButton = new JButton("Pin");

        public TreePopupContent(final Runnable onPin) {
            super(new BorderLayout());
            JScrollPane scrollPane = new JScrollPane(view) {
                @Override
                public void setBorder(final Border border) {}
            };
            add(scrollPane);
            Box box = Box.createHorizontalBox();
            box.add(Box.createHorizontalStrut(5));
            box.add(label);
            box.add(Box.createHorizontalGlue());

            pinButton.setMargin(new Insets(0, 0, 0, 0));
            pinButton.setFocusable(false);
            pinButton.setFocusPainted(false);
            pinButton.addActionListener(e -> onPin.run());

            box.add(pinButton);
            box.add(Box.createHorizontalStrut(5));
            add(box, BorderLayout.NORTH);
        }

        public void setPinned(final boolean pinned) {
            pinButton.setVisible(!pinned);
        }

        @Override
        public void updateUI() {
            super.updateUI();
            setBorder(new JPopupMenu().getBorder());
        }

        public void setTitle(final String title) {
            label.setText(title);
        }

        public void setContent(final JComponent content) {
            setBackground(content.getBackground());
            view.removeAll();
            view.add(content);
            revalidate();
            repaint();
            Dimension pref = view.getPreferredSize();
            setPreferredSize(new Dimension(Math.max(pref.width, 100) + 10,
                                           Math.max(pref.height, 200) + 10));
        }
    }

    private static class DemoContent extends JPanel {

        private DemoContent() {
            setPreferredSize(new Dimension(300, 300));
            setLayout(new BorderLayout());
            setLayout(new GridLayout(2, 2));
            add(new DemoContentPanel(new CheckBoxTree(populate(new TristateNode("root1"), 1, 1))));
            add(new DemoContentPanel(new CheckBoxTree(populate(new TristateNode("root2"), 2, 2))));
            add(new DemoContentPanel(new CheckBoxTree(populate(new TristateNode("root3"), 3, 3))));
            add(new DemoContentPanel(new CheckBoxTree(populate(new TristateNode("root4"), 4, 4))));
        }

        private TristateNode populate(final TristateNode node, final int count, final int depth) {
            if (depth == 0) return node;
            for (int i = 0; i < count; i++) {
                node.add(populate(new TristateNode("Node-" + depth + "-" + i), count, depth - 1));
            }
            return node;
        }
    }

    private static class DemoContentPanel extends JPanel {

        private final CheckBoxTree tree;

        private DemoContentPanel(final CheckBoxTree tree) {
            this.tree = tree;
            setFocusable(true);
            addMouseListener(new MouseAdapter() {
                @Override
                public void mousePressed(final MouseEvent e) {
                    requestFocus();
                    onMousePressed(e);
                }
            });
            addFocusListener(new FocusListener() {
                @Override
                public void focusGained(final FocusEvent e) {
                    repaint();
                }

                @Override
                public void focusLost(final FocusEvent e) {
                    repaint();
                }
            });
        }

        @Override
        protected void paintComponent(final Graphics g) {
            super.paintComponent(g);
            if (hasFocus()) {
                g.setColor(Color.RED);
            } else {
                g.setColor(Color.BLACK);
            }
            drawRect(g, 0, 0, getWidth(), getHeight());
        }

        protected void onMousePressed(final MouseEvent e) {
            SwingUtilities.invokeLater(() -> {
                TreePopupManager manager = TreePopupManager.getInstance();
                if (SwingUtilities.isRightMouseButton(e) || manager.isPopupVisible()) {
                    Point p = e.getPoint();
                    SwingUtilities.convertPointToScreen(p, this);
                    manager.showPopup(this, tree, p, "Tree");
                }
            });
        }
    }

    private static class CheckBoxTree extends JTree {

        public CheckBoxTree(final TristateNode root) {
            super(root);
            setShowsRootHandles(true);
            setCellRenderer(new CheckBoxTreeRenderer());
            setCellEditor(new CheckBoxTreeEditor(this));
            setEditable(true);
        }
    }

    private static class CheckBoxTreeRenderer extends DefaultTreeCellRenderer {

        private final TristateIcon icon = new TristateIcon();

        @Override
        public Component getTreeCellRendererComponent(final JTree tree, final Object value, final boolean sel,
                                                      final boolean expanded, final boolean leaf,
                                                      final int row, final boolean hasFocus) {
            Component component = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
            JLabel label = (JLabel) component; // DefaultTreeCellRenderer uses JLabel as its renderer;
            TristateNode node = (TristateNode) value;
            label.setText(node.getLabel());
            label.setIcon(icon);
            icon.setState(node.getState());
            return label;
        }
    }

    private static class CheckBoxTreeEditor implements TreeCellEditor {

        private final JTree tree;

        public CheckBoxTreeEditor(final JTree tree) {
            this.tree = tree;
        }

        @Override
        public Component getTreeCellEditorComponent(final JTree tree, final Object value,
                                                    final boolean isSelected,
                                                    final boolean expanded,
                                                    final boolean leaf, final int row) {
            return null;
        }

        @Override
        public Object getCellEditorValue() {
            return ((TristateNode) tree.getEditingPath().getLastPathComponent()).getState();
        }

        @Override
        public boolean isCellEditable(final EventObject anEvent) {
            if (!(anEvent instanceof InputEvent)) return false;
            InputEvent event = (InputEvent) anEvent;
            Object source = event.getSource();
            if (!(source instanceof JTree)) return false;
            JTree tree = (JTree) source;
            Object value = null;
            if (event instanceof MouseEvent) {
                Point p = ((MouseEvent) event).getPoint();
                TreePath path = tree.getPathForLocation(p.x, p.y);
                if (path != null) {
                    value = path.getLastPathComponent();
                }
            } else {
                value = tree.getLeadSelectionPath().getLastPathComponent();
            }
            if (value instanceof TristateNode) {
                ((TristateNode) value).iterateState();
                tree.repaint();
            }
            return false;
        }

        @Override
        public boolean shouldSelectCell(final EventObject anEvent) {
            return false;
        }

        @Override
        public boolean stopCellEditing() {
            return false;
        }

        @Override
        public void cancelCellEditing() {}

        @Override
        public void addCellEditorListener(final CellEditorListener l) {}

        @Override
        public void removeCellEditorListener(final CellEditorListener l) {}
    }

    private static class TristateNode extends DefaultMutableTreeNode {
        private String label;

        public TristateNode() {
            this(null);
        }

        public TristateNode(final String label) {
            this(label, TristateState.DESELECTED);
        }

        public TristateNode(final String label, final TristateState state) {
            this(label, state, true);
        }

        public TristateNode(final String label, final TristateState state, final boolean allowsChildren) {
            super();
            parent = null;
            this.allowsChildren = allowsChildren;
            this.userObject = state;
            this.label = label;
        }

        @Override
        public void add(final MutableTreeNode newChild) {
            if (!(newChild instanceof TristateNode)) {
                throw new IllegalArgumentException("Only children of type TristateTreeNode are allowed.");
            }
            super.add(newChild);
        }

        public TristateState getState() {
            return getUserObject();
        }

        public void setState(final TristateState state) {
            setState(state, false, false);
        }

        private void setState(final TristateState state, final boolean invokedByParent, final boolean invokedByChild) {
            if (isLeaf() && state == TristateState.INDETERMINATE) {
                throw new IllegalArgumentException("Leaf nodes cannot have an indeterminate state");
            }
            super.setUserObject(state);
            if (!isLeaf() && !invokedByChild) {
                if (state != TristateState.INDETERMINATE) {
                    for (TreeNode node : children) {
                        if (node instanceof TristateNode) {
                            ((TristateNode) node).setState(state, true, false);
                        }
                    }
                }
            }
            if (!invokedByParent) {
                Object treeNode = getParent();
                if (treeNode instanceof TristateNode) {
                    ((TristateNode) treeNode).setState(((TristateNode) treeNode).getEffectiveState(), false, true);
                }
            }
        }

        public void setSelected(final boolean selected) {
            this.userObject = selected;
        }

        @Override
        public TristateState getUserObject() {
            return (TristateState) super.getUserObject();
        }

        @Override
        public void setUserObject(final Object userObject) {
            if (!(userObject instanceof TristateState)) {
                throw new IllegalArgumentException("Only values of type TristateState are allowed but got "
                                                   + userObject);
            }
            setState((TristateState) userObject);
        }

        public TristateState getEffectiveState() {
            if (isLeaf()) return getState();
            TristateState state = null;
            for (TreeNode node : children) {
                if (node instanceof TristateNode) {
                    TristateState nodeState = ((TristateNode) node).getState();
                    if (state == null) state = nodeState;
                    if (state != nodeState) {
                        state = TristateState.INDETERMINATE;
                        break;
                    }
                }
            }
            return state != null ? state : TristateState.DESELECTED;
        }

        public String getLabel() {
            return label;
        }

        public void setLabel(final String label) {
            this.label = label;
        }

        public void iterateState() {
            TristateState state = getState();
            switch (state) {
                case DESELECTED :
                    setState(TristateState.SELECTED);
                    break;
                case SELECTED :
                    setState(TristateState.DESELECTED);
                    break;
                case INDETERMINATE :
                    setState(state.next());
                    break;
            }
        }
    }

    private enum TristateState {
        DESELECTED() {
            @Override
            public TristateState next() {
                return INDETERMINATE;
            }
        },
        SELECTED() {
            @Override
            public TristateState next() {
                return DESELECTED;
            }
        },
        INDETERMINATE() {
            @Override
            public TristateState next() {
                return SELECTED;
            }
        };

        public abstract TristateState next();
    }

    private static class TristateIcon implements Icon {

        private TristateState state = TristateState.DESELECTED;

        @Override
        public void paintIcon(final Component c, final Graphics g, final int x, final int y) {
            switch (state) {
                case DESELECTED :
                    g.setColor(Color.RED);
                    break;
                case SELECTED :
                    g.setColor(Color.GREEN);
                    break;
                case INDETERMINATE :
                    g.setColor(Color.ORANGE);
                    break;
                default :
                    return;
            }
            g.fillRect(x, y, getIconWidth(), getIconHeight());
        }

        public void setState(final TristateState state) {
            this.state = state;
        }

        @Override
        public int getIconWidth() {
            return 16;
        }

        @Override
        public int getIconHeight() {
            return 16;
        }
    }
}

结果如下:

结果


推荐阅读