问题描述
Java Swing 是一个非常古老的 GUI 开发技术,不过虽然老旧,但同时也是市面上一个不可缺少的技术。比如 IDEA 全家桶都是基于 Swing 开发的。
具体问题先看看图片:
仔细对比上下两组图片,在超过 100% 缩放显示下,可以看到 Swing 界面里面的各个图片显示都很模糊。做过 Swing 开发的应该都知道这个图片缩放的痛点。
但是这个问题被我解决了:
问题原因
在使用 JLabel 显示图片时,我们先看看相关代码:
// 比如一张原始大小为200*200的图片,将它放在30*30的JLabel中
// 获取package中的图片
var icon = new ImageIcon(Objects.requireNonNull(MyFrame.class.getResource("/com/xxx/xxx.png")));
// 设置图片尺寸
icon.setImage(icon.getImage().getScaledInstance(30, 30, Image.SCALE_SMOOTH));
// 创建JLabel
var label = new JLabel();
label.setBounds(0, 0, 30, 30);
// 在JLabel中放入图片
label.setIcon(icon);
// 将JLable放入容器中
panel.add(jcefIconLabel);
在上述代码中,我们要给将图片放入 JLabel 中,就需要给它设置一个固定的尺寸。于是问题出现在这里:
- 200 * 200 的图片被 Java 压缩至 30 * 30 的大小
- 此时桌面缩放为 150% ,该 30 * 30 的 JLabel 在屏幕上实际像素大小为 45 * 45
- 因此在第一步被压缩至 30 * 30 大小的图片,会被拉伸至 45 * 45 显示在屏幕上
- 于是上图中马赛克效果就出现了
如果是 H5 页面,使用<img>
会自动帮你显示正确尺寸与正确缩放的图片。但是 Swing 就不一样了,这东西得自己去想办法去做适配实现。
解决方案
其实这个问题解决起来也不难。你看是因为 30 * 30 被拉伸为 45 * 45 才导致的马赛克效果,那能不能在一开始就将图片大小设置为 45 * 45 呢?答案是可以的,接下来我就贴上相关代码。
声明一个AutoScalingIcon
类,让它实现javax.swing.Icon
接口,来替代上述代码中的ImageIcon
。
package xxx.xxx.xxx;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.math.BigDecimal;
import java.util.Objects;
public class AutoScalingIcon implements Icon {
private final boolean autoSize;
private final ImageIcon originIcon;
private final ImageIcon icon = new ImageIcon();
private int width;
private int height;
private double lastScaleX = -999.0;
private double lastScaleY = -999.0;
public AutoScalingIcon(String path) {
this.autoSize = true;
this.width = 2;
this.height = 2;
this.originIcon = new ImageIcon(Objects.requireNonNull(NoScalingIcon.class.getResource(path)));
this.icon.setImage(this.originIcon.getImage().getScaledInstance(width, height, Image.SCALE_SMOOTH));
}
public AutoScalingIcon(int width, int height, String path) {
this.autoSize = false;
this.width = width;
this.height = height;
this.originIcon = new ImageIcon(Objects.requireNonNull(NoScalingIcon.class.getResource(path)));
this.icon.setImage(this.originIcon.getImage().getScaledInstance(width, height, Image.SCALE_SMOOTH));
}
public int getIconWidth() {
return this.icon.getIconWidth();
}
public int getIconHeight() {
return this.icon.getIconHeight();
}
public void paintIcon(Component c, Graphics g, int x, int y) {
if (this.autoSize) {
this.width = c.getWidth() != 0 ? c.getWidth() : (int) c.getPreferredSize().getWidth();
this.height = c.getHeight() != 0 ? c.getHeight() : (int) c.getPreferredSize().getHeight();
}
// 最低图片宽高,防止宽高在后面计算中为0
if (this.width < 2) {
this.width = 2;
}
if (this.height < 2) {
this.height = 2;
}
var g2d = (Graphics2D) g.create();
var at = g2d.getTransform();
if (this.lastScaleX != at.getScaleX() || this.lastScaleY != at.getScaleY()) {
var imgWidth = this.width;
var imgHeight = this.height;
if (at.getScaleX() > 1.0) {
imgWidth = (int) (this.width * at.getScaleX());
}
if (at.getScaleY() > 1.0) {
imgHeight = (int) (this.height * at.getScaleY());
}
// 在1除以缩放比例时,如果结果为无限小数,则需要将宽高减去1才能正确显示
if (this.cantDivide(1.0, at.getScaleX())) {
imgWidth -= 1;
}
if (this.cantDivide(1.0, at.getScaleY())) {
imgHeight -= 1;
}
this.icon.setImage(this.originIcon.getImage().getScaledInstance(imgWidth, imgHeight, Image.SCALE_SMOOTH));
this.lastScaleX = at.getScaleX();
this.lastScaleY = at.getScaleY();
}
// 新的 X 位置
var locationX = x * at.getScaleX();
// 新的 Y 位置
// Y 位置需要修正
var originY = y * at.getScaleY();
var offsetY = (this.height * at.getScaleY()) * ((at.getScaleY() - 1) / 2);
var locationY = originY + offsetY;
// 将缩放还原为 1.0
var scaled = AffineTransform.getScaleInstance(1.0 / at.getScaleX(), 1.0 / at.getScaleY());
at.concatenate(scaled);
g2d.setTransform(at);
// 绘制图片
this.icon.paintIcon(c, g2d, (int) locationX, (int) locationY);
g2d.dispose();
}
/* 判断两个数相除是否为无限小数 */
private boolean cantDivide(double a, double b) {
try {
new BigDecimal(a).divide(new BigDecimal(b));
} catch (ArithmeticException e) {
if (e.getMessage().contains("Non-terminating")) {
return true;
}
}
return false;
}
}
简单讲讲这个类的使用和原理:
- 两种构造方法,传入图片在package中的路径,若传入宽高则为固定尺寸,不传入宽高则使用容器尺寸
- 将显示缩放还原至 100% ,即该图片只对屏幕做点对点显示,根据在屏幕上显示所需要的实际像素点大小来进行尺寸压缩
如何使用
图片类搞定了,现在就来使用它。在此顺便封装一个专门用来显示图片的 JLabel 。
IconLabel.java
package com.jmd.ui.common;
import javax.swing.*;
import java.io.Serial;
public class IconLabel extends JLabel {
@Serial
private static final long serialVersionUID = -2643890482143441343L;
private final AutoScalingIcon icon;
public IconLabel(String path) {
this.icon = new AutoScalingIcon(path);
this.setIcon(this.icon);
}
}
使用方法:
- 创建上述简单封装的 IconLabel ,构造参数即为 package 中图片的位置
- 可以看到上述封装的代码中,没有传入宽高,因此图片会自动充满整个 JLabel 容器
var label = new IconLabel("/xxx/xxx/xxx.png");
panel.add(label);
本文就不发 git 了,直接将上述AutoScalingIcon.java
与IconLabel.java
的内容复制到自己项目中使用即可。