一种解决Swing中JLabel图片在高分屏上显示不正常的方案

854 阅读4分钟

问题描述

Java Swing 是一个非常古老的 GUI 开发技术,不过虽然老旧,但同时也是市面上一个不可缺少的技术。比如 IDEA 全家桶都是基于 Swing 开发的。

具体问题先看看图片:

png

png

png

仔细对比上下两组图片,在超过 100% 缩放显示下,可以看到 Swing 界面里面的各个图片显示都很模糊。做过 Swing 开发的应该都知道这个图片缩放的痛点。

但是这个问题被我解决了:

png

png

问题原因

在使用 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 中,就需要给它设置一个固定的尺寸。于是问题出现在这里:

  1. 200 * 200 的图片被 Java 压缩至 30 * 30 的大小
  2. 此时桌面缩放为 150% ,该 30 * 30 的 JLabel 在屏幕上实际像素大小为 45 * 45
  3. 因此在第一步被压缩至 30 * 30 大小的图片,会被拉伸至 45 * 45 显示在屏幕上
  4. 于是上图中马赛克效果就出现了

如果是 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;
    }

}

简单讲讲这个类的使用和原理:

  1. 两种构造方法,传入图片在package中的路径,若传入宽高则为固定尺寸,不传入宽高则使用容器尺寸
  2. 将显示缩放还原至 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.javaIconLabel.java的内容复制到自己项目中使用即可。