I want the column headers of my JTable to get an additional icon on their left side when their values are the base of the table's rowFilter. The sorting icon shows on the right side, so "glueing" the two icons into one doesn't count (although I also couldn't make that work properly for Nimbus...). I've tried a couple of ideas for a renderer but I couldn't do it... The descriptions of the "methods" and what's wrong with them are included in the posted code.
EDIT: forgot about a simple setIcon - in this case the problem is with the sort icon. Making the sort icon visible hides the other icon.
EDIT: this Nimbus TableHeader was not highlighted as 'pressed' gives an idea on how to solve the problems with the 3rd attempt below (the one using mockup background images). But I don't know how to know the values of MouseOver, Focused, etc... How can I get those as boolean values? (I mean a true/false for the mouseOver state, tru/false for the focused state etc so I can prepare a lookup table of the mockup images and get the right image for the current state of the column header)...
EDIT: To see the output for each of the three cases you have to modify the line table.getTableHeader().setDefaultRenderer(new FilterIconHeaderRenderer3(table));
with the wanted renderer class.
I've tried to show the problems with the three attempts on this image
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableRowSorter;
public class TableHeaderTest extends JFrame {
JTable table = new JTable(new DefaultTableModel(new Object[]{"Column1", "Column2", "Column3"}, 3));
TableHeaderTest() {
TableRowSorter sorter = new TableRowSorter(table.getModel());
table.setRowSorter(sorter);
table.getTableHeader().setDefaultRenderer(new FilterIconHeaderRenderer3(table));
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JScrollPane scrollPane = new JScrollPane();
scrollPane.setViewportView(table);
add(scrollPane, BorderLayout.CENTER);
pack();
setVisible(true);
}
public static void main(String[] args) {
try {
UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
} catch (UnsupportedLookAndFeelException | ClassNotFoundException | IllegalAccessException | InstantiationException ex) {
System.out.println("[L&F][Exception] " + ex.getMessage());
}
EventQueue.invokeLater(() -> {
new TableHeaderTest();
});
}
}
/**
* Trying to copy the look of a TableHeader and override its paintComponent
* method for drawing the additional icon. However the look can't be entirely
* copied and for example the sorting icon and background behave differently.
* Also the indent of column name dissapeared.
*/
class FilterIconHeaderRenderer1 implements TableCellRenderer {
TableCellRenderer delegate;
ImageIcon filterIcon = new ImageIcon(getClass().getResource("/res/marker-dot-green.png"));
public FilterIconHeaderRenderer1(JTable table) {
this.delegate = table.getTableHeader().getDefaultRenderer();
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
Component c = delegate.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
if (c instanceof JLabel) {
JLabelCopy label = new JLabelCopy((JLabel) c);
return label;
}
return c;
}
class JLabelCopy extends JLabel {
boolean withIcon = true;
JLabelCopy(JLabel label) {
this.ui = label.getUI();
this.setText(label.getText());
this.setPreferredSize(label.getPreferredSize());
this.setVerticalTextPosition(label.getVerticalTextPosition());
this.setHorizontalTextPosition(label.getHorizontalTextPosition());
this.setVerticalAlignment(label.getVerticalAlignment());
this.setHorizontalAlignment(label.getHorizontalAlignment());
this.setIcon(label.getIcon());
this.setIconTextGap(label.getIconTextGap());
this.setAlignmentX(label.getAlignmentX());
this.setAlignmentY(label.getAlignmentY());
this.setLayout(label.getLayout());
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
if (withIcon) {
g.drawImage(filterIcon.getImage(), 4, 4, null);
}
}
}
}
/**
* Also trying to copy the look of a TableHeader but without overriding its
* paintComponent method. Instead I make the header a panel consiting of two
* jLabels - the original column header and a jlabel of copied look with added
* icon. Problem with this method is theseparator of the visible separator of
* the two labels and the color difference of when a column is sorted (the
* copied-look-label doesn't change it's background to match a sorted header).
*
*/
class FilterIconHeaderRenderer2 implements TableCellRenderer {
TableCellRenderer delegate;
ImageIcon filterIcon = new ImageIcon(getClass().getResource("/res/marker-dot-green.png"));
public FilterIconHeaderRenderer2(JTable table) {
this.delegate = table.getTableHeader().getDefaultRenderer();
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
JPanel newHeader = new JPanel(new BorderLayout());
Component c = delegate.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
if (c instanceof JLabel) {
JLabel label = (JLabel) c;
JLabelCopy filterIconLabel = new JLabelCopy(label);
filterIconLabel.setText("");
filterIconLabel.setIcon(filterIcon);
filterIconLabel.setPreferredSize(new Dimension(filterIcon.getIconWidth() + 4, filterIconLabel.getPreferredSize().height));
newHeader.add(filterIconLabel, BorderLayout.WEST);
newHeader.add(label, BorderLayout.CENTER);
return newHeader;
}
return c;
}
class JLabelCopy extends JLabel {
boolean withIcon = true;
JLabelCopy(JLabel label) {
this.ui = label.getUI();
this.setPreferredSize(label.getPreferredSize());
this.setVerticalTextPosition(label.getVerticalTextPosition());
this.setHorizontalTextPosition(label.getHorizontalTextPosition());
this.setVerticalAlignment(label.getVerticalAlignment());
this.setHorizontalAlignment(label.getHorizontalAlignment());
this.setIcon(label.getIcon());
this.setIconTextGap(label.getIconTextGap());
this.setAlignmentX(label.getAlignmentX());
this.setAlignmentY(label.getAlignmentY());
this.setLayout(label.getLayout());
}
}
}
/**
* Having an array of header mockups for each state for selected and hasFocus
* (lacks sorted state) of the column header. Using these as background of panel
* loaded with two labels - original header and label with just the new icon.
* Both have setOpaque(false). Problem with this is the problem with choosing
* the right background image for the panel as the isSelected and hasFocus
* parameters of getRendererComponent don't work as you think they should.
*/
class FilterIconHeaderRenderer3 implements TableCellRenderer {
private BufferedImage[][] headerImages = new BufferedImage[2][2];
TableCellRenderer delegate;
ImageIcon filterIcon = new ImageIcon(getClass().getResource("/res/marker-dot-green.png"));
public FilterIconHeaderRenderer3(JTable table) {
this.delegate = table.getTableHeader().getDefaultRenderer();
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
JLabel comp = (JLabel) delegate.getTableCellRendererComponent(table, " ", i == 1, j == 1, 0, 0);
headerImages[i][j] = createUMPFake(comp);
}
}
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
Component c = delegate.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
if (c instanceof JLabel) {
JPanel newHeader = new JPanel(new BorderLayout()) {
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(headerImages[isSelected ? 1 : 0][hasFocus ? 1 : 0], 0, 0, null);
}
};
JLabel label = (JLabel) c;
label.setOpaque(false);
JLabel filterIconLabel = new JLabel();
filterIconLabel.setText("");
filterIconLabel.setOpaque(false);
filterIconLabel.setIcon(filterIcon);
filterIconLabel.setPreferredSize(new Dimension(filterIcon.getIconWidth() + 4, filterIconLabel.getPreferredSize().height));
newHeader.add(filterIconLabel, BorderLayout.WEST);
newHeader.add(label, BorderLayout.CENTER);
return newHeader;
}
return c;
}
/*
* Following methods were taken from:
* https://stackoverflow.com/questions/4028898/create-an-image-from-a-non-visible-awt-component
*/
private BufferedImage createUMPFake(Component comp) {
JFrame invisibleFrame = new JFrame();
invisibleFrame.setSize(comp.getPreferredSize());
JPanel colorPanel = new JPanel();
colorPanel.setOpaque(false);
colorPanel.setLayout(new BorderLayout());
colorPanel.setBackground(new Color(0, 0, 255, 0));
colorPanel.setSize(invisibleFrame.getSize());
colorPanel.add(comp, BorderLayout.CENTER);
invisibleFrame.add(colorPanel);
colorPanel.setVisible(true);
return createImage((JComponent) colorPanel, new Rectangle(invisibleFrame.getBounds()));
}
/**
* Create a BufferedImage for Swing components. All or part of the component
* can be captured to an image.
*
* @param component component to create image from
* @param region The region of the component to be captured to an image
* @return image the image for the given region
*/
private static BufferedImage createImage(Component component, Rectangle region) {
// Make sure the component has a size and has been layed out.
// (necessary check for components not added to a realized frame)
if (!component.isDisplayable()) {
Dimension d = component.getSize();
if (d.width == 0 || d.height == 0) {
d = component.getPreferredSize();
component.setSize(d);
}
layoutComponent(component);
}
BufferedImage image = new BufferedImage(region.width, region.height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = image.createGraphics();
// Paint a background for non-opaque components,
// otherwise the background will be black
if (!component.isOpaque()) {
g2d.setColor(component.getBackground());
g2d.fillRect(region.x, region.y, region.width, region.height);
}
g2d.translate(-region.x, -region.y);
component.paint(g2d);
g2d.dispose();
return image;
}
private static void layoutComponent(Component component) {
synchronized (component.getTreeLock()) {
component.doLayout();
if (component instanceof Container) {
for (Component child : ((Container) component).getComponents()) {
layoutComponent(child);
}
}
}
}
}
Or simpler, you can use HTML tags to display the icon on the HeaderRenderer
:
import java.awt.*;
import java.awt.event.*;
import java.net.URL;
import java.util.Objects;
import javax.swing.*;
import javax.swing.table.*;
public class TableHeaderIconTest {
//private final URL url = getClass().getResource("a.png");
public JComponent makeUI() {
String[] columnNames = {"Column1", "Column2", "Column3"};
JTable table = new JTable(new DefaultTableModel(columnNames, 3));
TableColumnModel m = table.getColumnModel();
for (int i = 0; i < m.getColumnCount(); i++) {
TableColumn column = m.getColumn(i);
column.setHeaderRenderer(new FilterIconHeaderRenderer4());
//column.setHeaderValue(
// String.format("<html><table><td><img src='%s'/><td>%s", url, columnNames[i]));
}
table.setAutoCreateRowSorter(true);
JPanel p = new JPanel(new BorderLayout());
p.add(new JScrollPane(table));
return p;
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
@Override public void run() {
createAndShowGUI();
}
});
}
public static void createAndShowGUI() {
try {
for (UIManager.LookAndFeelInfo laf : UIManager.getInstalledLookAndFeels()) {
if ("Nimbus".equals(laf.getName())) {
UIManager.setLookAndFeel(laf.getClassName());
}
}
} catch (Exception e) {
e.printStackTrace();
}
JFrame f = new JFrame();
f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
f.getContentPane().add(new TableHeaderIconTest().makeUI());
f.setSize(320, 240);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
class FilterIconHeaderRenderer4 implements TableCellRenderer {
private final URL url = getClass().getResource("Ok2mc.png");
@Override public Component getTableCellRendererComponent(
JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
TableCellRenderer r = table.getTableHeader().getDefaultRenderer();
String str = Objects.toString(value, "");
String html = String.format("<html><table><td><img src='%s'/><td>%s", url, str);
return r.getTableCellRendererComponent(table, html, isSelected, hasFocus, row, column);
}
}