Set the button "background" of a Nimbus button
Asked Answered
P

2

6

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:

Buttons with incorrect background

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:

Also incorrect button background

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:

Still incorrect buttons

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?

Pilchard answered 12/8, 2013 at 22:10 Comment(2)
Have you tried using setOpaque(false) (sorry on mobile so its hard to read all the code)Atalanti
Yes I have, with setOpaque(false) the background is always white.Pilchard
V
1

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

  1. Keep reference to original renderer
  2. Let original renderer render its own component (for every rendering)!
  3. 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:

Result

Violate answered 13/8, 2013 at 6:19 Comment(9)
I have working code for it, but no time to post more detailed example now. What you mean by not working. You still need to set background to JPanel correctly.Violate
I have updated my answer after some dinner testing. At the end it seems to be simpler than i though (hard to tell in old code what is important)Violate
Yes i meant at rendering time. I am creating JPanel for every render, but reuse would probably work tooViolate
added code to play with (as I understand your suggestion related to default renderer) and screenshots to compare both approaches (w/out unwrap ui-resource)Schutzstaffel
I missed override in JPanel, posted complete exampleViolate
Actually, I prefer your approach over my hack - see my last edit on how-to implement the renderer cleanly :-) You don't have to override paintComponent, just set the color as not-uiresource when configuring the panelSchutzstaffel
Ok, i added that solution to complete example. This was really inspiring sessionViolate
Wrapping the button in a JPanel and setting the panel's background color did the trick. Thanks for your effort.Pilchard
next cleanup step would be to remove the component creation on each call plus move it out of the table subclass (the latter probably is just in this quick example?) - yeah, liked this also :-)Schutzstaffel
S
2

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

enter image description here

Screenshot: using default colors (from the renderer installed for Object.class)

enter image description here

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

Schutzstaffel answered 13/8, 2013 at 9:48 Comment(0)
V
1

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

  1. Keep reference to original renderer
  2. Let original renderer render its own component (for every rendering)!
  3. 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:

Result

Violate answered 13/8, 2013 at 6:19 Comment(9)
I have working code for it, but no time to post more detailed example now. What you mean by not working. You still need to set background to JPanel correctly.Violate
I have updated my answer after some dinner testing. At the end it seems to be simpler than i though (hard to tell in old code what is important)Violate
Yes i meant at rendering time. I am creating JPanel for every render, but reuse would probably work tooViolate
added code to play with (as I understand your suggestion related to default renderer) and screenshots to compare both approaches (w/out unwrap ui-resource)Schutzstaffel
I missed override in JPanel, posted complete exampleViolate
Actually, I prefer your approach over my hack - see my last edit on how-to implement the renderer cleanly :-) You don't have to override paintComponent, just set the color as not-uiresource when configuring the panelSchutzstaffel
Ok, i added that solution to complete example. This was really inspiring sessionViolate
Wrapping the button in a JPanel and setting the panel's background color did the trick. Thanks for your effort.Pilchard
next cleanup step would be to remove the component creation on each call plus move it out of the table subclass (the latter probably is just in this quick example?) - yeah, liked this also :-)Schutzstaffel

© 2022 - 2024 — McMap. All rights reserved.