如何让自定义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
相关产品推荐
相关产品推荐

