如果显示区域太小,带有工具提示的 JComboBox,同时保持外观和感觉

JComboBox with tooltip if display area too small, while maintaining look and feel

我正在创建一个组合框,当显示区域太小而无法显示所选项目的所有文本时,它会显示工具提示。

我目前的解决方案存在的问题:

我在底部包含的小 MCVE 显示了我的想法。它显示了四个组合框。第一个和第三个是常规的未调整的 JComboBox 实例,第二个和第四个是调整过的组合框,用于在显示区域对于文本来说太小时显示工具提示。第一和第二启用,第三和第四禁用;这是为了更好地展示外观和感觉上的差异。快照:

如何保持常规组合框的外观,并在显示区域太小时显示工具提示?我走在正确的道路上吗?如果是,我还需要做些什么?如果我不是,我应该怎么做?


import java.awt.Dimension;
import java.awt.EventQueue;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class ComboWithTooltip {
    public static void main(String[] args) {
        try {
            // On my system: "com.sun.java.swing.plaf.windows.WindowsLookAndFeel"
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName( ));
        } 
        catch (ClassNotFoundException | InstantiationException | IllegalAccessException
                | UnsupportedLookAndFeelException e) {
            System.err.println("Can't set look and feel");
            return;
        }
        EventQueue.invokeLater (
            new Runnable() {
                @Override
                public void run() {
                    buildFrame().setVisible(true);
                }
            }
        );
    }

    static JFrame buildFrame() {
        JPanel contentPane = new JPanel();
        BoxLayout layout = new BoxLayout(contentPane, BoxLayout.Y_AXIS);
        contentPane.setLayout(layout);
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createDefaultCombo(true/*enabled*/));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createTweakedCombo(true/*enabled*/));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createDefaultCombo(false/*enabled*/));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createTweakedCombo(false/*enabled*/));
        contentPane.add(Box.createVerticalStrut(25));

        JFrame frame = new JFrame();
        frame.setContentPane(contentPane);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        return frame;
    }

    private final static String[] items = new String[]{ "Long text 1234444444444444444444444", "Some more of that long texttttttttttttttttttttt", "The longer the text, the harder to read it becomessssssssssssssssssssssssssssssssssssssss" };

    @SuppressWarnings("serial")
    private static JComboBox<String> createDefaultCombo(boolean enabled) {
        JComboBox<String> comboBoxDefault = new JComboBox<String>(items) {
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(100,25); // intentionally too small
            }
        };
        comboBoxDefault.setEnabled(enabled);
        return comboBoxDefault;
    }

    @SuppressWarnings("serial")
    private static class JComboBoxTweaked extends JComboBox<String> {
        public JComboBoxTweaked(String[] items) {
            super(items);

            setRenderer (
                new DefaultListCellRenderer() {
                    @Override
                    public void setBounds(int x, int y, int width, int height) {
                        super.setBounds( x, y, width, height );

                        if( width != 0 ) {
                            if( getPreferredSize( ).width > getSize( ).width )
                                JComboBoxTweaked.this.setToolTipText( getText( ) );
                            else
                                JComboBoxTweaked.this.setToolTipText( null );
                        }
                    }
                }
            );
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(100,25); // intentionally too small
        }
    };

    private static JComboBox<String> createTweakedCombo(boolean enabled) {
        JComboBox<String> comboBoxTweaked = new JComboBoxTweaked(items);
        comboBoxTweaked.setEnabled(enabled);
        return comboBoxTweaked;
    }
}

此处的解决方案:提供对标准 L&F 渲染器的委托。这是代码:

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.GridLayout;
import java.util.Objects;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.ListCellRenderer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class ComboWithTooltip {
    public static void main(String[] args) {
        try {
            // On my system: "com.sun.java.swing.plaf.windows.WindowsLookAndFeel"
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException
                | UnsupportedLookAndFeelException e) {
            System.err.println("Can't set look and feel");
            return;
        }
        EventQueue.invokeLater(
                new Runnable() {
                    @Override
                    public void run() {
                        buildFrame().setVisible(true);
                    }
                });
    }

    static JFrame buildFrame() {
        JPanel contentPane = new JPanel();
        BoxLayout layout = new BoxLayout(contentPane, BoxLayout.Y_AXIS);
        contentPane.setLayout(layout);
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createDefaultCombo(true/* enabled */));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createTweakedCombo(true/* enabled */));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createDefaultCombo(false/* enabled */));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createTweakedCombo(false/* enabled */));
        contentPane.add(Box.createVerticalStrut(25));

        JFrame frame = new JFrame();
        frame.setContentPane(contentPane);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        return frame;
    }

    private final static String[] items =
            new String[] {"Long text 1234444444444444444444444", "Some more of that long texttttttttttttttttttttt",
                    "The longer the text, the harder to read it becomessssssssssssssssssssssssssssssssssssssss"};

    @SuppressWarnings("serial")
    private static JComboBox<String> createDefaultCombo(boolean enabled) {
        JComboBox<String> comboBoxDefault = new JComboBox<String>(items) {
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(100, 25); // intentionally too small
            }
        };
        comboBoxDefault.setEnabled(enabled);
        return comboBoxDefault;
    }

    @SuppressWarnings("serial")
    private static class JComboBoxTweaked extends JComboBox<String> {
        public JComboBoxTweaked(String[] items) {
            super(items);

            setRenderer(new DelegateComboBoxRenderer<>(getRenderer(), this));
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(100, 25); // intentionally too small
        }
    };

    private static class DelegateComboBoxRenderer<E> extends JPanel implements ListCellRenderer<E> {

        private final ListCellRenderer<E> delegate;

        private final JComboBox<?> combo;
        private String lastText;

        /**
         * @param delegate
         */
        public DelegateComboBoxRenderer(ListCellRenderer<E> delegate, JComboBox<?> combo) {
            setLayout(new GridLayout(1, 1));
            setOpaque(true);
            this.delegate = delegate;
            this.combo = combo;

        }

        @Override
        public Component getListCellRendererComponent(JList<? extends E> list, E value, int index, boolean isSelected,
                boolean cellHasFocus) {
            removeAll();
            Component c = delegate.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
            add(c);
            if (c instanceof JLabel) {
                lastText = ((JLabel) c).getText();
            } else {
                lastText = Objects.toString(value, null);
            }
            setOpaque(c.isOpaque()); // some renderers works with opacity state, we need to consider it
            return this;
        }

        // some UI classes reset background/foreground, so we need to forward them
        @Override
        public void setBackground(Color bg) {
            super.setBackground(bg);
            for (Component c : getComponents()) {
                c.setBackground(bg);
            }
        }

        @Override
        public void setForeground(Color fg) {
            super.setForeground(fg);
            for (Component c : getComponents()) {
                c.setForeground(fg);
            }
        }

        // not sure whether it's required
        @Override
        public void setFont(Font font) {
            super.setFont(font);
            for (Component c : getComponents()) {
                c.setFont(font);
            }
        }

        @Override
        public void setBounds(int x, int y, int width, int height) {
            super.setBounds(x, y, width, height);

            if (width != 0) {
                if (getPreferredSize().width > getSize().width) {
                    combo.setToolTipText(lastText);
                } else {
                    combo.setToolTipText(null);
                }
            }
        }

    }
    private static JComboBox<String> createTweakedCombo(boolean enabled) {
        JComboBox<String> comboBoxTweaked = new JComboBoxTweaked(items);
        comboBoxTweaked.setEnabled(enabled);
        return comboBoxTweaked;
    }
}

另一种方法是使用ListCellRenderer,可以通过JComboBox#getRenderer()方法获得,而不是new DefaultListCellRenderer() {...}

ListCellRenderer<? super String> renderer = getRenderer();
setRenderer(new ListCellRenderer<String>() {
  @Override public Component getListCellRendererComponent(
      JList<? extends String> list, String value, int index,
      boolean isSelected, boolean cellHasFocus) {
    Component c = renderer.getListCellRendererComponent(
      list, value, index, isSelected, cellHasFocus);

WindowsLookAndFeel的情况下会是WindowsComboBoxRenderer:

// @see com/sun/java/swing/plaf/windows/WindowsComboBoxUI.java
/**
 * Subclassed to set opacity {@code false} on the renderer
 * and to show border for focused cells.
 */
@SuppressWarnings("serial") // Superclass is not serializable across versions
private static class WindowsComboBoxRenderer
      extends BasicComboBoxRenderer.UIResource {

ComboWithTooltip2.java

import java.awt.*;
import java.util.Objects;
import javax.swing.*;

public class ComboWithTooltip2 {
    public static void main(String[] args) {
        try {
            // On my system: "com.sun.java.swing.plaf.windows.WindowsLookAndFeel"
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName( ));
        } 
        catch (ClassNotFoundException | InstantiationException | IllegalAccessException
                | UnsupportedLookAndFeelException e) {
            System.err.println("Can't set look and feel");
            return;
        }
        EventQueue.invokeLater (
            new Runnable() {
                @Override
                public void run() {
                    buildFrame().setVisible(true);
                }
            }
        );
    }

    static JFrame buildFrame() {
        JPanel contentPane = new JPanel();
        BoxLayout layout = new BoxLayout(contentPane, BoxLayout.Y_AXIS);
        contentPane.setLayout(layout);
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createDefaultCombo(true/*enabled*/));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createTweakedCombo(true/*enabled*/));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createDefaultCombo(false/*enabled*/));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createTweakedCombo(false/*enabled*/));
        contentPane.add(Box.createVerticalStrut(25));

        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createDefaultCombo2(true));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createDefaultCombo2(false));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(Box.createVerticalGlue());
        contentPane.setPreferredSize(new Dimension(100, 320));

        JFrame frame = new JFrame();
        frame.setContentPane(contentPane);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        return frame;
    }

    private final static String[] items = new String[]{ "Long text 1234444444444444444444444", "Some more of that long texttttttttttttttttttttt", "The longer the text, the harder to read it becomessssssssssssssssssssssssssssssssssssssss" };

    @SuppressWarnings("serial")
    private static JComboBox<String> createDefaultCombo(boolean enabled) {
        JComboBox<String> comboBoxDefault = new JComboBox<String>(items) {
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(100,25); // intentionally too small
            }
        };
        comboBoxDefault.setEnabled(enabled);
        return comboBoxDefault;
    }

    @SuppressWarnings("serial")
    private static class JComboBoxTweaked extends JComboBox<String> {
        public JComboBoxTweaked(String[] items) {
            super(items);

            setRenderer (
                new DefaultListCellRenderer() {
                    @Override
                    public void setBounds(int x, int y, int width, int height) {
                        super.setBounds( x, y, width, height );

                        if( width != 0 ) {
                            if( getPreferredSize( ).width > getSize( ).width )
                                JComboBoxTweaked.this.setToolTipText( getText( ) );
                            else
                                JComboBoxTweaked.this.setToolTipText( null );
                        }
                    }
                }
            );
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(100,25); // intentionally too small
        }
    };

    private static JComboBox<String> createTweakedCombo(boolean enabled) {
        JComboBox<String> comboBoxTweaked = new JComboBoxTweaked(items);
        comboBoxTweaked.setEnabled(enabled);
        return comboBoxTweaked;
    }

  private static JComboBox<String> createDefaultCombo2(boolean enabled) {
    JComboBox<String> comboBox = new JComboBox<String>(items) {
      @Override public void updateUI() {
        setRenderer(null);
        super.updateUI();
        ListCellRenderer<? super String> renderer = getRenderer();
        setRenderer(new ListCellRenderer<String>() {
          @Override public Component getListCellRendererComponent(JList<? extends String> list, String value, int index, boolean isSelected, boolean cellHasFocus) {
            Component c = renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);

            Insets i1 = ((JComponent) c).getInsets();
            Insets i2 = getInsets();
            int availableWidth = getWidth() - i1.top - i1.bottom - i2.top - i2.bottom;
            if (index < 0) {
              int buttonSize = getHeight() - i2.top - i2.bottom;
              availableWidth -= buttonSize;
              JTextField tf = (JTextField) getEditor().getEditorComponent();
              Insets i3 = tf.getMargin();
              availableWidth -= i3.left + i3.right;
            }

            String str = Objects.toString(value, "");
            FontMetrics fm = c.getFontMetrics(getFont());
            setToolTipText(fm.stringWidth(str) > availableWidth ? str : null);
            return c;
          }
        });
      }
    };
    comboBox.setEnabled(enabled);
    return comboBox;
  }
}