I'm trying to create a custom cell renderer that will display an image in JTable's header cell. I've gotten the source code to work with the Metal L&F but I am encountering problems with Nimbus. Under normal circumstances, Nimbus displays the image just fine. However, when a table is sorted, Nimbus will draw the sort icon instead of the icon I've specified. This is different than the Metal L&F, as that will always draw the icon I've provided.

Does anyone know of a way to have Nimbus draw the image even if a column is sorted?
I'm using Java 6.29 & Nimbus. I can't change the Java release or the L&F.
Also, I've tried to do some other workarounds, like changing the label to use HTML and and img tag to display the image, but this produces a weird visual effect. EDIT The text and image aren't aligned well (even with a HTML align tag on the img tag) Here is a picture, notice how the text in the Temp Hi column doesn't align:

import java.awt.Component;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
public class ImageChangeDemo extends JFrame {
public static void main(String args[]) {
//comment out the code below to try in Metal L&F
try {
for(javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.
getInstalledLookAndFeels()) {
if("Nimbus".equals(info.getName())) {
javax.swing.UIManager.setLookAndFeel(info.getClassName());
break;
}
}
}
catch(Exception ex) {
ex.printStackTrace();
}
java.awt.EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
new ImageChangeDemo().setVisible(true);
}
});
}
public ImageChangeDemo(){
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
JScrollPane pane = new javax.swing.JScrollPane();
JTable table = new javax.swing.JTable();
table.setAutoCreateRowSorter(true);
table.setModel(new javax.swing.table.DefaultTableModel(
new Object [][] {
{"a", "q", "h", "v"},
{"b", "m", "l", "h"},
{"d", "c", "a", "d"},
{"j", "o", "y", "e"}
},
new String [] {
"Col 1", "Col 2", "Col 3", "Col 4"
}
) {
Class[] types = new Class [] {
String.class, String.class, String.class, String.class
};
@Override
public Class getColumnClass(int columnIndex) {
return types [columnIndex];
}
});
pane.setViewportView(table);
this.add(pane);
table.getTableHeader().setDefaultRenderer(new ImageRenderer(table));
pack();
}
public class ImageRenderer extends DefaultTableCellRenderer{
TableCellRenderer orig;
ImageIcon icon;
ImageRenderer(JTable table){
orig = table.getTableHeader().getDefaultRenderer();
}
@Override
public Component getTableCellRendererComponent(final JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
Component c =
orig.getTableCellRendererComponent(
table, value, isSelected, hasFocus, row, column);
if(c instanceof JLabel){
if(true){
JLabel label = (JLabel)c;
label.setIcon(makeIcon());
}
}
return c;
}
public ImageIcon makeIcon(){
if(icon == null)
icon = new ImageIcon(
ImageChangeDemo.class.getResource("/resources/green_triangle_down.png"));
return icon;
}
}
}
EDIT: Here is an example scenario of what my real application should do: If the table column contains certain data (such as any strings beginning with a certain word) display a warning icon next to the column name in the table header. I've gotten this to work fine. Now, if the user sorts a column with the image, Nimbus is removing the image and replacing it with a sort icon - I still want the original warning icon to display.
don't recreate an Icon inside Renderer, prepare that before, otherwise you'll recreating Icon in the crazy periods
not to add Icon to the Component / JComponent / JLabel returns Renderer
put to the Renderer
code made by Darryl or Rob
protected Icon getIcon(JTable table, int column) {
SortKey sortKey = getSortKey(table, column);
if (sortKey != null && table.convertColumnIndexToView(
sortKey.getColumn()) == column) {
switch (sortKey.getSortOrder()) {
case ASCENDING:
return UIManager.getIcon("Table.ascendingSortIcon");
case DESCENDING:
return UIManager.getIcon("Table.descendingSortIcon");
}
}
return null;
}
EDIT
thank to Renderer by @trashgod, UNSORTED isn't required to override for Renderer, try & enjoy
initial view

ASCENDING

DESCENDING

UNSORTED

import java.awt.Component;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Icon;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.RowSorter;
import javax.swing.SortOrder;
import javax.swing.UIManager;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;
public class ImageChangeDemo extends JFrame {
private static final long serialVersionUID = 1L;
private JTable table = new javax.swing.JTable();
public static void main(String args[]) {
//comment out the code below to try in Metal L&F
try {
for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
if ("Nimbus".equals(info.getName())) {
javax.swing.UIManager.setLookAndFeel(info.getClassName());
break;
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
java.awt.EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
new ImageChangeDemo().setVisible(true);
}
});
}
public ImageChangeDemo() {
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
JScrollPane pane = new javax.swing.JScrollPane();
table.setModel(new javax.swing.table.DefaultTableModel(
new Object[][]{
{"a", "q", "h", "v"},
{"b", "m", "l", "h"},
{"d", "c", "a", "d"},
{"j", "o", "y", "e"}
},
new String[]{
"Col 1", "Col 2", "Col 3", "Col 4"
}) {
private static final long serialVersionUID = 1L;
Class[] types = new Class[]{
String.class, String.class, String.class, String.class
};
@Override
public Class getColumnClass(int columnIndex) {
return types[columnIndex];
}
});
TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(table.getModel()) {
@Override
public void toggleSortOrder(int column) {
if (column >= 0 && column < getModelWrapper().getColumnCount() && isSortable(column)) {
List<SortKey> keys = new ArrayList<SortKey>(getSortKeys());
if (!keys.isEmpty()) {
SortKey sortKey = keys.get(0);
if (sortKey.getColumn() == column && sortKey.getSortOrder() == SortOrder.DESCENDING) {
setSortKeys(null);
return;
}
}
}
super.toggleSortOrder(column);
}
};
table.setRowSorter(sorter);
table.setPreferredScrollableViewportSize(table.getPreferredSize());
table.setDefaultRenderer(ImageChangeDemo.class, new HeaderRenderer(table));
pane.setViewportView(table);
add(pane);
pack();
}
class HeaderRenderer implements TableCellRenderer {
final TableCellRenderer renderer;
public HeaderRenderer(JTable table) {
renderer = table.getTableHeader().getDefaultRenderer();
}
@Override
public Component getTableCellRendererComponent(
JTable table, Object value, boolean isSelected,
boolean hasFocus, int row, int col) {
return renderer.getTableCellRendererComponent(
table, value, isSelected, hasFocus, row, col);
}
public Icon getIcon(JTable table, int column) {
for (RowSorter.SortKey sortKey : table.getRowSorter().getSortKeys()) {
if (sortKey.getColumn() == column) {
switch (sortKey.getSortOrder()) {
case ASCENDING:
return (UIManager.getIcon("Table.ascendingSortIcon"));
case DESCENDING:
return (UIManager.getIcon("Table.descendingSortIcon"));
}
}
}
return null;
}
}
}
EDIT 2
then to set Icon directly to the UIManager


import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Icon;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.RowSorter.SortKey;
import javax.swing.SortOrder;
import javax.swing.UIManager;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;
public class ImageChangeDemo extends JFrame {
private static final long serialVersionUID = 1L;
private JTable table = new javax.swing.JTable();
public static void main(String args[]) {
//comment out the code below to try in Metal L&F
try {
for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
if ("Nimbus".equals(info.getName())) {
javax.swing.UIManager.setLookAndFeel(info.getClassName());
UIManager.getLookAndFeelDefaults().put("Table.ascendingSortIcon", new BevelArrowIcon(BevelArrowIcon.UP, false, false));
UIManager.getLookAndFeelDefaults().put("Table.descendingSortIcon", new BevelArrowIcon(BevelArrowIcon.DOWN, false, false));
break;
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
java.awt.EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
new ImageChangeDemo().setVisible(true);
}
});
}
public ImageChangeDemo() {
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
JScrollPane pane = new javax.swing.JScrollPane();
//table.setAutoCreateRowSorter(true);
table.setModel(new javax.swing.table.DefaultTableModel(
new Object[][]{
{"a", "q", "h", "v"},
{"b", "m", "l", "h"},
{"d", "c", "a", "d"},
{"j", "o", "y", "e"}
},
new String[]{
"Col 1", "Col 2", "Col 3", "Col 4"
}) {
private static final long serialVersionUID = 1L;
Class[] types = new Class[]{
String.class, String.class, String.class, String.class
};
@Override
public Class getColumnClass(int columnIndex) {
return types[columnIndex];
}
});
TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(table.getModel()) {
@Override
public void toggleSortOrder(int column) {
if (column >= 0 && column < getModelWrapper().getColumnCount() && isSortable(column)) {
List<SortKey> keys = new ArrayList<SortKey>(getSortKeys());
if (!keys.isEmpty()) {
SortKey sortKey = keys.get(0);
if (sortKey.getColumn() == column && sortKey.getSortOrder() == SortOrder.DESCENDING) {
setSortKeys(null);
return;
}
}
}
super.toggleSortOrder(column);
}
};
table.setRowSorter(sorter);
//table.getTableHeader().setDefaultRenderer(new DefaultTableHeaderCellRenderer());
//table.setDefaultRenderer(ImageChangeDemo.class, new HeaderRenderer(table));
table.setPreferredScrollableViewportSize(table.getPreferredSize());
pane.setViewportView(table);
add(pane);
pack();
}
static class BevelArrowIcon implements Icon {
public static final int UP = 0; // direction
public static final int DOWN = 1;
private static final int DEFAULT_SIZE = 11;
private Color edge1;
private Color edge2;
private Color fill;
private int size;
private int direction;
public BevelArrowIcon(int direction, boolean isRaisedView, boolean isPressedView) {
if (isRaisedView) {
if (isPressedView) {
init(UIManager.getColor("controlLtHighlight"), UIManager.getColor("controlDkShadow"), UIManager.getColor("controlShadow"), DEFAULT_SIZE, direction);
} else {
init(UIManager.getColor("controlHighlight"), UIManager.getColor("controlShadow"), UIManager.getColor("control"), DEFAULT_SIZE, direction);
}
} else {
if (isPressedView) {
init(UIManager.getColor("controlDkShadow"), UIManager.getColor("controlLtHighlight"), UIManager.getColor("controlShadow"), DEFAULT_SIZE, direction);
} else {
init(UIManager.getColor("controlShadow"), UIManager.getColor("controlHighlight"), UIManager.getColor("control"), DEFAULT_SIZE, direction);
}
}
}
public BevelArrowIcon(Color edge1, Color edge2, Color fill, int size, int direction) {
init(edge1, edge2, fill, size, direction);
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
switch (direction) {
case DOWN:
drawDownArrow(g, x, y);
break;
case UP:
drawUpArrow(g, x, y);
break;
}
}
@Override
public int getIconWidth() {
return size;
}
@Override
public int getIconHeight() {
return size;
}
private void init(Color edge1, Color edge2, Color fill, int size, int direction) {
edge1 = Color.red;
edge2 = Color.blue;
this.edge1 = edge1;
this.edge2 = edge2;
this.fill = fill;
this.size = size;
this.direction = direction;
}
private void drawDownArrow(Graphics g, int xo, int yo) {
g.setColor(edge1);
g.drawLine(xo, yo, xo + size - 1, yo);
g.drawLine(xo, yo + 1, xo + size - 3, yo + 1);
g.setColor(edge2);
g.drawLine(xo + size - 2, yo + 1, xo + size - 1, yo + 1);
int x = xo + 1;
int y = yo + 2;
int dx = size - 6;
while (y + 1 < yo + size) {
g.setColor(edge1);
g.drawLine(x, y, x + 1, y);
g.drawLine(x, y + 1, x + 1, y + 1);
if (0 < dx) {
g.setColor(fill);
g.drawLine(x + 2, y, x + 1 + dx, y);
g.drawLine(x + 2, y + 1, x + 1 + dx, y + 1);
}
g.setColor(edge2);
g.drawLine(x + dx + 2, y, x + dx + 3, y);
g.drawLine(x + dx + 2, y + 1, x + dx + 3, y + 1);
x += 1;
y += 2;
dx -= 2;
}
g.setColor(edge1);
g.drawLine(xo + (size / 2), yo + size - 1, xo + (size / 2), yo + size - 1);
}
private void drawUpArrow(Graphics g, int xo, int yo) {
g.setColor(edge1);
int x = xo + (size / 2);
g.drawLine(x, yo, x, yo);
x--;
int y = yo + 1;
int dx = 0;
while (y + 3 < yo + size) {
g.setColor(edge1);
g.drawLine(x, y, x + 1, y);
g.drawLine(x, y + 1, x + 1, y + 1);
if (0 < dx) {
g.setColor(fill);
g.drawLine(x + 2, y, x + 1 + dx, y);
g.drawLine(x + 2, y + 1, x + 1 + dx, y + 1);
}
g.setColor(edge2);
g.drawLine(x + dx + 2, y, x + dx + 3, y);
g.drawLine(x + dx + 2, y + 1, x + dx + 3, y + 1);
x -= 1;
y += 2;
dx += 2;
}
g.setColor(edge1);
g.drawLine(xo, yo + size - 3, xo + 1, yo + size - 3);
g.setColor(edge2);
g.drawLine(xo + 2, yo + size - 2, xo + size - 1, yo + size - 2);
g.drawLine(xo, yo + size - 1, xo + size, yo + size - 1);
}
}
}
So after much trial and error I was able to figure out a way to have my custom icon in the header row even if the column is sorted. Basically what I did was have the renderer return a custom panel containing 2 children, the image in a JLabel and the component that was originally produced by default renderer. (Note that this workaround is only necessary for Nimbus L&F, and the original example code works fine in the Metal L&F)
This code uses StackLayout created by Romain Guy as demonstrated in his book Filthy Rich Clients - see page p245. Here is the source for StackLayout
Here is the code I created for the renderer. Make sure to download StackLayout else this won't compile.
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FontMetrics;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
public class ImageChangeDemo extends JFrame {
public static void main(String args[]) {
//comment out the code below to try in Metal L&F
try {
for(javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.
getInstalledLookAndFeels()) {
if("Nimbus".equals(info.getName())) {
javax.swing.UIManager.setLookAndFeel(info.getClassName());
break;
}
}
}
catch(Exception ex) {
ex.printStackTrace();
}
java.awt.EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
new ImageChangeDemo().setVisible(true);
}
});
}
public ImageChangeDemo(){
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
JScrollPane pane = new javax.swing.JScrollPane();
JTable table = new javax.swing.JTable();
table.setAutoCreateRowSorter(true);
table.setModel(new javax.swing.table.DefaultTableModel(
new Object [][] {
{"a", "q", "h", "v"},
{"b", "m", "l", "h"},
{"d", "c", "a", "d"},
{"j", "o", "y", "e"}
},
new String [] {
"Col 1", "Col 2", "Col 3", "Col 4"
}
) {
Class[] types = new Class [] {
String.class, String.class, String.class, String.class
};
@Override
public Class getColumnClass(int columnIndex) {
return types [columnIndex];
}
});
pane.setViewportView(table);
this.add(pane);
pack();
//set renderer after pack so header row has correct default height
table.getTableHeader().setDefaultRenderer(new ImageRenderer(table));
}
public class ImageRenderer extends DefaultTableCellRenderer{
TableCellRenderer orig;
private final ImageIcon icon = new ImageIcon(
ImageChangeDemo.class.getResource("/resources/exclamation-icon.png"));;
private JPanel jp = new JPanel(new StackLayout());
private final JLabel pic = new JLabel(icon);
{ //extra initialization for PIC
pic.setHorizontalAlignment(JLabel.LEADING); //so it isn't centered in stack layout
}
ImageRenderer(JTable table){
orig = table.getTableHeader().getDefaultRenderer();
}
@Override
public Component getTableCellRendererComponent(final JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
Component c =
orig.getTableCellRendererComponent(
table, value, isSelected, hasFocus, row, column);
if(true){
int width = table.getColumnModel().getColumn(column).getWidth();
int height = table.getTableHeader().getSize().height;
System.out.println("height"+height);
jp.removeAll(); //clean the JPanel
//move text in label to the left so it isn't covered by the icon
if(c instanceof JLabel){
JLabel l = (JLabel) c;
l.setPreferredSize(new Dimension(width, height));
FontMetrics fontMetrics = l.getFontMetrics(l.getFont());
int sizeOfSpace = fontMetrics.charWidth(' ');
int numSpaces = (int)Math.round(icon.getIconWidth() / (double)sizeOfSpace);
StringBuilder sb = new StringBuilder();
for(int i = 0; i < numSpaces; i++)
sb.append(' ');
//account for HTML in header messages
if(l.getText().toLowerCase().startsWith("<html>")){
l.setText( l.getText().substring(0, "<html>".length()) +
sb.toString() +
l.getText().substring("<html>".length()));
}
else
l.setText(sb.toString()+l.getText());
}
//Add components to the JPanel & return it.
jp.add(c, StackLayout.BOTTOM); //will contain modifications for spacing.
jp.add(pic, StackLayout.TOP);
return jp;
}
else
return c;
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With