Search code examples
javaswing

Wrong JCheckBox painting in pressed state with custom icons


I need a JCheckBox with custom icons

However, once I set custom icons, my check box is painted wrong in the "pressed" state

Specifically, it always displays the selected icon

// snippet 1

package demos.button;

import di.Icons;

import javax.swing.*;
import java.awt.*;

public class CheckBoxDemo {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Check Box demo");
        JPanel mainPanel = createMainPanel();
        frame.setContentPane(mainPanel);
        frame.setLocationRelativeTo(null);
        frame.pack();
        frame.setVisible(true);
    }

    private static JPanel createMainPanel() {
        FlowLayout layout = new FlowLayout();
        layout.setAlignment(FlowLayout.CENTER);
        JPanel mainPanel = new JPanel(layout);
        mainPanel.add(createCheckBox());
        return mainPanel;
    }

    private static JCheckBox createCheckBox() {
        JCheckBox checkBox = new JCheckBox();
        checkBox.setIcon(Icons.unchecked());
        checkBox.setSelectedIcon(Icons.checked());
        return checkBox;
    }
}
// snippet 2

package di;

import util.IconUtil;

import javax.swing.*;

public class Icons {
    private Icons() {
    }
    public static Icon checked() {
        return IconUtil.findIcon("/checked.gif").orElse(null);
    }
    public static Icon unchecked() {
        return IconUtil.findIcon("/unchecked.gif").orElse(null);
    }
}
// snippet 3

package util;

import javax.swing.*;
import java.net.URL;
import java.util.Optional;

public class IconUtil {
    private IconUtil() {
    }

    public static Optional<Icon> findIcon(String path) {
        URL iconUrl = IconUtil.class.getResource(path);
        return Optional.ofNullable(iconUrl).map(ImageIcon::new);
    }
}

checked.gif

checked icon

unchecked.gif

unchecked icon

It's not reproducible if I don't set custom icons

// snippet 4

    private static JCheckBox createCheckBox() {
        JCheckBox checkBox = new JCheckBox();
        return checkBox;
    }

It seems to contradict the documentation for setIcon() which clearly says

Sets the button's default icon. This icon is also used as the "pressed" and "disabled" icon if there is no explicitly set pressed icon.

Since I set the unchecked icon as the default icon, it should be displayed whenever the checkbox is in the pressed state

However, the selected icon is

You may set the borderPainted property to true to see it even more clearly. It's always visually selected when it's pressed

// snippet 5

    private static JCheckBox createCheckBox() {
        JCheckBox checkBox = new JCheckBox();
        checkBox.setIcon(Icons.unchecked());
        checkBox.setSelectedIcon(Icons.checked());
        checkBox.setBorderPainted(true);
        return checkBox;
    }

It's as if I invoked

// snippet 6

        checkBox.setIcon(Icons.checked());

instead of

// snippet 7

        checkBox.setIcon(Icons.unchecked());

check box demo: pressed icon is selected

At the end of the day what I really want is this:

  1. Once pressed, keep my custom icon (checked or unchecked)
  2. But make the check box look "pressed" (shadow the box area)

Basically, the same thing that happens in a default JCheckBox (see snippet 4), but with custom icons

no custom icons: unselected, pressed

no custom icons: selected, pressed


Solution

  • Reason

    JCheckBoxes don't have a pressedSelectedIcon (similar to how there are rolloverIcon and rolloverSelectedIcon)

    It means that whatever pressedIcon is set, it's displayed in the pressed state regardless of the check box's selected property

    If pressedIcon is not explicitly set, it's up to the L&F to find a substitute. MetalLookAndFeel and WindowsLookAndFeel use selectedIcon

    // javax.swing.plaf.metal.MetalRadioButtonUI#paint
    // OR javax.swing.plaf.basic.BasicRadioButtonUI#paint
    // it seems the code was copied and pasted
    
                } else if(model.isPressed() && model.isArmed()) {
                    altIcon = b.getPressedIcon();
                    if(altIcon == null) {
                        // Use selected icon
                        altIcon = b.getSelectedIcon();
                    }
    

    NimbusLookAndFeel follows the doc and uses defaultIcon (the icon that you pass to setIcon())

    If you don't set custom icons, getPressedIcon() is not invoked at all and the "radio button UI"'s default icon, as returned by getDefaultIcon() (not to be confused with the button's defaultIcon), is painted instead

    // in the same paint method
            if(altIcon != null) {
                // invoke getPressedIcon() if necessary etc.
            } else {
                getDefaultIcon().paintIcon(c, g, iconRect.x, iconRect.y);
            }
    
    // javax.swing.plaf.metal.MetalIconFactory.CheckBoxIcon#paintIcon
            public void paintIcon(Component c, Graphics g, int x, int y) {
                if (MetalLookAndFeel.usingOcean()) {
                    paintOceanIcon(c, g, x, y);
                    return;
                }
    

    The UI's default icon is a "smart" one and does consult the button model to paint itself

            private void paintOceanIcon(Component c, Graphics g, int x, int y) {
                ButtonModel model = ((JCheckBox)c).getModel();
    
                g.translate(x, y);
                int w = getIconWidth();
                int h = getIconHeight();
                if ( model.isEnabled() ) {
                    if (model.isPressed() && model.isArmed()) {
                        g.setColor(MetalLookAndFeel.getControlShadow());
                        g.fillRect(0, 0, w, h);
                        g.setColor(MetalLookAndFeel.getControlDarkShadow());
                        g.fillRect(0, 0, w, 2);
                        g.fillRect(0, 2, 2, h - 2);
                        g.fillRect(w - 1, 1, 1, h - 1);
                        g.fillRect(1, h - 1, w - 2, 1);
    

    Solution

    So you going to have to explicitly set your own "smart" pressedIcon that takes the check box's model into account too

    // CheckBoxDemo
        private static JCheckBox createCheckBox() {
            JCheckBox checkBox = new JCheckBox();
            checkBox.setIcon(Icons.uncheckedBox());
            checkBox.setPressedIcon(Icons.pressedBox(checkBox));
            checkBox.setSelectedIcon(Icons.checkedBox());
            return checkBox;
        }
    
    // Icons
        public static Icon pressedBox(JCheckBox checkBox) {
            return new Icon() {
                @Override
                public void paintIcon(Component c, Graphics g, int x, int y) {
                    if (checkBox.isSelected())
                        checkedPressedBox().paintIcon(c, g, x, y);
                    else
                        uncheckedPressedBox().paintIcon(c, g, x, y);
                }
    
                @Override
                public int getIconWidth() {
                    return Math.max(checkedPressedBox().getIconWidth(), uncheckedPressedBox().getIconWidth());
                }
    
                @Override
                public int getIconHeight() {
                    return Math.max(checkedPressedBox().getIconHeight(), uncheckedPressedBox().getIconHeight());
                }
            };
        }
    
        private static Icon checkedPressedBox() {
            return IconUtil.findIcon("/checkedPressed.png").orElse(null);
        }
    
        private static Icon uncheckedPressedBox() {
            return IconUtil.findIcon("/uncheckedPressed.png").orElse(null);
        }
    

    checkedPressed.png

    checked pressed icon

    uncheckedPressed.png

    unchecked pressed icon

    demo: check box unselected, pressed

    demo: check box selected, pressed