I'm implemting TableCellRenderers using the Decorator design-pattern.
All works nice and well as long as all I need is to decorate the returned component from the decorated renderer in such manner that can be performed inside the scope of getTableCellRendererComponent(..).
But how can I decorate the returned component for such cases which need the Graphics object in the paint process? In particular - inside his paintComponent(Graphics g) method? For example, when I want to draw a certain line where a simple setBorder(..) will not be suffice:
import java.awt.Color;
import java.awt.Component;
import javax.swing.JTable;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.DefaultTableCellRenderer;
public class MyTableCellRendererDecorator extends DefaultTableCellRenderer {
private TableCellRenderer decoratedRenderer;
private Component decoratedComponent;
public MyTableCellRendererDecorator(TableCellRenderer decoratedRenderer) {
super();
this.decoratedRenderer = decoratedRenderer;
}
#Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
this.decoratedComponent = decoratedRenderer.getTableCellRendererComponent(table, value,
isSelected, hasFocus, row, column);
//an example for a decoration which works fine
decoratedComponent.setBackground(Color.red);
return decoratedComponent;
}
/**
* This code will NOT be executed, because the paint will be called on the returned component
* from getTableCellRendererComponent() and NOT on this JLabel in which this renderer subclasses.
*/
#Override
public void paintComponent(Graphics g) {
decoratedComponent.paint(g);
//an example for a needed decoration in paintComponent()
Rectangle bounds = g.getClipBounds();
g.setColor(Color.BLUE);
g.drawLine(0, 0, bounds.width, bounds.height);
}
}
I had 2 different solutions in mind:
1. Introduce an interface called DecoratedTableCellRenderer:
import javax.swing.table.TableCellRenderer;
public interface DecoratedTableCellRenderer extends TableCellRenderer {
public void setPostPaintComponentRunnable(Runnable postPaintComponentRunnable);
}
So now MyTableCellRendererDecorator will receive a DecoratedTableCellRenderer in his constructor instead of a simple TableCellRenderer, and the responsability for decorating inside paintComponent moves to the decorated class. If we assume a renderer is a JComponent which paints itself, this can be done by overriding paintComponent(..) and applying postPaintComponentRunnable.run() after his own paint code.
But, what if I want to support such decoration renderers which will apply on any TableCellRenderer in which I may not be able to modify?
2. Using java's reflection and a dynamic proxy to instantiate a new ComponentUI delegate to decoratedComponent, which will perform each method as its original ComponentUI object, only with a decorated version of paint(Graphics g, JComponent c).
This will keep the decoration responsibilty in the decorating class, but dynamic proxies are always somewhat hard to read and maintain in my perspective, I would be very happy if I will find a more elegant idea.
As it turns out, the original sample code you gave is close to correct because, in fact, the returned component is this. That is, getTableCellRendererComponent returns the DefaultTableCellRenderer it was called on, so any overridden methods in your subclass of DefaultTableCellRenderer will be called during the painting of the component.
Here's the code for DefaultTableCellRenderer that shows what I'm talking about:
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
//...
return this;
}
Therefore, if you override the paint(Graphics gfx) method and call your super classes getTableCellRendererComponent you will be able to draw whatever you want in the cell.
So your getTableCellRendererComponent should read like this:
#Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
JLabel component = super.getTableCellRendererComponent(table, value,
isSelected, hasFocus, row, column);
//an example for a decoration which works fine
component.setBackground(Color.red);
return component;
}
And use the paint method in your subclass of DefaultTableCellRenderer instead of paintComponent like this:
/**
* This code WILL be executed.
*/
#Override
public void paint(Graphics g) {
super.paint(g);
//an example for a needed decoration in paintComponent()
Rectangle bounds = g.getClipBounds();
g.setColor(Color.BLUE);
g.drawLine(0, 0, bounds.width, bounds.height);
}
Related
I'm trying to implement mouse hover effects for my JTable.
(When the mouse goes over a table's row the row's background changes).
In order to do that, I extended the DefaultTableCellRenderer like this:
public class FileTableCellRenderer extends DefaultTableCellRenderer{
public FileTableCellRenderer() {
setOpaque(true);
}
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
FileTable fileTable = (FileTable)table;
Component c = super.getTableCellRendererComponent(fileTable, value, isSelected, hasFocus, row, column);
if(!isSelected){
if(row == fileTable.getCursorRow())
{
c.setBackground(Color.pink);
c.setForeground(Color.darkGray);
}
else
{
c.setBackground(Color.white);
c.setForeground(Color.darkGray);
}
}
this.setText(value.toString());
return this;
}
}
I set the JTable's defaultRenderer, and it works. The problem is I have one column which is Boolean. before I set my renderer I had this cute checkbox as default renderer for it.
Now, it just shows "true" or "false".
On the other hand, if I leave the defualt BooleanRenderer for the Boolean column, it will not be highlighted with the whole row...
I also tried to extned the JTable.BooleanRenderer, but it seems to be protected, so I cannot even extend it.
How can I leave this checkbox of the BooleanRenderer, but change background color with the rest of the row?
This cannot be done using inheritance since BooleanRenderer is a non-public inner class of JTable. But you can use composition instead. E.g., create a wrapper class that will accept a TableCellRenderer ('parent') as a constructor argument. If you pass a BooleanRenderer from your table as a parent, calling its getTableCellRendererComponent() method will return a Checkbox component, so you'll be able to make any further adjustments to it (I use code from the question to set the background color):
import javax.swing.*;
import javax.swing.plaf.UIResource;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
public class BooleanCellRenderer implements TableCellRenderer, UIResource {
private final TableCellRenderer parent;
public BooleanCellRenderer(TableCellRenderer parent) {
this.parent = parent;
}
#Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
Component c = parent.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
FileTable fileTable = (FileTable) table;
if (!isSelected) {
if (row == fileTable.getCursorRow()) {
c.setBackground(Color.pink);
c.setForeground(Color.darkGray);
} else {
c.setBackground(Color.white);
c.setForeground(Color.darkGray);
}
}
return c;
}
}
Then, in your main GUI class containing a JTable, take the table's default Boolean renderer and pass it to the wrapper:
FileTable fileTable = new FileTable();
fileTable.setDefaultRenderer(Boolean.class, new BooleanCellRenderer(fileTable.getDefaultRenderer(Boolean.class)));
You can leave the FileTableCellRenderer to render String cells:
FileTable fileTable = new FileTable();
fileTable.setDefaultRenderer(String.class, new FileTableCellRenderer());
fileTable.setDefaultRenderer(Boolean.class, new
I have a JTable with three columns in each row, see the image:
For some reason depending on the column i select i get the little dark blue border around it (V140116554) in the image above.
I currently use this to select the entire row:
vTable.setRowSelectionAllowed(true);
How can i disable this?
EDIT:
Added a class:
public class VisitorRenderer extends DefaultTableCellRenderer {
#Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
setBorder(noFocusBorder);
return this;
}
}
And added it:
vTable.setDefaultRenderer(String.class, new VisitorRenderer());
But still get the border
The TableCellRenderer is responsible for drawing the focus rectangle around the currently focused cell. You need to supply your own renderer that is capable of either overriding this feature or providing its own...
For example;
public class MyRenderer extends DefaultTableCellRenderer {
#Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
setBorder(noFocusBorder);
return this;
}
}
This uses the DefaultTableCellRenderer as the base renderer and sets the component's Border to noFocusBorder which is defined in DefaultTableCellRenderer as a EmptyBorder
You will then need to set this renderer as the default renderer for the effected columns. Check out How to use tables for more details
Update with example
Works fine for me...
import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.border.LineBorder;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
public class TableRenderer {
public static void main(String[] args) {
new TableRenderer();
}
public TableRenderer() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
DefaultTableModel model = new DefaultTableModel(new Object[][]{{"", "One"}, {"", "Two"}}, new Object[]{"Check", "Vistor"}) {
#Override
public Class<?> getColumnClass(int columnIndex) {
return String.class;
}
};
JTable table = new JTable(model);
table.setRowSelectionAllowed(true);
table.setShowGrid(false);
table.setDefaultRenderer(String.class, new VisitorRenderer());
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new JScrollPane(table));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class VisitorRenderer extends DefaultTableCellRenderer {
#Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
setBorder(noFocusBorder);
return this;
}
}
}
And just to be sure, I changed setBorder(noFocusBorder); to...
if (hasFocus) {
setBorder(new LineBorder(Color.RED));
}
From the looks of things, the visitor column class type isn't being reported as String by the TableModel...
Updated with proxy renderer concept
Because you want to remove the focus border from every cell. You have three choices...
Write a custom cell renderer for every possibility of Class type you might need for your table. This can time consuming and repeats a lot of code to achieve only a small effect.
Do nothing a live with it...
Use a "proxy" renderer. This is a renderer that uses another TableCellRenderer to perform the actual rendering process, but applies some minor changes to the result, for example, remove the border...
...
public static class ProxyCellRenderer implements TableCellRenderer {
protected static final Border DEFAULT_BORDER = new EmptyBorder(1, 1, 1, 1);
private TableCellRenderer renderer;
public ProxyCellRenderer(TableCellRenderer renderer) {
this.renderer = renderer;
}
#Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
Component comp = renderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
if (comp instanceof JComponent) {
((JComponent)comp).setBorder(DEFAULT_BORDER);
}
return comp;
}
}
Instead of doing something like...
table.setDefaultRenderer(String.class, new VisitorRenderer());
Which we did before, we would do this instead...
table.setDefaultRenderer(String.class,
new ProxyCellRenderer(table.getDefaultRenderer(String.class)));
This means we can take advantage of the what ever default renderer is already available without knowing what that might be, but also supply our own custom requirements to it...
Instead of creating your own TableCellRenderer you could also do:
#Override
public Component prepareRenderer(TableCellRenderer renderer, int row, int col) {
Component c = super.prepareRenderer(renderer, row, col);
if (c instanceof JComponent)
((JComponent)c).setBorder(new EmptyBorder(1, 1, 1, 1));
return c;
}
If you have absolutely no need for the border on any cell in that table, just apply MyRenderer to all cells, regardless of class. You can do it like this:
table.setDefaultRenderer(Object.class, new MyRenderer());
There is a common method when using JTable TableCellRenderers for setting the background and foreground when the cell is selected. Here is an example question that was asked:
Why does my Java custom cell renderer not show highlighting when the row/cell is selected?
This solution is lacking one thing ... the border around the cell. (Note I am not asking about a border around the row, as was asked here.) The border should highlight when the cell is selected. It is not acceptable to just create your own Border, and set it, because the border you create may not fit in with the Look & Feel.
I've successfully got the border by initializing a default renderer, and then scavenging it for its border, as follows:
private final DefaultTableCellRenderer defTblRend = new DefaultTableCellRenderer();
private final JComponent renderer = new ComplexCell(); // Whatever object type extends JComponent
#Override public Component getTableCellRendererComponent(JTable table,
Object value, boolean isSelected, boolean hasFocus, int row,
int column)
{
// ... Set values on "renderer" object here ...
renderer.setBackground(isSelected ? table.getSelectionBackground() : table.getBackground());
renderer.setForeground(isSelected ? table.getSelectionForeground() : table.getForeground());
renderer.setOpaque(!renderer.getBackground().equals(table.getBackground()));
JComponent comp = (JComponent)defTblRend.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
renderer.setBorder(comp.getBorder());
return renderer;
}
Is there a better way?
You might be able to use the UIManager. See UIManager Defaults. "Table.focusCellHighlightBorder" would appear to be the property you want.
ADDED BY ORIGINAL POSTER:
Here is the solution I came up with based on camickr's info. Optimizations/cleanup welcome.
Set up static borders so they are available wherever you need them (I put them in a class called "UiUtils"):
public static final Border focusedCellBorder = UIManager.getBorder("Table.focusCellHighlightBorder");
public static final Border unfocusedCellBorder = createEmptyBorder();
private static Border createEmptyBorder()
{
Insets i = focusedCellBorder.getBorderInsets(new JLabel());
return BorderFactory.createEmptyBorder(i.top, i.left, i.bottom, i.right);
}
Renderer
#Override public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column)
{
// [... set component values here ...]
label.setBorder(hasFocus ? UiUtils.focusedCellBorder : UiUtils.unfocusedCellBorder);
return label;
}
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.
Hi I have a class called ColorChooser (in the net.java.dev.colorchooser.ColorChooser package)
This is a custom component used to select colors. What I want is to display a JTable with ColorChoosers in the second column. So I created my own TableCellRenderer and it works:
#SuppressWarnings("serial")
class ColorChooserTableRenderer extends DefaultTableCellRenderer {
public static List<ColorChooser> colors;
public ColorChooserTableRenderer(int rows) {
colors = new ArrayList<ColorChooser>(rows);
for (int i = 0; i<rows ; i ++) {
colors.add(new ColorChooser());
}
}
#Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
return colors.get(row);
}
}
I register this in my table :
JTable t = new JTable(5,3);
t.getColumn(t.getColumnName(1)).setCellRenderer(new ColorChooserTableRenderer(5));
The display is good. It even displays the tool tip of the ColorChoosers when i hover my mouse over one of them. The problem is that the ColorChoosers do not receive MouseEvents.
Normally when you press and hold the mouse on a ColorChooser, you get a pop up window that you can use to select a color. When in the JTable the ColorChooser component does not receive the mouse event.
Any solutions?
Edit: The question can be easily modified to this:
Can you please give me a small example of a table containing JButtons in the second column that actually work? You know, buttons that can be pressed?
This sounds vaguely familiar as I have been using table cell renderers for other purposes.
My understanding is that TableCellRenderer is only used to render the component; a component does not actually exist at each of the cells.
So you'd probably have to somehow forward mouse events from the JTable itself to the ColorChooser.
edit: p.s., see my question -- also for custom table cell rendering, you only need 1 instance of the component itself for the entire column, if the column is rendered with the same logic. Don't store persistent state in the TableCellRenderer, store it in the TableModel instead, and use that state immediately prior to rendering when you handle the call to getTableCellRendererComponent().
A renderer only paints the component on the screen and does not allow for interaction. What you need is to also implement a TableCellEditor. It is recommend that you inherit the AbstractCellEditor and you'll save some work. Check out the java tutorial for tables.
Example:
public class MyTableCellRenderer implements TableCellRenderer
{
private JButton button = new JButton("Press Me");
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
return button;
}
}
public class MyTableCellEditor extends AbstractCellEditor implements TableCellEditor
{
private JButton button;
public MyTableCellEditor()
{
button = new JButton("Press Me");
button.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e) {
System.out.println("buttonPressed");
}
});
}
public Object getCellEditorValue() {
return null;
}
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
return button;
}
}