javaswinguser-interfacejscrollpanejlist

JList - Horizontal list, visible amount of values


I'm trying to create a horizontal JList with seven items inside a JScrollPane and only three of those items need to be visible and the others accessible via a scrollbar. Here's an example of what I'm trying to do:

image of desired result

This is what I tried to do:

        JFrame frame = new JFrame("JList Example");

        String[] items = {"Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", "Item 10"};
        JList<String> list = new JList<>(items);
        list.setLayoutOrientation(JList.HORIZONTAL_WRAP);
        list.setVisibleRowCount(1);

        JScrollPane scrollPane = new JScrollPane(list);
        scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);

        frame.add(scrollPane);

And this is what I got:

Image of actual result

So I can't find a way to limit the number of visible items other than one way to setPreferredSize of the JScrollPane. Which is not a very good way as described here, and it doesn't really work for me. Is there another way to do what I want?

P.S. I'm not good at English, so sorry if you misunderstood what I want. Trying my best


Solution

  • I just want to point out the obvious that "only three of those items need to be visible" is problematic, as the first three values might be larger or smaller then the following items or with different data you'll get different, and sometimes, weird results.

    Do we make use of a "prototype" value, the model's first element or try and get the first n items from the model to calculate the "desirable viewable width"? What happens when the model contains less then n elements or no elements at all?

    The point is, there's a "lot" to think about. If you just want to reduce the horizontal size of the component, you could just override the getPreferredScrollableViewportSize and return a "desirable" width based on you needs, but just remember that this not likely to be n columns.

    The following example basically demonstrates this concept (as best as I can without trying to figure out each and every possible edge case).

    It extends JList and overrides the getPreferredScrollableViewportSize method.

    For example...

    enter image description here

    import java.awt.BorderLayout;
    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.util.Vector;
    import javax.swing.JFrame;
    import javax.swing.JList;
    import javax.swing.JPanel;
    import javax.swing.JScrollPane;
    import javax.swing.ListModel;
    
    public class Main {
        public static void main(String[] args) {
            new Main();
        }
    
        public Main() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    JFrame frame = new JFrame();
                    frame.add(new TestPane());
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
        public class TestPane extends JPanel {
            public TestPane() {
                setLayout(new BorderLayout());
                String[] items = {"Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", "Item 10"};
                FixedColumnWidthList<String> list = new FixedColumnWidthList<>(items);
                list.setVisibleColumnCount(3);
    
                JScrollPane scrollPane = new JScrollPane(list);
                add(scrollPane);
            }
        }
    
        public class FixedColumnWidthList<T> extends JList<T> {
    
            private int visibleColumnCount = -1;
    
            public FixedColumnWidthList() {
                configureDefaults();
            }
    
            public FixedColumnWidthList(ListModel<T> dataModel) {
                super(dataModel);
                configureDefaults();
            }
    
            public FixedColumnWidthList(T[] listData) {
                super(listData);
                configureDefaults();
            }
    
            public FixedColumnWidthList(Vector<T> listData) {
                super(listData);
                configureDefaults();
            }
    
            protected void configureDefaults() {
                setLayoutOrientation(JList.HORIZONTAL_WRAP);
                setVisibleRowCount(1);
            }
    
            public int getVisibleColumnCount() {
                return visibleColumnCount;
            }
    
            public void setVisibleColumnCount(int visibleColumnCount) {
                if (this.visibleColumnCount == visibleColumnCount) {
                    return;
                }
                this.visibleColumnCount = visibleColumnCount;
                revalidate();
                repaint();
            }
    
            @Override
            public Dimension getPreferredScrollableViewportSize() {
                int visibleColumnCount = getVisibleColumnCount();
                Dimension original = super.getPreferredScrollableViewportSize();
                if (visibleColumnCount <= 0) {
                    return original;
                }
                ListModel<T> model = getModel();
                int desirableWidth = getFixedCellWidth();
                if (desirableWidth <= 0) {
                    T testValue = getPrototypeCellValue();
                    if (testValue != null) {
                        desirableWidth = getCellRenderer().getListCellRendererComponent(this, testValue, 0, false, false).getPreferredSize().width;
                        desirableWidth *= visibleColumnCount;
                    } else if (getModel().getSize() == 0) {
                        desirableWidth = 256; // Just pick a number :/
                    } else {
                        desirableWidth = 0;
                        // You could, instead, just take the first value and mutiply
                        // it's rendered width by the number of visible columns,
                        // but this could mean you're not actually getting the
                        // first three columns ... but this whole thing is a bag
                        // of ambiguity
                        for (int column = 0; column < visibleColumnCount && column < model.getSize(); column++) {
                            int width = getCellRenderer().getListCellRendererComponent(this, model.getElementAt(column), column, false, false).getPreferredSize().width;
                            System.out.println(column + " = " + width);
                            desirableWidth += width;
                        }
                        // I can't find the "horizontal insets", so I'm guessing...
                        desirableWidth += (8 * (visibleColumnCount - 1));
                        // If we have less data then visible columns, just
                        // average the available information and multiply it by
                        // the remaining number of columns as a guess
                        if (visibleColumnCount > model.getSize()) {
                            int averageWidth = desirableWidth / model.getSize();
                            desirableWidth += (visibleColumnCount - model.getSize());
                        }
                    }
                } else {
                    desirableWidth *= visibleColumnCount;
                }
    
                return new Dimension(desirableWidth, original.height);
            }
        }
    }
    

    I should not that the JavaDocs talk about "horizontal insets" but I can't find anywhere that it's accessible/available, so I made a guess. I suspect that this is probably a UI delegate option, so if you really want to make use of this example, I'd encourage you to experiment with it.

    Now, having said all this, just override the getPreferredScrollableViewportSize method and return a width which otherwise meets your desirable result, messing about with "visible columns" is ambiguous and problematic