Java Swing: JList with ListCellRenderer selected item different height - java

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?

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

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

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!

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.

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

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

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.

Related

JList highlight row issue

I'm trying to highlight all rows in a JList which have been "matched" with data elsewhere, and disable that row.
When I debug, I can see the correct data is set within cbNameIsMatched. However, what happens instead is that after I create the first match, each row I select in the JList is highlighted instead of the one with the "matched" index. The setEnabled is also setting for all items from the end of the list up to where I click in the list.
class MyListCellRenderer extends JLabel implements ListCellRenderer
{
public MyCopybookListCellRenderer()
{
setOpaque(false);
}
#Override
public Component getListCellRendererComponent(JList paramList, Object value,
int index, boolean isSelected, boolean cellHasFocus)
{
setText(value.toString());
if(isSelected)
{
setOpaque(true);
}
else
{
setOpaque(false);
}
if(cbNameIsMatched[index]==2)
{
setBackground(Color.YELLOW);
setEnabled(false);
}
myList.repaint();
return this;
}
You would do better extending DefaultListCellRenderer over JLabel as the former already takes care of everything and all you have to do is change the specific things you need. It gives you a "safety net" for the cases you didn't touch.
public class GetterText extends JFrame {
GetterText() {
JList<String> list = new JList<>(new String[]{"AAAA", "BBBB", "CCCC", "DDDD"});
list.setCellRenderer(new MyListCellRenderer());
getContentPane().add(list);
setLocationRelativeTo(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
pack();
setVisible(true);
}
private class MyListCellRenderer extends DefaultListCellRenderer {
#Override
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
label.setOpaque(isSelected); // Highlight only when selected
label.setOpaque(true); // Highlight always
if(index == 2) { // I faked a match for the second index, put you matching condition here.
label.setBackground(Color.YELLOW);
label.setEnabled(false);
}
return label;
}
}
public static void main(String[] args) {
new GetterText();
}
}
Edit: elaboration on the use of super
super gives a reference to the superclass which you can use to call its methods. When overriding a method of the superclass, calling that superclass's method means "do what you did before", or, "retain the implementation". This is good because you start from a point where everything works as the default and what you have left to do is tweak specific behaviors without the need to take care for all the others.
In this case, if you return label after
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
you would get the default behavior because it is the one being used by the extending DefaultListCellRenderer. The arguments are the same in order to get the same result as the superclass would give. Then I go on to change this default JLabel. What you do is create a new JLabel with no default behavior. Note that I "cheat" here by knowing that the Component returned by DefaultListCellRenderer.getListCellRendererComponent is a JLabel.
ListCellRenderer acts like a stamp. The same instance with the same data is reused. You have to set the correct color every time.
if(cbNameIsMatched[index]==2)
{
setBackground(Color.YELLOW);
setEnabled(false);
}
else
{
setBackground(your default color);
setEnabled(true);
}

Set the button "background" of a Nimbus button

I'm working on an app using the Nimbus Look and Feel. There's a table and one column contains buttons (using the Table Button Column from Rob Camick). That does work, but the result isn't what I had expected. I have tried to fix the look, but to no avail.
So the question is: how do I change the "background" (the area outside the rounded rectangle) of a Nimbus button? Preferably in a non-hacky way :-)
Using the default Table Column Button, the result looks like this:
As you can see, the background (and by this I mean the area outside the button's rounded rectangle) is wrong for the odd (white) rows. The code that produces this output is:
public Component getTableCellRendererComponent(
JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
{
if (isSelected) {
renderButton.setForeground(table.getSelectionForeground());
renderButton.setBackground(table.getSelectionBackground());
} else {
renderButton.setForeground(table.getForeground());
renderButton.setBackground(table.getBackground());
}
if (hasFocus) {
renderButton.setBorder( focusBorder );
} else {
renderButton.setBorder( originalBorder );
}
// <snip some code>
renderButton.setOpaque(true);
return renderButton;
}
The renderButton is an instance of a default JButton. I've tried messing with the background color of the button, but that didn't work out like I expected at first:
Color alternate = (Color)LookAndFeel.getDesktopPropertyValue("Table.alternateRowColor", Color.lightGray);
Color normal = (Color)LookAndFeel.getDesktopPropertyValue("Table.background", Color.white);
if (row % 2 == 0) {
renderButton.setBackground(normal);
} else {
renderButton.setBackground(alternate);
}
This produces:
So this time the buttons that look alright in the first image are now bad and vice versa. The button's inner backgrounds (the areas inside the rounded rectangles) do seem to have the correct color according to the background color property (which is what's really modified with the setBackground() call). But the area outside is all wrong. Alright, let's combine the two :
Color alternate = table.getBackground();
Color normal = (Color)LookAndFeel.getDesktopPropertyValue("Table.background", Color.white);
if (row % 2 == 0) {
renderButton.setBackground(normal);
} else {
renderButton.setBackground(alternate);
}
The result:
So now the "background" does look correct, but the buttons don't look like Nimbus buttons any more. How do I make the "background" have the correct color while still looking like Nimbus buttons?
Below's a hacky way, following up on #Piro's suggestion: using a JPanel with the button as child component. Which in itself is a nice idea, given that we don't really want to touch the "inner" background visuals of the button.
Here the hack comes when forcing Nimbus internals to not use a JPanel's default background for filling its area but instead use the background of the given panel instance This needs relying on implementation details, particularly the lookup mechanism of a background color. That happens in SynthStyle.getColor():
// If the developer has specified a color, prefer it. Otherwise, get
// the color for the state.
Color color = null;
if (!id.isSubregion()) {
if (type == ColorType.BACKGROUND) {
color = c.getBackground();
}
....
}
if (color == null || color instanceof UIResource) {
// Then use what we've locally defined
color = getColorForState(context, type);
}
Translated: it does indeed query the instance's color, but overrules it with the default if the instance color is a UIResource - which typically is the case if used as a renderer. So the trick out (tried unsuccessfully by SynthBooleanRenderer, but that's another story ;-) is to make the instance color not a UIResource. An additional quirk is that being UIResource is necessary to ensure the striping color - which is not of type UIResource, haha - be applied ... intuitive, isn't it ...
public class RendererPanel implements TableCellRenderer {
private JComponent panel;
private JButton button;
public RendererPanel() {
panel = new JPanel(new BorderLayout());
panel.setBorder(BorderFactory.createEmptyBorder(3, 10, 2, 10));
button = new JButton();
panel.add(button);
}
#Override
public Component getTableCellRendererComponent(JTable table,
Object value, boolean isSelected, boolean hasFocus, int row,
int column) {
// suggestion by Piro - use background of default
DefaultTableCellRenderer dt = (DefaultTableCellRenderer) table.getDefaultRenderer(Object.class);
dt.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
// first try: set the color as-is - doesn't work
// panel.setBackground(dt.getBackground());
// second try: set color as not ui-resource
// that's working because at this point we already have the color that will be used
// let's hinder synth background color searching to fall back to component defaults
panel.setBackground(new Color(dt.getBackground().getRGB()));
// hack: unwrap ui-resource as needed
// updateBackground(isSelected ? table.getSelectionBackground() : table.getBackground(), row);
button.setText(String.valueOf(value));
return panel;
}
private void updateBackground(Color color, int row) {
Color hack = row % 2 == 0 ? unwrap(color) : color;
panel.setBackground(hack);
}
private Color unwrap(Color c) {
if (c instanceof UIResource) {
return new Color(c.getRGB());
}
return c;
}
}
Screenshot: with unwrap hack
Screenshot: using default colors (from the renderer installed for Object.class)
The non-hacky way out might be (didn't try here, but remember having done once) to register a Region with the style, similarly to what NimbusDefaults does internally:
register(Region.PANEL, "Table:\"Table.cellRenderer\"");
Problem here being that there's no public api to do so (or could be that I simply don't know enough about Synth ;-)
Do not set background to JButton. Use JPanel to wrap JButton and set background to JPanel. This would be probably obvious if you used more buttons in one JTable column.
To set correct background color of JPanel i did (you should):
Keep reference to original renderer
Let original renderer render its own component (for every rendering)!
Use background of rendered component to set background of JPanel (for every rendering)!
This way you don't have to choose correct color yourself
Also you have to override paintComponent to correctly paint white background of JPanel:
#Override
protected void paintComponent(Graphics g) {
Color background = getBackground();
setBackground(new Color(background.getRGB()));
super.paintComponent(g);
}
Edit: as #kleopatra suggests you don't have to override paintComponent, only set background color as not-uiresource (shown in complete example)
Here is complete example:
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;
import javax.swing.table.TableCellRenderer;
public class Test {
public static void main(String[] args) throws Throwable {
for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
if ("Nimbus".equals(info.getName())) {
UIManager.setLookAndFeel(info.getClassName());
break;
}
}
String[] columnNames = new String[]{"c1"};
Object[][] data = new Object[4][1];
data[0][0] = "First";
data[1][0] = "Second";
data[2][0] = "Third";
data[3][0] = "Fourth";
JTable table = new JTable(data, columnNames){
#Override
public javax.swing.table.TableCellRenderer getCellRenderer(int row, int column) {
final TableCellRenderer ori = super.getCellRenderer(row, column);
final TableCellRenderer mine = new TableCellRenderer() {
#Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
Component c = ori.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
JPanel p = new JPanel();
if(value == null){
value = "";
}
p.add(new JButton(value.toString()));
p.setBackground(new Color(c.getBackground().getRGB()));
return p;
}
};
return mine;
};
};
table.setRowHeight(50);
JFrame f = new JFrame();
f.add(table);
f.setVisible(true);
f.pack();
}
}
Result:

using a custom Swing JComponent in a TableCellRenderer

OK, I know how to make a simple custom JComponent. I know how to override a TableCellRenderer. I can't seem to combine the two.
Here's a sample JComponent I created:
public static class BarRenderer extends JComponent
{
final private double xmin;
final private double xmax;
private double xval;
public BarRenderer(double xmin, double xmax)
{
this.xmin=xmin;
this.xmax=xmax;
}
#Override protected void paintComponent(Graphics g)
{
super.paintComponent(g);
Rectangle r = g.getClipBounds();
g.drawRect(r.x, r.y,
(int)(r.width * ((xval-xmin)/(xmax-xmin))), r.height);
}
public void setXval(double x) {
this.xval = x;
repaint();
}
public double getXval() { return xval; }
}
It works fine as a standalone JComponent. I call setXval(something) and it updates just fine. (edit: I have a Swing Timer that updates the data periodically)
But if this component is something I return in TableCellRenderer.getTableCellRendererComponent(), then it only repaints when I click on the cell in question. What gives? I must be leaving out something really simple.
For performance reasons a JTable reuses renderer components to paint multiple cells - so when you see the component in the JTable it isn't actually there in the traditional sense of a Component in a Container which is present at a location. This means that calling repaint() on the renderer component does nothing.
The most effective option would be to store the Integer value of the bar in your TableModel. Your TableCellRenderer would then look something like this:
public class BarTableCellRenderer implements TableCellRenderer {
private final BarRenderer rendererComponent = new BarRenderer(0, 10);
#Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
rendererComponent.setXval((Integer)value);
return rendererComponent;
}
}
Then you could change the Integer in your TableModel and it would trigger a repaint of the bar (you may need a TableModel.fireTableCellUpdated dependent on the TableModel implementation you are using).
Both of you (Russ Hayward and Andrew) helped, the key was essentially to do the following:
store the state to be made visible in the TableModel itself, not in the renderer
make sure that when the TableModel's state changes, fireTableCellUpdated() is called
have only one TableCellRenderer object and one JComponent for my custom column (not one per cell)
within TableCellRenderer.getTableCellRendererComponent() store the cell's state for purposes of being rendering soon after (long-term storage is in the TableModel)
provide that state to the JComponent
return the JComponent
override JComponent.PaintComponent()
one convenient possibility is for a custom renderer to extend JComponent and implement TableCellRenderer, then in TableCellRenderer.getTableCellRendererComponent() you store the cell's state and return this;
Here's the relevant excerpt of my code that now works:
class TraceControlTableModel extends AbstractTableModel {
/* handle table state here */
// convenience method for setting bar value (table model's column 2)
public void setBarValue(int row, double x)
{
setValueAt(x, row, 2);
}
}
// one instance of BarRenderer will be set as the
// TableCellRenderer for table column 2
public static class BarRenderer extends JComponent
implements TableCellRenderer
{
final private double xmin;
final private double xmax;
private double xval;
public BarRenderer(double xmin, double xmax)
{
super();
this.xmin=xmin;
this.xmax=xmax;
}
#Override protected void paintComponent(Graphics g)
{
super.paintComponent(g);
Rectangle r = g.getClipBounds();
g.drawRect(r.x, r.y,
(int)(r.width * ((xval-xmin)/(xmax-xmin))), r.height);
}
#Override
public Component getTableCellRendererComponent(JTable arg0,
Object value,
boolean isSelected, boolean hasFocus,
int row, int col)
{
// save state here prior to returning this object as a component
// to be painted
this.xval = (Double)value;
return this;
}
}
If you make a table with say 3 rows, each having a different Xval, then does it initially renderer correctly, meaning each cell has a different looking bar?
When you say it does not repaint unless you click it, has something happened to your underlying data that should have caused the visual display of the data (the rendered bar) to change?
If the data changed, but the table does not immediatley re-render, then I would say that your TableModel is not working properly.
underlying data changes -> TableModel changes -> fires TableModelEvent -> JTable re-renders
Look at the TableModel tuturial: http://java.sun.com/docs/books/tutorial/uiswing/components/table.html#data
to make sure you are doing everything correct.

How do I combine a Combo Box with a Tree in Swing?

For my application, I want a Combo Box that displays its elements when dropped down as a Tree. Problem is, I'm not versed well enough in Swing to know how to go about doing this. At least without ending up writing a new widget from scratch, or something to that effect.
How would I do something like this without creating one from scratch?
I think I would implement this as a JTree component in a JViewPort, followed by an expansion button. When collapsed, it would look like a combo box. When you click the expansion button, the viewport would expand, allowing you to scroll and select a node in the JTree. When you selected the node, the view port would collapse back to only show the selected node and the expansion button.
Hey, guess what! This is your lucky day.
I've used this framework in the past. It is very complete. I didn't know they have this
already.
JIDE Soft
alt text http://img89.imageshack.us/img89/8324/combotreejj1.png
Is not too expensive, but it will take you some time to understand the API ( it is not that is complex, but they've created a LOT of new stuff )
Override the getListCellRendererComponent methode and create the components in level order.
For every tree level move the painted string 3 spaces to right.
Example:
1
. a
. b
2
. c
The original implementation you can look from
public Component getListCellRendererComponent(
JList list,
Object value,
int index,
boolean isSelected,
boolean cellHasFocus) {
//Get the selected index. (The index param isn't
//always valid, so just use the value.)
int selectedIndex = ((Integer)value).intValue();
if (isSelected) {
setBackground(list.getSelectionBackground());
setForeground(list.getSelectionForeground());
} else {
setBackground(list.getBackground());
setForeground(list.getForeground());
}
//Set the icon and text. If icon was null, say so.
ImageIcon icon = images[selectedIndex];
String pet = petStrings[selectedIndex];
setIcon(icon);
if (icon != null) {
setText(pet);
setFont(list.getFont());
} else {
setUhOhText(pet + " (no image available)",
list.getFont());
}
return this;
}
You can create a ComboBoxEditor whose component ( returned by getEditorComponent ) is a JTree
Although you may have already tried that.
I don't know how would it look like. Post an screenshot if you make it work. :)
EDIT
I give it a quick dirty try. Its awful, but is a start.
alt text http://img120.imageshack.us/img120/2563/yiakxk2.png
Here's the code, for what is worth. :(
Probably you should start thinking in alternatives. What about a fake Combo that is a JButton without border when pushed a hidden panel will appear with the tree displayed.
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
public class ComboTree {
public static void main( String [] args ) {
JComboBox c = new JComboBox( new String [] { "Hello", "there"});
c.setModel( new CustomComboModel() );
c.setEditor( new TreeComboEditor() );
c.setRenderer( new TreeComboEditor() );
JFrame frame = new JFrame();
frame.add( c , BorderLayout.NORTH ) ;
frame.pack();
frame.setVisible( true );
}
}
class CustomComboModel implements ComboBoxModel {
public Object getSelectedItem() { return ":P"; }
public void setSelectedItem(Object anItem) {}
public void addListDataListener(ListDataListener l) {}
public Object getElementAt(int index) { return "at " + index ; }
public int getSize() { return 2; }
public void removeListDataListener(ListDataListener l) {}
}
class TreeComboEditor implements ComboBoxEditor, ListCellRenderer {
// Editor interface
public void addActionListener(ActionListener l) {}
public Component getEditorComponent() {
return new JTree() ;
}
public Object getItem() { return "";}
public void removeActionListener(ActionListener l) {}
public void selectAll() {}
public void setItem(Object anObject) {}
// Render interface
public Component getListCellRendererComponent(JList list,
Object value,
int index,
boolean isSelected,
boolean cellHasFocus) {
return new JTree();
}
}

How do I make a list with checkboxes in Java Swing?

What would be the best way to have a list of items with a checkbox each in Java Swing?
I.e. a JList with items that have some text and a checkbox each?
A wonderful answer is this CheckBoxList. It implements Telcontar's answer (though 3 years before :)... I'm using it in Java 1.6 with no problems. I've also added an addCheckbox method like this (surely could be shorter, haven't used Java in a while):
public void addCheckbox(JCheckBox checkBox) {
ListModel currentList = this.getModel();
JCheckBox[] newList = new JCheckBox[currentList.getSize() + 1];
for (int i = 0; i < currentList.getSize(); i++) {
newList[i] = (JCheckBox) currentList.getElementAt(i);
}
newList[newList.length - 1] = checkBox;
setListData(newList);
}
I tried out the demo for the Jidesoft stuff, playing with the CheckBoxList I encountered some problems (behaviors that didn't work). I'll modify this answer if I find problems with the CheckBoxList I linked to.
Create a custom ListCellRenderer and asign it to the JList.
This custom ListCellRenderer must return a JCheckbox in the implementantion of getListCellRendererComponent(...) method.
But this JCheckbox will not be editable, is a simple paint in the screen is up to you to choose when this JCheckbox must be 'ticked' or not,
For example, show it ticked when the row is selected (parameter isSelected), but this way the check status will no be mantained if the selection changes. Its better to show it checked consulting the data below the ListModel, but then is up to you to implement the method who changes the check status of the data, and notify the change to the JList to be repainted.
I Will post sample code later if you need it
ListCellRenderer
Just implement a ListCellRenderer
public class CheckboxListCellRenderer extends JCheckBox implements ListCellRenderer {
public Component getListCellRendererComponent(JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
setComponentOrientation(list.getComponentOrientation());
setFont(list.getFont());
setBackground(list.getBackground());
setForeground(list.getForeground());
setSelected(isSelected);
setEnabled(list.isEnabled());
setText(value == null ? "" : value.toString());
return this;
}
}
and set the renderer
JList list = new JList();
list.setCellRenderer(new CheckboxListCellRenderer());
this will result in
Details at Custom swing component renderers.
PS: If you want radio elements just replace extends JCheckbox with extends JRadioButton.
I'd probably be looking to use a JTable rather than a JList and since the default rendering of a checkbox is rather ugly, I'd probably be looking to drop in a custom TableModel, CellRenderer and CellEditor to represent a boolean value. Of course, I would imagine this has been done a bajillion times already. Sun has good examples.
Better solution for Java 7 and newer
I stumbled upon this question and realized that some of the answers are pretty old and outdated. Nowadays, JList is generic and thus there are better solutions.
My solution of the generic JCheckBoxList:
import java.awt.Component;
import javax.swing.*;
import javax.swing.border.*;
import java.awt.event.*;
#SuppressWarnings("serial")
public class JCheckBoxList extends JList<JCheckBox> {
protected static Border noFocusBorder = new EmptyBorder(1, 1, 1, 1);
public JCheckBoxList() {
setCellRenderer(new CellRenderer());
addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
int index = locationToIndex(e.getPoint());
if (index != -1) {
JCheckBox checkbox = (JCheckBox) getModel().getElementAt(index);
checkbox.setSelected(!checkbox.isSelected());
repaint();
}
}
});
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
}
public JCheckBoxList(ListModel<JCheckBox> model){
this();
setModel(model);
}
protected class CellRenderer implements ListCellRenderer<JCheckBox> {
public Component getListCellRendererComponent(
JList<? extends JCheckBox> list, JCheckBox value, int index,
boolean isSelected, boolean cellHasFocus) {
JCheckBox checkbox = value;
//Drawing checkbox, change the appearance here
checkbox.setBackground(isSelected ? getSelectionBackground()
: getBackground());
checkbox.setForeground(isSelected ? getSelectionForeground()
: getForeground());
checkbox.setEnabled(isEnabled());
checkbox.setFont(getFont());
checkbox.setFocusPainted(false);
checkbox.setBorderPainted(true);
checkbox.setBorder(isSelected ? UIManager
.getBorder("List.focusCellHighlightBorder") : noFocusBorder);
return checkbox;
}
}
}
For dynamically adding JCheckBox lists you need to create your own ListModel or add the DefaultListModel.
DefaultListModel<JCheckBox> model = new DefaultListModel<JCheckBox>();
JCheckBoxList checkBoxList = new JCheckBoxList(model);
The DefaultListModel are generic and thus you can use methods specified by JAVA 7 API here like this:
model.addElement(new JCheckBox("Checkbox1"));
model.addElement(new JCheckBox("Checkbox2"));
model.addElement(new JCheckBox("Checkbox3"));
I recommend you use a JPanel with a GridLayout of 1 column. Add the checkBoxes to the JPanel, and set the JPanel as the data source of a JScrollPane. And to get the selected CheckBoxes, just call the getComponents() of the JPanel to get the CheckBoxes.
Odds are good w/ Java that someone has already implemented the widget or utility you need. Part of the benefits of a large OSS community. No need to reinvent the wheel unless you really want to do it yourself. In this case it would be a good learning exercise in CellRenderers and Editors.
My project has had great success with JIDE. The component you want, a Check Box List, is in the JIDE Common Layer (which is OSS and hosted on java.net). The commercial stuff is good too, but you don't need it.
http://www.jidesoft.com/products/oss.htm
https://jide-oss.dev.java.net/
I don't like the solutions that put a Checkbox into the model. The model should only contain data not display elements.
I found this http://www.java2s.com/Tutorials/Java/Swing_How_to/JList/Create_JList_of_CheckBox.htm
which I optimized a bit. The ACTIVE flag represents the Checkbox, the SELECTED flag shows what entry the cursor sits on.
my version requires a renderer
import java.awt.Component;
import javax.swing.JCheckBox;
import javax.swing.JList;
import javax.swing.ListCellRenderer;
class CheckListRenderer extends JCheckBox implements ListCellRenderer<Entity> {
#Override
public Component getListCellRendererComponent(JList<? extends Entity> list,
Entity value, int index, boolean isSelected, boolean cellHasFocus) {
setEnabled(list.isEnabled());
setSelected(value.isActive()); // sets the checkbox
setFont(list.getFont());
if (isSelected) { // highlights the currently selected entry
setBackground(list.getSelectionBackground());
setForeground(list.getSelectionForeground());
} else {
setBackground(list.getBackground());
setForeground(list.getForeground());
}
setText(value.toString()+" - A" + value.isActive()+" - F"+cellHasFocus+" - S"+isSelected );
return this;
}
}
and an entity that got the active field:
public class Entity {
private boolean active = true;
public boolean isActive() {
return active;
}
public void setActive(boolean isActive) {
this.active = isActive;
}
}
Now you only have to add this to your JList:
list = new JList<Entity>();
list.setModel(new DefaultListModel<Entity>());
list.setCellRenderer(new CheckListRenderer());
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent event) {
if (event.getX() < 20) {
// Quick and dirty: only change the tick if clicked into the leftmost pixels
#SuppressWarnings("unchecked")
JList<Entity> list = ((JList<Entity>) event.getSource());
int index = list.locationToIndex(event.getPoint());// Get index of item clicked
if (index >= 0) {
Entity item = (Entity) list.getModel().getElementAt(index);
item.setActive(!item.isActive()); // Toggle selected state
list.repaint(list.getCellBounds(index, index));// Repaint cell
}
}
}
});
All of the aggregate components in Swing--that is, components made up other components, such as JTable, JTree, or JComboBox--can be highly customized. For example, a JTable component normally displays a grid of JLabel components, but it can also display JButtons, JTextFields, or even other JTables. Getting these aggregate components to display non-default objects is the easy part, however. Making them respond properly to keyboard and mouse events is a much harder task, due to Swing's separation of components into "renderers" and "editors." This separation was (in my opinion) a poor design choice and only serves to complicate matters when trying to extend Swing components.
To see what I mean, try enhancing Swing's JList component so that it displays checkboxes instead of labels. According to Swing philosophy, this task requires implementing two interfaces: ListCellRenderer (for drawing the checkboxes) and CellEditor (for handling keyboard and mouse events on the checkboxes). Implementing the ListCellRenderer interface is easy enough, but the CellEditor interface can be rather clumsy and hard to understand. In this particular case, I would suggest forgetting CellEditor entirely and to handle input events directly, as shown in the following code.
import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
import java.awt.event.*;
public class CheckBoxList extends JList
{
protected static Border noFocusBorder = new EmptyBorder(1, 1, 1, 1);
public CheckBoxList()
{
setCellRenderer(new CellRenderer());
addMouseListener(new MouseAdapter()
{
public void mousePressed(MouseEvent e)
{
int index = locationToIndex(e.getPoint());
if (index != -1) {
JCheckBox checkbox = (JCheckBox)
getModel().getElementAt(index);
checkbox.setSelected(
!checkbox.isSelected());
repaint();
}
}
}
);
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
}
protected class CellRenderer implements ListCellRenderer
{
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus)
{
JCheckBox checkbox = (JCheckBox) value;
checkbox.setBackground(isSelected ?
getSelectionBackground() : getBackground());
checkbox.setForeground(isSelected ?
getSelectionForeground() : getForeground());
checkbox.setEnabled(isEnabled());
checkbox.setFont(getFont());
checkbox.setFocusPainted(false);
checkbox.setBorderPainted(true);
checkbox.setBorder(isSelected ?
UIManager.getBorder(
"List.focusCellHighlightBorder") : noFocusBorder);
return checkbox;
}
}
}
Here, I intercept mouse clicks from the listbox and simulate a click on the appropriate checkbox. The result is a "CheckBoxList" component that is both simpler and smaller than an equivalent component using the CellEditor interface. To use the class, simply instantiate it, then pass it an array of JCheckBox objects (or subclasses of JCheckBox objects) by calling setListData. Note that the checkboxes in this component will not respond to keypresses (i.e. the spacebar), but you could always add your own key listener if needed.
Source: DevX.com
Here is just a little addition to the JCheckBoxList by Rawa. This will add the ability to select using space bar. If multiple items are selected, all will be set to inverted value of the first item.
addKeyListener(new KeyAdapter() {
#Override
public void keyPressed(KeyEvent e) {
int index = getSelectedIndex();
if (index != -1 && e.getKeyCode() == KeyEvent.VK_SPACE) {
boolean newVal = !((JCheckBox) (getModel()
.getElementAt(index))).isSelected();
for (int i : getSelectedIndices()) {
JCheckBox checkbox = (JCheckBox) getModel()
.getElementAt(i);
checkbox.setSelected(newVal);
repaint();
}
}
}
});
this is yet another example of making list with checkboxes
class JCheckList<T> extends JList<T> {
protected static Border noFocusBorder = new EmptyBorder(1, 1, 1, 1);
public void setSelected(int index) {
if (index != -1) {
JCheckBox checkbox = (JCheckBox) getModel().getElementAt(index);
checkbox.setSelected(
!checkbox.isSelected());
repaint();
}
}
protected static class CellListener
extends DefaultListModel
implements ListDataListener {
ListModel ls;
public CellListener(ListModel ls) {
ls.addListDataListener(this);
int i = ls.getSize();
for (int v = 0; v < i; v++) {
var r = new JCheckBox();
r.setText(ls.getElementAt(v).toString());
this.addElement(r);
}
this.ls = ls;
}
#Override
public void intervalAdded(ListDataEvent e) {
int begin = e.getIndex0();
int end = e.getIndex1();
for (; begin <= end; begin++) {
var r = new JCheckBox();
r.setText(ls.getElementAt(begin).toString());
this.add(begin, r);
}
}
#Override
public void intervalRemoved(ListDataEvent e) {
int begin = e.getIndex0();
int end = e.getIndex1();
for (; begin <= end; end--) {
this.remove(begin);
}
}
#Override
public void contentsChanged(ListDataEvent e) {
}
}
public JCheckList() {
setCellRenderer(new CellRenderer());
addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
int index = locationToIndex(e.getPoint());
setSelected(index);
}
}
);
addKeyListener(new KeyListener(){
#Override
public void keyTyped(KeyEvent e) {
}
#Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SPACE){
int index = JCheckList.this.getSelectedIndex();
setSelected(index);
}
}
#Override
public void keyReleased(KeyEvent e) {
}
});
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
}
#Override
public void setModel(ListModel<T> d) {
var r = new CellListener(d);
d.addListDataListener(r);
super.setModel(r);
}
protected class CellRenderer implements ListCellRenderer {
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JCheckBox checkbox = (JCheckBox) value;
checkbox.setBackground(isSelected
? getSelectionBackground() : getBackground());
checkbox.setForeground(isSelected
? getSelectionForeground() : getForeground());
checkbox.setEnabled(isEnabled());
checkbox.setFont(getFont());
checkbox.setFocusPainted(false);
checkbox.setBorderPainted(true);
checkbox.setBorder(isSelected
? UIManager.getBorder(
"List.focusCellHighlightBorder") : noFocusBorder);
return checkbox;
}
}
}

Categories

Resources