如何制作一个 JList,每个项目都包含 JCheckBox 和 JLabel,单击时事件不同

How to make a JList that each Item contains JCheckBox and JLabel with different events on click

我正在尝试构建一个 Jlist,其中包含具有分离的侦听器和行为的元素(自定义)。我的意思是,当 Cell 加载时,它应该:

现在我设法构建的是 JList,带有 CustomClass (SimpleTemplate),它绘制一个带有 SimpleTemplate 名称的复选框,当您单击它时,会显示另一个面板中 SimpleTemplate 的信息。然而,我不知道如何将听众和事件分开,并且之前提出过。

到目前为止我的代码如下:

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import javax.swing.DefaultListModel;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.ListCellRenderer;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;

public class CustomJListExample extends JFrame {

    private static final Dimension SIDE_PANEL_DIMENSION = new Dimension(190, 190);
    private static final Dimension CONTAINER_PANEL_DIMENSION = new Dimension(400, 200);
    private static final Dimension TEMPLATES_LIST_DIMENSION = new Dimension(180, 180);
    private static final Border SIMPLE_BORDER = new JTextField().getBorder();

    private JList<SimpleTemplate> templatesList = new JList<>();
    private JLabel templateName = new JLabel();
    private JLabel templateDescription = new JLabel();


    public CustomJListExample() {
        JPanel rightPanel = prepareRightSide();
        JPanel leftPanel = prepareLeftSide();

        JPanel containerPanel = new JPanel();
        containerPanel.setPreferredSize(CONTAINER_PANEL_DIMENSION);

        containerPanel.add(leftPanel);
        containerPanel.add(rightPanel);
        add(containerPanel);
        pack();
    }

    private JPanel prepareRightSide() {
        JPanel rightPanel = new JPanel();
        rightPanel.setBorder(SIMPLE_BORDER);
        rightPanel.setBackground(Color.GRAY);
        rightPanel.setPreferredSize(SIDE_PANEL_DIMENSION);

        templateName.setText("---");
        templateDescription.setText("---");

        rightPanel.add(templateName);
        rightPanel.add(templateDescription);

        return rightPanel;
    }

    private JPanel prepareLeftSide() {
        JPanel leftPanel = new JPanel();
        leftPanel.setBorder(SIMPLE_BORDER);
        leftPanel.setBackground(Color.GRAY);
        leftPanel.setPreferredSize(SIDE_PANEL_DIMENSION);

        DefaultListModel<SimpleTemplate> templatesListModel = new DefaultListModel<>();
        templatesListModel.addElement(new SimpleTemplate("Template 1", "Description template 1", false));
        templatesListModel.addElement(new SimpleTemplate("Template 2", "Description template 2", true));
        templatesListModel.addElement(new SimpleTemplate("Template 3", "Description template 3", false));

        templatesList.setCellRenderer(new JListRepositoryItem());
        templatesList.addListSelectionListener(e-> displayTemplateInfo());
        templatesList.setPreferredSize(TEMPLATES_LIST_DIMENSION);
        templatesList.setModel(templatesListModel);
        templatesList.repaint();

        leftPanel.add(templatesList);

        return leftPanel;
    }

    private void displayTemplateInfo() {
        SimpleTemplate selectedValue = templatesList.getSelectedValue();
        templateName.setText(selectedValue.getName());
        templateDescription.setText(selectedValue.getDescription());
    }

    class JListRepositoryItem extends JCheckBox implements ListCellRenderer {
        @Override
        public Component getListCellRendererComponent(JList list, Object value, int index,
            boolean isSelected, boolean cellHasFocus) {
            setComponentOrientation(list.getComponentOrientation());
            setFont(list.getFont());
            setBackground(list.getBackground());
            setForeground(list.getForeground());

            if (value instanceof SimpleTemplate) {
                SimpleTemplate template = (SimpleTemplate) value;
                setSelected(isSelected);
                setEnabled(list.isEnabled());
                setText(template.getName());
            }

            return this;
        }
    }

    class SimpleTemplate {
        private String name;
        private String description;
        private boolean installed;

        public SimpleTemplate(String name, String description, boolean installed) {
            this.name = name;
            this.description = description;
            this.installed = installed;
        }

        public String getName() {
            return name;
        }

        public String getDescription() {
            return description;
        }

        public boolean isInstalled() {
            return installed;
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new CustomJListExample().setVisible(true));
    }
}

即生成以下示例。

然而,我没能使 Text 有自己的行为,也没有让 CheckBox 有自己的行为。

如果你想正确地做到这一点 - 你将不得不更改 JList UI 实现,因为选择行为来自那里。如果您从未使用过它,那将是一件非常困难的事情。

此外,通常很难像您要求的那样做,因为 JList 组件不允许您直接与 ListCellRenderer 实现中提供的组件进行交互 - 它只是使用它们来 "stamp" 他们的图形表示多次使用不同的设置。这使得 JList 在大量数据上表现得非常好,但会阻止您与渲染器组件直接交互。

但是对于像您这样的简单情况,您可以使用一种解决方法 - 您可以将自定义 MouseListener 添加到列表中,并在用户点击的位置添加 "guess"。幸运的是 JList API 为您提供了所有必要的方法:

templatesList.addMouseListener ( new MouseAdapter ()
{
    @Override
    public void mousePressed ( final MouseEvent e )
    {
        final Point point = e.getPoint ();
        final int index = templatesList.locationToIndex ( point );
        if ( index != -1 )
        {
            // Next calculations assume that text is aligned to left, but are easy to adjust
            final SimpleTemplate element = templatesList.getModel ().getElementAt ( index );
            final Rectangle cellBounds = templatesList.getCellBounds ( index, index );
            final JListRepositoryItem renderer = ( JListRepositoryItem ) templatesList.getCellRenderer ();
            final int iconWidth = renderer.getIcon () !=null ? renderer.getIcon ().getIconWidth () : 16;
            final Insets insets = renderer.getInsets ();
            final int iconX = cellBounds.x + insets.left;

            // Ensure that mouse press happened within top/bottom insets
            if ( cellBounds.y + insets.top <= point.y && point.y <= cellBounds.y + cellBounds.height - insets.bottom )
            {
                // Check whether we hit the checkbox icon
                if ( iconX <= point.x && point.x <= cellBounds.x + insets.left + iconWidth )
                {
                    // We hit the checkbox icon
                    element.installed = !element.installed;
                    templatesList.repaint ( cellBounds );
                }
                else
                {
                    // Check whether we hit text
                    final int iconTextGap = renderer.getIconTextGap ();
                    final int textX = cellBounds.x + insets.left + iconWidth + iconTextGap;
                    final FontMetrics fontMetrics = renderer.getFontMetrics ( renderer.getFont () );
                    final int textWidth = fontMetrics.stringWidth ( element.getName () );
                    if ( textX <= point.x && point.x <= textX + textWidth )
                    {
                        // We hit the text
                        templateName.setText ( element.getName () );
                        templateDescription.setText ( element.getDescription () );
                    }
                    else
                    {
                        // Reset values
                        templateName.setText ( "---" );
                        templateDescription.setText ( "---" );
                    }
                }
            }
            else
            {
                // Reset values
                templateName.setText ( "---" );
                templateDescription.setText ( "---" );
            }
        }
        else
        {
            // Reset values
            templateName.setText ( "---" );
            templateDescription.setText ( "---" );
        }
    }
} );

出于演示目的,我添加了文本大小计算,但如果不需要,您可以简化整个过程。

这里是标签、按钮或复选框等任何基本组件的细分:

这应该更容易可视化,并且应该可以帮助您了解要制作哪个区域 "clickable",因为这并不总是一件容易决定的事情。例如,我的示例非常精确 - 您只能准确地单击复选图标或文本,但在实践中这将是糟糕的体验,您可能希望将其扩展到 insets/leftover 个区域。

您还需要删除 ListSelectionListener,因为它会与 MouseListener 冲突。这是完整的代码:

import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

public class CustomJListExample extends JFrame
{

    private static final Dimension SIDE_PANEL_DIMENSION = new Dimension ( 190, 190 );
    private static final Dimension CONTAINER_PANEL_DIMENSION = new Dimension ( 400, 200 );
    private static final Dimension TEMPLATES_LIST_DIMENSION = new Dimension ( 180, 180 );
    private static final Border SIMPLE_BORDER = new JTextField ().getBorder ();

    private JList<SimpleTemplate> templatesList = new JList<> ();
    private JLabel templateName = new JLabel ();
    private JLabel templateDescription = new JLabel ();


    public CustomJListExample ()
    {
        JPanel rightPanel = prepareRightSide ();
        JPanel leftPanel = prepareLeftSide ();

        JPanel containerPanel = new JPanel ();
        containerPanel.setPreferredSize ( CONTAINER_PANEL_DIMENSION );

        containerPanel.add ( leftPanel );
        containerPanel.add ( rightPanel );
        add ( containerPanel );
        pack ();
    }

    private JPanel prepareRightSide ()
    {
        JPanel rightPanel = new JPanel ();
        rightPanel.setBorder ( SIMPLE_BORDER );
        rightPanel.setBackground ( Color.GRAY );
        rightPanel.setPreferredSize ( SIDE_PANEL_DIMENSION );

        templateName.setText ( "---" );
        templateDescription.setText ( "---" );

        rightPanel.add ( templateName );
        rightPanel.add ( templateDescription );

        return rightPanel;
    }

    private JPanel prepareLeftSide ()
    {
        JPanel leftPanel = new JPanel ();
        leftPanel.setBorder ( SIMPLE_BORDER );
        leftPanel.setBackground ( Color.GRAY );
        leftPanel.setPreferredSize ( SIDE_PANEL_DIMENSION );

        DefaultListModel<SimpleTemplate> templatesListModel = new DefaultListModel<> ();
        templatesListModel.addElement ( new SimpleTemplate ( "Template 1", "Description template 1", false ) );
        templatesListModel.addElement ( new SimpleTemplate ( "Template 2", "Description template 2", true ) );
        templatesListModel.addElement ( new SimpleTemplate ( "Template 3", "Description template 3", false ) );

        templatesList.setCellRenderer ( new JListRepositoryItem () );
        templatesList.setPreferredSize ( TEMPLATES_LIST_DIMENSION );
        templatesList.setModel ( templatesListModel );
        templatesList.repaint ();

        templatesList.addMouseListener ( new MouseAdapter ()
        {
            @Override
            public void mousePressed ( final MouseEvent e )
            {
                final Point point = e.getPoint ();
                final int index = templatesList.locationToIndex ( point );
                if ( index != -1 )
                {
                    // Next calculations assume that text is aligned to left, but are easy to adjust
                    final SimpleTemplate element = templatesList.getModel ().getElementAt ( index );
                    final Rectangle cellBounds = templatesList.getCellBounds ( index, index );
                    final JListRepositoryItem renderer = ( JListRepositoryItem ) templatesList.getCellRenderer ();
                    final int iconWidth = renderer.getIcon () !=null ? renderer.getIcon ().getIconWidth () : 16;
                    final Insets insets = renderer.getInsets ();
                    final int iconX = cellBounds.x + insets.left;

                    // Ensure that mouse press happened within top/bottom insets
                    if ( cellBounds.y + insets.top <= point.y && point.y <= cellBounds.y + cellBounds.height - insets.bottom )
                    {
                        // Check whether we hit the checkbox icon
                        if ( iconX <= point.x && point.x <= cellBounds.x + insets.left + iconWidth )
                        {
                            // We hit the checkbox icon
                            element.installed = !element.installed;
                            templatesList.repaint ( cellBounds );
                        }
                        else
                        {
                            // Check whether we hit text
                            final int iconTextGap = renderer.getIconTextGap ();
                            final int textX = cellBounds.x + insets.left + iconWidth + iconTextGap;
                            final FontMetrics fontMetrics = renderer.getFontMetrics ( renderer.getFont () );
                            final int textWidth = fontMetrics.stringWidth ( element.getName () );
                            if ( textX <= point.x && point.x <= textX + textWidth )
                            {
                                // We hit the text
                                templateName.setText ( element.getName () );
                                templateDescription.setText ( element.getDescription () );
                            }
                            else
                            {
                                // Reset values
                                templateName.setText ( "---" );
                                templateDescription.setText ( "---" );
                            }
                        }
                    }
                    else
                    {
                        // Reset values
                        templateName.setText ( "---" );
                        templateDescription.setText ( "---" );
                    }
                }
                else
                {
                    // Reset values
                    templateName.setText ( "---" );
                    templateDescription.setText ( "---" );
                }
            }
        } );

        leftPanel.add ( templatesList );

        return leftPanel;
    }

    class JListRepositoryItem extends JCheckBox implements ListCellRenderer<SimpleTemplate>
    {
        @Override
        public Component getListCellRendererComponent ( JList list, SimpleTemplate value, int index,
                                                        boolean isSelected, boolean cellHasFocus )
        {
            setComponentOrientation ( list.getComponentOrientation () );
            setFont ( list.getFont () );
            setBackground ( list.getBackground () );
            setForeground ( list.getForeground () );

            setSelected ( value.isInstalled () );
            setEnabled ( list.isEnabled () );
            setText ( value.getName () );

            return this;
        }
    }

    class SimpleTemplate
    {
        private String name;
        private String description;
        private boolean installed;

        public SimpleTemplate ( String name, String description, boolean installed )
        {
            this.name = name;
            this.description = description;
            this.installed = installed;
        }

        public String getName ()
        {
            return name;
        }

        public String getDescription ()
        {
            return description;
        }

        public boolean isInstalled ()
        {
            return installed;
        }
    }

    public static void main ( String[] args )
    {
        SwingUtilities.invokeLater ( () -> new CustomJListExample ().setVisible ( true ) );
    }
}

尽管我确实想再次强调,这是在 JList 内部逻辑之外工作的 "hack",因此您不能依赖 JList 选择,因为它会由 JList UI 的内部 listeners 更改。但似乎您一开始并不真的需要 JList 选择,因此这可能对您来说很好。

如果您愿意调整 JList UI - 您将需要执行类似的计算,而且还提供自定义 ListUI 实现,如果您是使用本机 OS 外观。