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

发布于 2024-08-01 18:33:09 字数 2141 浏览 4 评论 0原文

我正在制作一个自定义 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<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);
    }
}

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

我已经搜索了很长一段时间,发现了很多无用的文章,其中一些涉及 ListCellRenderer/动态高度 的东西,但它们之所以有效,是因为各个单元格的高度保持不变。 我的身高正在发生变化,那么我该怎么办呢?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

月依秋水 2024-08-08 18:33:09

基本上,问题有两个方面,都位于 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 自愿更新其缓存的一种侵入性较小但稍微肮脏的方法是在选择更改时发送一个假的 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);

BTW,SwingX 项目有一个自定义的 ui delegate - 主要用于支持排序/过滤 - 使用公共 api 重新计算缓存,然后上面的 ListSelectionListener 将被简化(并且干净:-) 为

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

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);
烟花肆意 2024-08-08 18:33:09

我刚刚实现了这个功能。 问题是,单元格渲染器被请求两次来渲染单元格。 在第一轮中,所有列表条目均在不进行选择的情况下呈现,然后使用选择再次呈现选定的单元格。 因此,如果您在第一轮中提供首选大小,它将被缓存并也用于第二轮。

诀窍是忽略 getListCellRendererComponent 中的 isSelected 布尔参数,并通过检查 list.getSelectedIndices() 是否包含给定索引。

但是,我仍然有问题,在列表可见后,渲染组件的高度有时太大/太小。 用鼠标调整列表大小后,一切都恢复正常了。 我尝试过验证/重新验证、重新绘制、重置缓存高度,但没有任何效果。 Swing有时有点奇怪......

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...

故人如初 2024-08-08 18:33:09

JList 无法根据选择或其他情况更改单元格的大小。 该列表使用“缓存”大小。 如果有新的 cellRenderer,则该大小将被重新计数并应用到列表中的所有单元格中。 我认为原因是包含大量条目的列表的性能。 可能的解决方案是编写自己的 ListUI 实现,它能够对选定和未选定的单元格使用不同的大小。 这还带来了通过对数或其他插值来调整选择周围的单元格大小的可能性。 我希望你有这样做的充分理由。 这是很多工作!

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!

小女人ら 2024-08-08 18:33:09

我一直在为这个愚蠢的 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 周围放置一个防护。

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.

他是夢罘是命 2024-08-08 18:33:09

感谢 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();
    }
}

updateLayoutState 方法通常在 JList 高度更改时触发。
我在这里做的唯一“疯狂”的事情是我的渲染器需要知道所选索引是什么。 这是因为 updateLayoutState 方法在其高度计算中不使用选定的索引。
不知何故,在 getListCellRendererComponent 中使用 list.getSelectedIndex() 效果不佳。

编辑:
另请检查内夫斯特和克利奥帕特拉的答案,他们看起来更聪明,先尝试一下......

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...

电影里的梦 2024-08-08 18:33:09

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

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

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());
    }
  }
)
...
一笔一画续写前缘 2024-08-08 18:33:09

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

public class VariableHeightListUI extends BasicListUI {

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

当然您需要编写自己的ListCellRenderer实现,并且根据列表元素的不同选择状态,您可以设置返回组件的不同首选高度。

只需要继续的一个问题是:当您第一次选择列表的元素时,无法正确绘制。 但在那之后,一切都运转良好。

希望这可以帮到你。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文