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
unchecked.gif
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:
Basically, the same thing that happens in a default JCheckBox
(see snippet 4), but with custom icons
JCheckBox
es 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);
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
uncheckedPressed.png