I am designing a GUI with Java Swing and AWT (Java 8) and am struggling with the icons I use.
I load a large PNG image and scale it to 18x18px and then use it in a button or label. It works well in all resolutions when the operating system does not zoom in.
However, with the advent of large screen resolutions (hidpi), it is common practice to use operating system settings to zoom in on user interface controls, including buttons and such things in Java applications. For example, on Windows I use a 150% or 200% scaling of user elements with my 4K resolution to ensure the user interface is still usable. I imagine many users will do so as well.
When that is the case, however, the icons are merely increased in size after already scaling them down to 18x18px. That is, I first scale them down and then the operating system tries to scale them up again with the little information that is still left in the image.
Is there any way to design image icons in Java that are based on a higher resolution when the zooming/scaling capabilities of the operating system are used in order to avoid them appearing blurred?
Here is a working example:
import java.awt.Container;
import java.awt.Image;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
@SuppressWarnings("serial")
class Example extends JFrame {
public static void main(String[] args) {
new Example();
}
public Example() {
Container c = getContentPane();
JPanel panel = new JPanel();
ImageIcon icon = new ImageIcon(new ImageIcon(getClass().getResource("tabler-icon-beach.png")).getImage().getScaledInstance(18, 18, Image.SCALE_SMOOTH));
JButton button = new JButton("Test button", icon);
panel.add(button);
c.add(panel);
this.pack();
this.setLocationRelativeTo(null);
this.setVisible(true);
}
}
You can find the icon here. All icons are available as PNG or SVG files.
To illustrate the problem, let me first show you two screenshots in the normal 100% screen resolution:
On Linux with 100% zoom:
On Windows with 100% zoom:
And now when I set Windows 7 to have a 200% magnification of layout elements, it's obviously just the 18x18px version stretched out, which becomes blurred:
Is there any way to provide a higher-resolution image icon that is used when the operating system uses a scaling that is larger than 100%? Moreover, you can see that even at 100% the image quality is not perfect; is there any way to improve that as well?
Java 8 does not support High DPI, the UI gets scaled up by Windows. You should use Java 11 or a later version which support per-monitor High DPI settings.
If your goal is to make the icons look crisp, prepare a set of icons for different resolutions using BaseMultiResolutionImage
(the basic implementation of MultiResolutionImage
) to provide higher resolution alternatives. (These are not available in Java 8.)
You say that you scaled down the original image (240×240) to 18×18px. If the UI needs a higher resolution according to the system setting, all it has now is your small icon (18×18) which will be scaled up, which results in poor quality. You should use a MultiResolutionImage
or paint the original image into the required size, letting Graphics
to scale it down for you.
This is the simplest way I came up with to make the icon 18×18 without downscaling the original image:
private static final String IMAGE_URL =
"https://tabler-icons.io/static/tabler-icons/icons-png/beach.png";
private static ImageIcon getIcon() {
return new ImageIcon(Toolkit.getDefaultToolkit()
.getImage(new URL(IMAGE_URL))) {
@Override
public int getIconWidth() {
return 18;
}
@Override
public int getIconHeight() {
return 18;
}
@Override
public synchronized void paintIcon(Component c, Graphics g,
int x, int y) {
g.drawImage(getImage(), x, y, 18, 18, null);
}
};
}
I left out the exception handling code for MalformedURLException
which can be thrown from the URL
constructor.
In this case, the painted image gets down-scaled each time it's painted, which is ineffective. Yet the quality is better. Well, for the standard resolution screen, it's nearly the same as if you down-scaled the image when loading. But in High DPI case, it looks better. It's because for 200% UI Scale, the image will be rendered to 36×36 pixels and these pixels will be created from the source of 240×240 rather than up-scaling the down-scaled version which lost its quality.
To get even better results, I recommend using MultiResolutionImage
.
The app below loads the images from base64-encoded strings (for simplicity so that there are no external dependencies). There are three variants provided: 24×24 (100%, 96dpi), 36×36 (150%, 144dpi), 48×48 (200%, 192dpi).
If the current scale factor is set to any of the provided resolutions, the image will be rendered as is. If 125% or 175% are used, the larger image will be scaled down; if the scale is greater than 200%, then the image for 200% will be scaled up. You can add more resolutions if needed.
The app doesn't compile in Java 8 because MultiResolutionImage
is not available there. To compile it with JDK 11, you have to replace text blocks with regular String concatenation.
import java.awt.Image;
import java.awt.image.BaseMultiResolutionImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Base64;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class BeachIconButton {
public static void main(String[] args) {
SwingUtilities.invokeLater(BeachIconButton::new);
}
private BeachIconButton() {
JPanel panel = new JPanel();
ImageIcon icon = getIcon();
JButton button = new JButton("Test button", icon);
panel.add(button);
JFrame frame = new JFrame("Beach Icon Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(panel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private static ImageIcon getIcon() {
return new ImageIcon(
new BaseMultiResolutionImage(
Arrays.stream(new String[] { BEACH_100, BEACH_150, BEACH_200})
.map(BeachIconButton::loadImage)
.toArray(Image[]::new)));
}
private static Image loadImage(String base64) {
try {
return ImageIO.read(new ByteArrayInputStream(
Base64.getMimeDecoder().decode(base64)));
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
private static final String BEACH_100 = """
iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAA7DAAAO
wwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAXxJ
REFUSInl1E9LVVEUBfAfVJBIIoWGIDiRGtagN7Yg+gTZXPwICU4FyUGDahBRCTrK
qUVNIxrkJHjWJMKZikIjSyHoYd0GZ186vOef7n2zWnB4e591WXudt/c5/G8Ywiw+
4gd28QGLuNCt+C18RXHI+onJuuLjIVBgGWOYyPb2syI3q4qfzZw/ir1raMXeNBay
k+zgfNUC25nTBWxGPhfffI78XfzOVD1FP+5lrgusoweXIv+C6xE3qxYocRFrWZHv
eBPxA5yJeK9ugRJPdU7QGPoi3+1GfFBqZIFn+BVxC88jft9NgdL9q8gH8MSfUa3V
5BKXQ6gl9aOdey1N2GBV4VO4ik/h8CWG67rMcQ53Hf5ErOBGXfEGtjKxVSxhHi/w
LeMe4mQV8ROZ67fSZWrHadyW7kKBqaonuC89Bcc5uyKNZ+M4wTvS9d/AY/RmXK80
iht/wa+HVgfKB61cTYzGaups8FH89kEFhjBSUzDnR0LrSFT5Sw7i/yH8BmQ0mnmX
f2wqAAAAAElFTkSuQmCC""";
private static final String BEACH_150 = """
iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAACXBIWXMAABYlAAAW
JQFJUiTwAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAoVJ
REFUWIXt1kuoTVEYB/DfdZWiq7gykZBHHokyw0QGREpKLgMDGSojQ6+JARFi4JFn
ySPyyoCJR8zcIUVJiPLopiMhLoO1d3ftc+7Z95xrnYHyr1V7feu/1/ff3/etby/+
4x9DW8K9ZmE9FmTP7fiBp3iE03iS0F9dTMQN/G5gvMLMVopZhi8NisnHD6xohZiV
2ea5o584h+WYgAclor5jaUoxE9ATOXiOednaUMUU9mKPkK5YVCXbJwkuKkZmYbS2
t8rxpsz+QW2krqYSdF5tCnZjCX5F9h0Zf25k66nizEghaBSOV22cRyt/vhnxd0b2
E7gQzXelEJRjnv6L96dQ9IR+9CxaWymcsnx+L6UgQnPtUizyfDwRaiiff8JwjIts
71MLytGBy0I9xaLitG7LuCMi2+d4kyEJBVWwWkhPfz568UaI6Mho/XtCDTXYqO/L
v+GF2jQ+wNZofrdVYjrwNnK0XYjOYSEt9bp20lMWY0/k5LVQJzlGCn2qur56Mb0V
YiYLKcodddXhTcX1iJesU1cj/nc9NPA9axE2Cw02GUYITe6UYgoOCv+29pTOyjAH
VxRT1N/4iP0Y2yohHTij9v810Khgiwavy43eqSfhGmZHtt/oxm2h4fUI0Zim75IW
4wI24GuDPutiCF4q1sklAx/XJXisGK2zfysGhum7N3/F2ibebce+SNCdFIJgMQ4J
F63BYBUOYEqzL87HLaEeKrivPCLrMk5lEPyezNf8MkHv9H9STioegDbF/vM3/Hdl
grpLnBxFJ8bgWAmvWX53maBOId/jMRpHGnB8JOM2yx+f+eosE1SNNiH89TY/oTY1
zfAHjS7hMv45G3exJiH/P/5d/AHE21JDZYKOHAAAAABJRU5ErkJggg==""";
private static final String BEACH_200 = """
iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAB2HAAAd
hwGP5fFlAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAA5FJ
REFUaIHt2ctrXFUcB/BPYoWGVGOwBZtYQQoqhUp9IFEoulC04spGExQEpYoiVBBc
qVAVwYX/gEWqooKboItqN4oV0frWPgR3XRSl9mHT2GjaJh0X5w6ZOXPuzO08roL5
wlnM+T3O93cev3vOb1jCEv7f6CtpnBswjg1Yh2HM4wx+wh58jC9K4lMIfXgIB1Ap
2H7FC7jwX+Bbh2uwW3HiqUDWlE26io040YJgkXYKd5TM3Z34K0HmNKYwgavxgLD/
WwUxi1vKIr8GfyRIfIAravTuxtmE3g+Jvkrm86pek+/XuOfP4elIbz1mIr15bBUm
IG8lPtHjjLkpMeg+jNboDOKXSOcsJjP5bU0CqODBsgOoHsRnsRyvJ+RP1vh4PpId
in7v7WUA/diRE0QFhxN92yMfeyL5MxoP+nW9DAJuxY85QdS2AxiosVsrnJmqfAGX
4/3I7uVeBwAX4DEcaRLAFFbV2LwSyXdn/ZNR/86es6/BCmyTTpnVWX4Tl2pMv49k
PtZF/QfLIl+L9fhN/mrEK3XE4vYajGQzZRKPcTuOyw+k2rbV2FwSyY7GTvt7yTjC
UQwV0FspEKf+nMBcVxmdJz7VeEVYkF6F43gKD0f9H5bOOsN9Gkluws04lpBV21z0
+7myiRO+wgcjInE6HE/opNpYOZTrEV8RzghX6hjLhWvHKWny35RBNsZogtCrLWxG
8JrG8zHRO5r5eCci8bvFDNMKY/hMeNRsF77wpWJM/f2mgke7PciyLvtbKby4NuJe
9Y+QGdyEv7FLSJX/CfThfqGmM691Jqlkep8L6bWs2lQSd+F7xUjnte+EYkCpGMQb
HRKP247M73mhneVbhY9wY0J2TpjRnUKV4bBQIxrGalyPe4RSY+oe9q1who61wasQ
+vC1xtlbwFu4sqCftXhb+i70lR6ei+HEgPtxbZv+NuDnhM/hjpnmoE+YoepAu7Sx
byOsELZkWyvQzlINCTWaGbwnpMROsUx4/16Md3GyE2cDQqXsS0wLRPfhJfWFqjyM
Zrr78WcX7KczLlvVVzJyMSU/1U1brKSlMJnp9Mp+qkgAs00cVL+imxN244p9iTux
ny0SQLMVqLY5PCHs2SGhRHi6gF2n9oVWYABbhOLUZbgoGyx+3rUi+Hhm26n96ozL
FgXPQB4262yLdGrfFUxo/vfRCc1fTp3adwUjeFEod5/M2l7h38WREuyXsIQl1OAf
9zFZ1uiy3BkAAAAASUVORK5CYII=""";
}