0

I have a horizontally arranged JList. All of the items in the list are of significantly different sizes, and by default the renderer scales each item to the size of the largest item in the list. I have attempted to implement a custom renderer as follows, but each item in the list remains the same size. Any advice?

The following is the ListCellRenderer:

package ui.wizards;

import java.awt.Component;
import java.awt.Dimension;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.ListCellRenderer;
import javax.swing.SwingConstants;

public class WizHistoryCellRenderer extends JLabel
        implements ListCellRenderer<String>
{
    private static final long serialVersionUID = -3650608268225337416L;
    JList<? extends String> list;

    @Override
    public Component getListCellRendererComponent(JList<? extends String> list,
        String value, int index, boolean isSelected, boolean cellHasFocus)
    {
        this.list = list;
        int width = this.getFontMetrics(this.getFont())
            .stringWidth((String) value);
        int height = 20;
        setText(value);

        if (list != null)
        {
            if (index == list.getSelectedIndex())
                showSelected();
            else
                showUnselected();
        }
        setMaximumSize(new Dimension((int) (1.1 * width), height));
        setPreferredSize(new Dimension((int) (1.1 * width), height));

        setHorizontalAlignment(SwingConstants.CENTER);
        setOpaque(true);

        return this;
    }

    private void showSelected()
    {
        setBackground(list.getSelectionBackground());
        setForeground(list.getSelectionForeground());
    }

    private void showUnselected()
    {
        setBackground(list.getBackground());
        setForeground(list.getForeground());
    }
}
Bryant
  • 664
  • 2
  • 9
  • 23

3 Answers3

0

Try calling the JList method setFixedCellHeight, with -1 as the argument. This causes JList to use the preferred height returned by any custom renderer when drawing the list items.

  • I tried calling setFixedCellHeight(-1) and setFixedCellWidth(-1) both before and after calling setListCellRenderer(), but neither had any effect. – Bryant Sep 03 '15 at 00:04
0

I think you could try to subclass javax.swing.plaf.basic.BasicListUI and override protected void paintCell(Graphics g, int row, Rectangle rowBounds, ListCellRenderer cellRenderer, ListModel dataModel, ListSelectionModel selModel, int leadIndex) and maybe some other methods performing cell size calculation.

Then use JList.setUI(ListUI) to apply your custom UI. The UI is responsible for drawing the JList using ListCellRenders. Please excuse that I do not provide a complete example as it is presumably a lot of work tweaking all aspects of the custom drawing process.

Lars Gendner
  • 1,816
  • 2
  • 14
  • 24
0

You will need 3 things:

  1. A scale factor for each individual cell. You may for example add a scale factor variable of type double in each entry of the list.
  2. A custom ListCellRenderer.
  3. Make the method BasicListUI#updateLayoutState() public.

Follows self-contained code (read the comments for more info):

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.plaf.basic.BasicListUI;

public class JListIndipendentCellSizes {

    //Configuration constants:
    private static final int ICON_SIZE = 20;
    private static final double SCALE_STEP_SIZE = 0.125; //Smaller values of this makes zooming slower. Greater values makes zooming faster.

    //Simple Icon implementation for demonstration purposes.
    public static class TJIcon implements Icon {
        private final Color rectColor, ovalColor;

        public TJIcon(final Color rectColor, final Color ovalColor) {
            this.rectColor = rectColor;
            this.ovalColor = ovalColor;
        }

        @Override
        public void paintIcon(final Component c, final Graphics g, final int x, final int y) {
            g.setColor(rectColor);
            g.fillRect(x, y, getIconWidth(), getIconHeight());
            g.setColor(ovalColor);
            g.fillOval(x, y, getIconWidth(), getIconHeight());
        }

        @Override public int getIconWidth() { return ICON_SIZE; }
        @Override public int getIconHeight() { return ICON_SIZE; }
    }

    //A simple list entry. Contains a text, an icon and (on top of them all) the scale factor:
    public static class TJListEntry {
        private final String text;
        private final Icon icon;
        private double scaleFactor;

        public TJListEntry(final String text,
                           final Icon icon) {
            this.text = text;
            this.icon = icon;
            scaleFactor = 1;
        }

        public String getText() {
            return text;
        }

        public Icon getIcon() {
            return icon;
        }

        public double getScaleFactor() {
            return scaleFactor;
        }

        public void zoomIn() {
            scaleFactor = scaleFactor + SCALE_STEP_SIZE;
        }

        public void zoomOut() {
            scaleFactor = Math.max(scaleFactor - SCALE_STEP_SIZE, SCALE_STEP_SIZE); //Do not allow underflow.
        }

        public void resetZoom() {
            scaleFactor = 1;
        }
    }

    public static class TJListCellRenderer extends JLabel implements ListCellRenderer<TJListEntry> {
        private double currentScaleFactor;

        public TJListCellRenderer() {
            //Ensure every pixel is painted starting from the top-left corner of the label:
            super.setVerticalAlignment(JLabel.TOP);
            super.setHorizontalAlignment(JLabel.LEFT);
            //We need to do this, because the scaling in paintComponent() is also relative to the top-left corner.
        }

        @Override
        public void paintComponent(final Graphics g) {
            //setRenderingHints here? Probably for ANTIALIAS...
            ((Graphics2D)g).scale(currentScaleFactor, currentScaleFactor); //Let's scale everything that is painted afterwards:
            super.paintComponent(g); //Let's paint the (scaled) JLabel!
        }

        @Override
        public Dimension getPreferredSize() {
            final Dimension superPrefDim = super.getPreferredSize(); //Handles automatically insets, icon size, text font, etc.
            final double w = superPrefDim.width * currentScaleFactor, //And we just scale the preferred size.
                         h = superPrefDim.height * currentScaleFactor; //And we just scale the preferred size.
            return new Dimension((int)w + 1, (int)h + 1); //Add one extra pixel to spare.
        }

        @Override
        public Component getListCellRendererComponent(final JList<? extends TJListEntry> list, final TJListEntry value, final int index, final boolean isSelected, final boolean cellHasFocus) {

            currentScaleFactor = value.getScaleFactor(); //Probably the most important step.

            setIcon(value.getIcon()); //Could be a loaded ImageIcon here (i.e. image)!
            setText(value.getText());

            if (isSelected) {
                setBackground(list.getSelectionBackground());
                setForeground(list.getSelectionForeground());
            } else {
                setBackground(list.getBackground());
                setForeground(list.getForeground());
            }
            setOpaque(true);

            return this;
        }
    }

    public static class TJListUI extends BasicListUI {
        @Override
        public void updateLayoutState() {
            super.updateLayoutState(); //Just make the following method public.
            /*Note: this is not really needed here:
            The method could remain protected, but in the case you want this
            code to be a bit more reusable, then you shall make it public.*/
        }
    }

    public static void main(final String[] args) {
        final TJListEntry[] entries = new TJListEntry[]{new TJListEntry("This is a sample text.", new TJIcon(Color.BLACK, Color.WHITE)),
                                                        new TJListEntry("This is a longer sample text.", new TJIcon(Color.GREEN, Color.RED)),
                                                        new TJListEntry("Small text", new TJIcon(Color.LIGHT_GRAY, Color.BLUE))};

        final TJListUI ui = new TJListUI();

        final JList<TJListEntry> list = new JList<>(entries);
        list.setUI(ui); //Important step! Without setting our UI, it won't work (at least as I have looked for).
        list.setCellRenderer(new TJListCellRenderer()); //Important step! Without setting our custom ListCellRenderer you will have to implement your own ListUI probably.

        final JScrollPane scroll = new JScrollPane(list, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);

        final JButton buttonZoomIn = new JButton("Zoom in selected cells"),
                      buttonZoomOut = new JButton("Zoom out selected cells"),
                      buttonResetZoom = new JButton("Reset zoom of selected cells");

        buttonZoomIn.addActionListener(e -> {
            for (final int i: list.getSelectedIndices())
                list.getModel().getElementAt(i).zoomIn();
            ui.updateLayoutState(); //Read the preferred sizes from the cell renderer.
            list.revalidate(); //Update the JScrollPane.
            list.repaint(); //Repaint the list.
        });

        buttonZoomOut.addActionListener(e -> {
            for (final int i: list.getSelectedIndices())
                list.getModel().getElementAt(i).zoomOut();
            ui.updateLayoutState(); //Read the preferred sizes from the cell renderer.
            list.revalidate(); //Update the JScrollPane.
            list.repaint(); //Repaint the list.
        });

        buttonResetZoom.addActionListener(e -> {
            for (final int i: list.getSelectedIndices())
                list.getModel().getElementAt(i).resetZoom();
            ui.updateLayoutState(); //Read the preferred sizes from the cell renderer.
            list.revalidate(); //Update the JScrollPane.
            list.repaint(); //Repaint the list.
        });

        final JPanel buttons = new JPanel(); //FlowLayout.
        buttons.add(buttonZoomIn);
        buttons.add(buttonZoomOut);
        buttons.add(buttonResetZoom);

        final JPanel panel = new JPanel(new BorderLayout());
        panel.add(buttons, BorderLayout.PAGE_START);
        panel.add(scroll, BorderLayout.CENTER);

        final JFrame frame = new JFrame("Independent JList cell sizes demo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(panel);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

And a screenshot:

Sample screenshot

To run it on your own, just select at least one index, and play with the buttons.

gthanop
  • 3,035
  • 2
  • 10
  • 27