Java Swing跟随系统缩放时避免图片缩放及自定义Icon异常解决求助
我正在梳理对应方案以确定最优解决路径,一年多前我曾提出过关于Java中图片画质的问题,当时处理了项目其他模块需求,现在对相关技术有了更多积累后又回到了这个问题上。
我发现Windows系统缩放设置为125%时,会导致Java Swing程序中的图片画质下降。我的目标是理清Java Swing响应系统缩放的逻辑,调研过程中我找到了JVM启动参数-Dsun.java2d.uiScale=1.0,可以强制程序无论系统缩放比例为多少都以100%比例运行。
使用该参数时图片画质表现优秀,按钮上16x16的小图标相比跟随系统缩放的场景模糊度低很多,效果对比如下:
- 125%缩放时效果:图标模糊,边缘有明显锯齿
- 100%缩放时效果:图标清晰,边缘锐利无失真
但我仍希望程序可以跟随系统缩放,方便视力不佳的用户使用,上述强制100%缩放的方案仅作为最终兜底选项。
因此我的核心问题是:如何让Swing程序界面跟随系统缩放的同时,避免内部图片被缩放?
我在相关技术讨论中了解到,缩放系数定义在Graphics对象的AffineTransform中,基于该信息我已经可以控制图片画质,但遇到了两个尚未解决的问题:
- 程序首次加载时,按钮上的图标不会显示,只有鼠标hover之后才会出现,窗口最大化或调整大小后图标又会消失,请问这是Graphics2D的相关问题吗?
- 自定义的MyIcon类渲染的图标位置不像原生
new ImageIcon(...)那样居中,我尝试添加at.setToTranslation(1.25, 1.25);调整位置,但效果不明显,有没有办法让图标像100%缩放时一样居中?
我在125%系统缩放下使用MyIcon类的实现效果画质比默认缩放的效果好很多,除了上述两个问题之外已经满足需求。
以下是复现问题的最小代码,要复现位置问题需要使用16x16的png图片:
import java.awt.GridBagLayout; import common.Functions; import java.awt.GridBagConstraints; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JButton; import java.awt.geom.AffineTransform; import java.net.URL; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import javax.swing.JFrame; @SuppressWarnings("serial") public class TestFrame extends JFrame { public static void main(String[] args) { TestFrame tf = new TestFrame(); tf.setLocationRelativeTo(null); tf.setVisible(true); } public TestFrame() { setSize(300, 150); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); Icon i = new MyIcon(Functions.class.getResource("/link.png")); GridBagLayout gridBagLayout = new GridBagLayout(); gridBagLayout.columnWidths = new int[]{0, 0}; gridBagLayout.rowHeights = new int[]{0, 0}; gridBagLayout.columnWeights = new double[]{1.0, Double.MIN_VALUE}; gridBagLayout.rowWeights = new double[]{1.0, Double.MIN_VALUE}; getContentPane().setLayout(gridBagLayout); JButton btnNewButton = new JButton("Test Button"); btnNewButton.setPreferredSize(new Dimension(150, 30)); btnNewButton.setIcon(i); btnNewButton.setFocusPainted(false); GridBagConstraints gbc_btnNewButton = new GridBagConstraints(); gbc_btnNewButton.gridx = 0; gbc_btnNewButton.gridy = 0; getContentPane().add(btnNewButton, gbc_btnNewButton); } public class MyIcon extends ImageIcon{ public MyIcon(String filename) { super(filename); } public MyIcon(URL resource) { super(resource); } @Override public synchronized void paintIcon(Component c, Graphics g, int x, int y) { Graphics2D g2d = (Graphics2D) g.create(); AffineTransform at = new AffineTransform(); at.setToTranslation(1.25, 1.25); g2d.setTransform(at); if(getImageObserver() == null) { g2d.drawImage(getImage(), x, y, c); } else { g2d.drawImage(getImage(), x, y, getImageObserver()); } g2d.dispose(); } } }
两个问题的根源都在paintIcon方法里直接覆盖了Graphics2D的全局变换矩阵,完全丢掉了原有的坐标映射、缩放偏移参数,导致渲染坐标错误、重绘触发逻辑异常。
问题1修复(图标不显示/消失)
不要直接调用setTransform覆盖全局变换,先读取原Graphics2D的缩放系数,再反向抵消缩放对图片的影响,保留原有平移参数即可:
@Override public synchronized void paintIcon(Component c, Graphics g, int x, int y) { Graphics2D g2d = (Graphics2D) g.create(); // 读取当前系统缩放系数 AffineTransform originalTransform = g2d.getTransform(); double scaleX = originalTransform.getScaleX(); double scaleY = originalTransform.getScaleY(); // 反向抵消缩放,保证图片按原始像素渲染 AffineTransform noScaleTransform = AffineTransform.getTranslateInstance(x, y); noScaleTransform.scale(1/scaleX, 1/scaleY); g2d.setTransform(noScaleTransform); // 绘制图片时坐标填0即可,因为已经在变换里处理了x/y偏移 if(getImageObserver() == null) { g2d.drawImage(getImage(), 0, 0, c); } else { g2d.drawImage(getImage(), 0, 0, getImageObserver()); } g2d.dispose(); }
修改后重绘逻辑会正常识别图标渲染区域,不会出现加载不显示、调整窗口消失的问题。
问题2修复(图标居中)
你之前硬编码的1.25偏移只适配125%缩放,换成动态计算的占位尺寸即可:
重写getIconWidth和getIconHeight方法,返回适配缩放后的尺寸,这样Swing布局计算时会自动给图标预留正确的居中位置:
@Override public int getIconWidth() { // 读取系统缩放系数,返回缩放后的占位宽度 GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment() .getDefaultScreenDevice().getDefaultConfiguration(); double scale = gc.getDefaultTransform().getScaleX(); return (int) Math.round(super.getIconWidth() * scale); } @Override public int getIconHeight() { GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment() .getDefaultScreenDevice().getDefaultConfiguration(); double scale = gc.getDefaultTransform().getScaleY(); return (int) Math.round(super.getIconHeight() * scale); }
如果还有细微的像素偏移,可以在noScaleTransform的平移参数里加上微调值,适配不同按钮的内边距。
补充优化
如果要适配多屏不同缩放的场景,可以在绘制时从Component对象读取当前屏幕的缩放系数,不要用全局默认值,避免跨屏拖动时渲染异常。
内容的提问来源于stack exchange,提问作者Kyle

