Java Swing:JList与ListCellRenderer选择的项目高度不同

我正在制作自定义ListCellRenderer。 我知道每个单元格可以有不同的尺寸。 但是现在我希望为所选单元格设置不同的维度。 不知何故,JList在第一次计算每个单元格的边界时,为每个单独的单元格缓存维度。 这是我的代码:

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 ints = new Vector(); 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); } } 

在评论中,您可以看到我已经尝试过的内容。

我已经搜索了很长时间并且发现了许多无用的文章,其中一些文章触及ListCellRenderer /动态高度的东西,但它们只能起作用,因为单个细胞的高度保持不变。 我的高度在变,所以我该怎么做?

基本上,问题有两个方面,都位于ui委托中

  • 它在测量时无法将渲染器配置为其真实状态,这完全忽略了选择(和焦点)
  • 众所周知,它不会被迫重新计算缓存的单元格大小:它没有公开的api这样做,只能自愿进行模型更改。

解决第一个问题的方法确实是渲染器:实现忽略给定的选定标志并查询列表中的实际选择,如@Andy所述。 在代码中,使用OP的组件

 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); 

要修复第二个问题,自定义ui委托(也可以在其他人的答案中建议)是一种可能的解决方案。 虽然有些工作在一般情况下,但是如果需要支持多个LAF。

强制ui自动更新其缓存的一种不那么干扰但稍微脏的方法是在selectionChange上发送一个假的ListDataEvent:

 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); 

顺便说一句, SwingX项目的JXList有一个自定义ui委托 – 主要用于支持排序/过滤 – 用public api重新计算缓存,然后上面的ListSelectionListener将被简化(并且干净:-)到

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

我刚刚实现了这个function。 问题是,细胞渲染器被要求两次渲染细胞。 在第一轮中,所有列表条目都在没有选择的情况下呈现,然后使用选择再次呈现所选单元格。 因此,如果您在第一轮中提供首选大小,它将被缓存并用于第二轮。

诀窍是忽略getListCellRendererComponentisSelected boolean参数,并通过检查list.getSelectedIndices()包含给定索引来确定选择状态。

但是,我仍有问题,在列表可见后,渲染组件的高度有时会变大/变小。 用鼠标调整列表大小后,一切都很好。 我玩了validation/重新validation,重新绘制,重置缓存高度,但没有任何效果。 摇摆有时候有点奇怪……

JList无法根据选择或其他任何方式更改单元格的大小。 该列表使用“缓存”大小。 如果提供了新的cellRenderer,则会重新计算此大小并将其应用于列表中的所有单元格中。 我认为原因是列表的表现有很多条目。 可能的解决方案是编写自己的ListUI实现,该实现能够对选定和未选择的单元使用不同的大小。 这也带来了通过对数或其他插值来调整选择周围的单元尺寸的可能性。 我希望你有一个很大的理由为什么这样做。 这是很多工作!

关于这个愚蠢的JList行高问题,我一直在撕扯我的头发。 我有一个单元格渲染器,它为每一行设置一个变量行高 – 问题是JList保持高度的缓存。

使用其他答案,我想我已经打动了圣杯。 这里是:

使用由Jaap创建的BasicListUI的简化版本:

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

然后在创建JList时 – 像这样扩展它:

 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); 

根据您的具体情况,您可能需要在创建期间围绕triggerUpdate设置防护。

感谢Rastislav Komara,我很容易解决这个问题:

我创建了一个扩展BasicListUI的内部类,并创建了在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(); } } 

通常在JList高度更改时触发updateLayoutState方法。 我在这里做的唯一“疯狂”事情是我的渲染器需要知道所选索引是什么。 这是因为updateLayoutState方法在其高度计算中不使用所选索引。 以某种方式在getListCellRendererComponent中使用list.getSelectedIndex()不能很好地工作。

编辑:
检查nevster和kleopatra的anser,他们看起来更聪明,先尝试一下……

JList可能正在“缓存”您的单元格渲染器。 尝试附加ListSelectionListener,并在更改选择时再次设置渲染器。

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

这是一个简单的解决方案:

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

当然你需要编写自己的ListCellRenderer实现,并根据list元素的不同选择状态,可以设置返回Component的不同首选高度。

只需要进行一个问题:当您选择List FIRST时间的元素时,不能正确绘制。 但在那之后,一切顺利。

希望这可以帮到你。