Java Swing: JList with ListCellRenderer selected i

2020-02-10 09:25发布

问题:

I'm making a custom ListCellRenderer. I know that you can have different dimensions for each individual cell. But now I want to have a different dimension for the selected cell. Somehow, the JList is caching the dimension for each individual cell the first time it has to calculate bounds for each cell. This is my code:

public class Test {

    static class Oh extends JPanel {

        public Oh() {
            setPreferredSize(new Dimension(100, 20));
        }

        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.setColor(Color.WHITE);
            g.fillRect(0, 0, getWidth(), getHeight());
        }
    }

    static class Yeah extends JPanel {
        private boolean isSelected;

        public Yeah(boolean isSelected) {
            setPreferredSize(new Dimension(100, 100));
            this.isSelected = isSelected;
        }

        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            //setSize(100, 100); // doesn't change the bounds of the component
            //setBounds(0, 0, 100, 100); // this doesn't do any good either.
            if (isSelected) g.setColor(Color.GREEN);
            else g.setColor(Color.BLACK);
            g.fillRect(0, 0, getWidth(), getHeight());
        }
    }

    public static void main(String[] args) {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        f.setSize(800, 500);
        Vector<Integer> ints = new Vector<Integer>();
        for (int i = 0; i < 100; i++) {
            ints.add(i);
        }
        JList list = new JList(ints);
        list.setCellRenderer(new ListCellRenderer() {
            public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
                if (isSelected || ((Integer) value) == 42) return new Yeah(isSelected);
                else return new Oh();
            }
        });
        //list.setPrototypeCellValue(null);
        //list.setFixedCellHeight(-1);
        f.add(new JScrollPane(list));
        f.setVisible(true);
    }
}

In the comments you can see what I've already tried.

I've already searched quite long and found a lot of useless articles, some of them touch the ListCellRenderer/dynamic height thing, but they only work because the height stays the same for the individual cells. My heights are changing, so how do I do this?

回答1:

Basically, there are two aspects of the problem, both located in the ui delegate

  • it fails to configure the renderer to its real state when measuring, that is ignores the selection (and focus) completely
  • it is notoriously stubborn against being forced to re-calculate the cached cell sizes: it has no public api to do so and only does voluntarily on model changes.

The remedy to fix the first is indeed the renderer: implement to ignore the given selected flag and query the list for the real selection, as outlined by @Andy. In code, using the OP's components

ListCellRenderer renderer = new ListCellRenderer() {
    Yeah yeah = new Yeah(false);
    Oh oh = new Oh();

    @Override
    public Component getListCellRendererComponent(JList list,
            Object value, int index, boolean isSelected,
            boolean cellHasFocus) {
        // ignore the given selection index, query the list instead
        if (list != null) {
            isSelected = list.isSelectedIndex(index);
        }
        if (isSelected || ((Integer) value) == 42) {
            yeah.isSelected = isSelected;
            return yeah;

        }
        return oh;
    }
};
list.setCellRenderer(renderer);

To fix the second, a custom ui delegate (as suggested in others answers as well) is a possible solution. Though some work in the general case, if supporting multiple LAFs is needed.

A less intrusive but slightly dirty method to force the ui into voluntarily update its cache is to send a fake ListDataEvent on selectionChange:

ListSelectionListener l = new ListSelectionListener() {
    ListDataEvent fake = new ListDataEvent(list, ListDataEvent.CONTENTS_CHANGED, -1, -1);
    @Override
    public void valueChanged(ListSelectionEvent e) {
        JList list = (JList) e.getSource();
        ListDataListener[] listeners = ((AbstractListModel) list.getModel())
                .getListDataListeners();
        for (ListDataListener l : listeners) {
            if (l.getClass().getName().contains("ListUI")) {
                l.contentsChanged(fake);
                break;
            }
        }
    }
};
list.addListSelectionListener(l);

BTW, JXList of the SwingX project has a custom ui delegate - mainly for supporting sorting/filtering - with public api to re-calculate the cache, then the above ListSelectionListener would be simplified (and clean :-) to

    ListSelectionListener l = new ListSelectionListener() {
        @Override
        public void valueChanged(ListSelectionEvent e) {
            ((JXList) e.getSource()).invalidateCellSizeCache();
        }
    };
    list.addListSelectionListener(l);


回答2:

I just implemented this feature. The problem is, that the cell renderer is asked twice for rendering a cell. In the first round all list entries are rendered without selection, then the selected cells are rendered again using selection. So if you provide a preferred size in the first round, it is cached and also used for the second round.

The trick is to ignore the isSelected boolean parameter in the getListCellRendererComponent and to figure out the selection state by checking if list.getSelectedIndices() contains the given index.

But, I still have the problem, that after the list is made visible, the height of the rendered components are sometimes to large/small. After resizing the list by mouse everything is fine again. I played around with validate/revalidate, repaint, reset of cached heights, but nothing worked. Swing is sometimes a bit strange...



回答3:

The JList has no ability to change size of cell depending on selection or whatever. The list use "cached" sizes. If there is new cellRenderer provided this sizes are recounted and applied within all cells in list. I think the reason is performance for list with a lot of entries. The possible solution is to write own ListUI implementation which is able to use different sizes for selected and unselected cells. This brings also possibility to adjust size of cells around selection by logarithm or other interpolation. I hope you have a big reason why to do this. It is a lot of work!



回答4:

I've been tearing my hair out about this stupid JList row height problem. I have a cell renderer which sets a variable row height for every row - problem is that JList keeps a cache of the heights.

Using the other answers, I think I've struck on the holy grail. Here it is:

Use a simplified version of the BasicListUI as created by Jaap:

public class BetterListUI extends BasicListUI {
    public void triggerUpdate() {
        updateLayoutState();
    }
}

Then when you create a JList - extend it like this :

betterListUI = new BetterListUI();
myJList = new JList() {
    @Override
    public void repaint(long tm, int x, int y, int width, int height) {
        betterListUI.triggerUpdate();
        super.repaint(tm, x, y, width, height);
    }
};
myJList.setUI(betterListUI);

You may need to put a guard around the triggerUpdate during creation depending on your circumstances.



回答5:

Thanks to Rastislav Komara I've been able to solve this quite easily:

I've created an inner class that extends BasicListUI and created public method that is called on ListSelectionListener.valueChanged:

private class MyRenderer implements ListCellRenderer {
    public int listSelectedIndex = -1;

    public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
            boolean cellHasFocus) {
        if (index == listSelectedIndex)
            return new Yeah(isSelected);
        else
            return new Oh();
    }
}
MyRenderer lcr = new MyRenderer();
private class MyListUI extends BasicListUI {

    public void triggerUpdate() {
        lcr.listSelectedIndex = list.getSelectedIndex();
        updateLayoutState();
        list.revalidate();
    }
}

The updateLayoutState method is normally triggered when the JList height changes. The only "insane" thing I'm doing here is that my renderer needs to know what the selected index is. This is because the updateLayoutState method doesn't use the selected index in it's height calculations. Somehow using list.getSelectedIndex() inside getListCellRendererComponent doesn't work well.

Edit:
Check also the anser by nevster and kleopatra, they look way smarter, try them first...



回答6:

The JList is probably "caching" your cell renderer. Try to attach a ListSelectionListener, and set the renderer again when selection is changed.

...
addListSelectionListener(new ListSelectionListener() {  
  public void valueChanged(ListSelectionEvent event) { 
    if(event.getValueIsAdjusting() == false) {
      list.setCellRenderer(new MyRenderer());
    }
  }
)
...


回答7:

this is a simple solution:

public class VariableHeightListUI extends BasicListUI {

  @Override
  public void paint(Graphics g, JComponent c) {
    updateLayoutState();
    super.paint(g, c); 
  }
}

of course you need write your own implementation of ListCellRenderer, and according to different selection state of list element, you can set different prefer height of returned Component.

Only one issue need to go on is : when you select an element of List FIRST time, not draw correctly. but after then, all work well.

hope this can help you.