You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让自定义Swing组件随HiDPI感知LAF自动缩放?

问题描述

当使用GTK这类支持HiDPI的Look and Feel(LAF)而非Metal时,标准Swing组件会自动进行缩放适配,但自定义组件无法同步缩放。示例代码中切换LAF时,按钮等组件尺寸随LAF变化,而自定义的十字组件尺寸保持不变。如何实现自定义组件随关联组件同步缩放?指定GDK_SCALING会全局改变显示,并非合适方案,实际应用是依赖HiDPI缩放的大型应用的一部分。

示例代码如下:

import java.awt.*;
import javax.swing.*;
public class DpiScale{
    public static void setLaf( String s , JFrame frame){
        try{
            UIManager.setLookAndFeel(s);
            SwingUtilities.updateComponentTreeUI(frame);
            frame.pack();
        } catch(Exception e){
            //oh well
        }
    }
    
    static class CustomComponent extends JPanel{
        int cross = 25;
        @Override
        public Dimension getPreferredSize(){
            return new Dimension( cross, cross );
        }
        @Override
        public void paintComponent( Graphics g){
            g.setColor(Color.BLUE);
            g.drawLine(0, 0, cross, cross);
            g.drawLine(0, cross, cross, 0);
        }
    }
    
    public static void main(String[] args) throws Exception{
            for(UIManager.LookAndFeelInfo lafi: UIManager.getInstalledLookAndFeels()){
                System.out.println(lafi.getClassName());
            }
            
        JFrame frame = new JFrame();
        JButton gtk = new JButton("GTK");
        gtk.addActionListener( 
                evt->{
                    setLaf("com.sun.java.swing.plaf.gtk.GTKLookAndFeel", frame);
                    frame.validate(); 
                    }
                );
        
        JButton metal = new JButton("metal");
        metal.addActionListener( 
                evt->{
                    setLaf("javax.swing.plaf.metal.MetalLookAndFeel", frame);
                    frame.validate();
                } 
            );
        frame.add(gtk, BorderLayout.NORTH);
        frame.add(new CustomComponent(), BorderLayout.CENTER);
        frame.add(metal, BorderLayout.SOUTH);
        frame.pack();
        frame.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
        frame.setVisible(true);
    }
}

切换LAF后,左侧为Metal、右侧为GTK时,自定义十字组件尺寸未变化。

解决方案

要让自定义组件随LAF的HiDPI缩放同步调整,核心是获取当前LAF的缩放因子,并基于该因子调整自定义组件的尺寸和绘制逻辑,而非使用固定像素值。以下是具体实现步骤:

1. 获取当前环境的缩放因子

通过Swing内置API获取准确的缩放比例,优先使用GraphicsConfiguration的变换信息,它能直接反映LAF应用的缩放:

private double getScaleFactor() {
    GraphicsConfiguration gc = getGraphicsConfiguration();
    if (gc != null) {
        return gc.getDefaultTransform().getScaleX();
    }
    // 降级方案:基于标准96DPI计算屏幕分辨率缩放
    Toolkit toolkit = Toolkit.getDefaultToolkit();
    return toolkit.getScreenResolution() / 96.0;
}

2. 动态计算组件首选尺寸

将基础尺寸乘以缩放因子,替代固定数值:

static class CustomComponent extends JPanel{
    int baseCross = 25; // 对应标准96DPI的基础尺寸
    
    @Override
    public Dimension getPreferredSize(){
        double scale = getScaleFactor();
        int scaledCross = (int) Math.round(baseCross * scale);
        return new Dimension(scaledCross, scaledCross);
    }
}

3. 适配绘制逻辑

可以直接使用缩放后的尺寸绘制,或者对Graphics2D应用缩放变换,简化绘制代码:

@Override
public void paintComponent(Graphics g){
    super.paintComponent(g); // 必须调用父类方法完成背景绘制
    double scale = getScaleFactor();
    
    // 使用Graphics2D缩放变换,无需修改绘制坐标
    Graphics2D g2d = (Graphics2D) g.create();
    g2d.scale(scale, scale);
    g2d.setColor(Color.BLUE);
    g2d.drawLine(0, 0, baseCross, baseCross);
    g2d.drawLine(0, baseCross, baseCross, 0);
    g2d.dispose();
}

4. 监听LAF切换事件

LAF切换时会触发组件的UI属性变更,添加监听以实时更新组件:

public CustomComponent() {
    addPropertyChangeListener("UI", evt -> {
        revalidate(); // 重新计算布局
        repaint();    // 重绘组件
    });
}

完整修改后的代码

import java.awt.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.*;

public class DpiScale {
    public static void setLaf(String s, JFrame frame) {
        try {
            UIManager.setLookAndFeel(s);
            SwingUtilities.updateComponentTreeUI(frame);
            frame.pack();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class CustomComponent extends JPanel {
        int baseCross = 25; // 标准96DPI下的基础尺寸

        public CustomComponent() {
            // 监听UI变化,LAF切换时触发更新
            addPropertyChangeListener("UI", (PropertyChangeEvent evt) -> {
                revalidate();
                repaint();
            });
        }

        @Override
        public Dimension getPreferredSize() {
            double scale = getScaleFactor();
            int scaledCross = (int) Math.round(baseCross * scale);
            return new Dimension(scaledCross, scaledCross);
        }

        @Override
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            double scale = getScaleFactor();

            // 应用缩放变换绘制十字
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.scale(scale, scale);
            g2d.setColor(Color.BLUE);
            g2d.drawLine(0, 0, baseCross, baseCross);
            g2d.drawLine(0, baseCross, baseCross, 0);
            g2d.dispose();
        }

        private double getScaleFactor() {
            GraphicsConfiguration gc = getGraphicsConfiguration();
            if (gc != null) {
                return gc.getDefaultTransform().getScaleX();
            }
            Toolkit toolkit = Toolkit.getDefaultToolkit();
            return toolkit.getScreenResolution() / 96.0;
        }
    }

    public static void main(String[] args) throws Exception {
        for (UIManager.LookAndFeelInfo lafi : UIManager.getInstalledLookAndFeels()) {
            System.out.println(lafi.getClassName());
        }

        JFrame frame = new JFrame();
        JButton gtk = new JButton("GTK");
        gtk.addActionListener(evt -> {
            setLaf("com.sun.java.swing.plaf.gtk.GTKLookAndFeel", frame);
            frame.validate();
        });

        JButton metal = new JButton("Metal");
        metal.addActionListener(evt -> {
            setLaf("javax.swing.plaf.metal.MetalLookAndFeel", frame);
            frame.validate();
        });

        frame.add(gtk, BorderLayout.NORTH);
        frame.add(new CustomComponent(), BorderLayout.CENTER);
        frame.add(metal, BorderLayout.SOUTH);
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setVisible(true);
    }
}

关键说明

  • 避免固定像素值:所有自定义尺寸都基于基础值乘以缩放因子,保证不同DPI/LAF下视觉比例一致。
  • 依赖Swing内置API:GraphicsConfiguration的变换信息能准确获取LAF应用的缩放比例,比手动计算更可靠。
  • 响应UI变更:通过监听UI属性变化,确保LAF切换时组件及时更新尺寸和绘制内容。

内容的提问来源于stack exchange,提问作者matt

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 19:40:33