Java swing application too small in ~HiDpi~ comput

2019-01-09 12:10发布

问题:

I have a java desktop application which uses java swing and it works fine with normal displays.

But when come to ~hiDpi~ displays( 3200*1800) whole application is too small.

As application code is very large and complex, it is difficult to rearrange code to match with hi dpi. Is there a solution to this problem?

I have seen application like IntelliJ idea and eclipse works fine with this kind of displays without a problem. Appreciate any help.

回答1:

Some time ago, I was tasked with developing a solution which would allow a user to increase or decrease the font size of the application dynamically. Needless to say, I spent a lot of time tearing my hair out (mostly because my predecessor was instant on using setPreferredSize and other stupid ideas which made any solution a pain to implement)

The following is an example of the idea I came up with. It allows you to modify the UIManagers "font" properties, applying a scale to the font size.

Basically, it scans the UIManagers UIDefaults, picking out all the "font" based attributes and stores these as a "base". Once it's done that, it uses these values, calculates the font size, based on the scale and the original size, and updates the values in the UIManager

import java.awt.Font;
import java.util.HashMap;
import java.util.Map;
import javax.swing.UIManager;
import sun.swing.SwingLazyValue;

public class FontUtilities {

    private static Map<String, Font> originals;

    public static void setFontScale(float scale) {

        if (originals == null) {
            originals = new HashMap<>(25);
            for (Map.Entry entry : UIManager.getDefaults().entrySet()) {
                Object key = entry.getKey();
                if (key.toString().toLowerCase().contains(".font")) {
                    Object value = entry.getValue();
                    Font font = null;
                    if (value instanceof SwingLazyValue) {
                        SwingLazyValue lazy = (SwingLazyValue) entry.getValue();
                        value = lazy.createValue(UIManager.getDefaults());
                    }

                    if (value instanceof Font) {
                        font = (Font) value;
                        originals.put(key.toString(), font);
                    }
                }
            }
        }

        for (Map.Entry<String, Font> entry : originals.entrySet()) {
            String key = entry.getKey();
            Font font = entry.getValue();

            float size = font.getSize();
            size *= scale;

            font = font.deriveFont(Font.PLAIN, size);
            UIManager.put(key, font);
        }
    }

}

The example is taking from the Swing tutorials, with the addition of the font scaling

Basically, when ever the + button is clicked, it's running this code...

scale += 1f;
FontUtilities.setFontScale(scale);
SwingUtilities.updateComponentTreeUI(TextSamplerDemo.this);
updateUI();
revalidate();
repaint();

The basic idea would be to define a scaling algorithm, based on the "default" screen resolution been 1 and as the screen resolution/DPI increases, you can increase the font scaling to follow.

There are problems with this. It might not work on look and feels (looking at you nimbus) and if you define your own fonts they won't be updated. It's also a really great way to see when you've done stupid things with the api, as it will play havoc with your layouts

The other solution would be to use JXLayer/JLayer to dynamically scale the UI as a whole.

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.RenderingHints;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.jdesktop.jxlayer.JXLayer;
import org.pbjar.jxlayer.demo.TransformUtils;
import org.pbjar.jxlayer.plaf.ext.transform.DefaultTransformModel;

public class TestJLayerZoom {

    public static void main(String[] args) {
        new TestJLayerZoom();
    }

    public TestJLayerZoom() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private JXLayer<JComponent> layer;
        private DefaultTransformModel transformModel;
        private JPanel content;

        public TestPane() {

            content = new JPanel(new GridBagLayout());
            GridBagConstraints gbc = new GridBagConstraints();
            gbc.gridy = 0;

            JLabel label = new JLabel("Hello");
            JTextField field = new JTextField("World", 20);

            content.add(label, gbc);
            content.add(field, gbc);

            gbc.gridy++;
            gbc.gridwidth = GridBagConstraints.REMAINDER;

            final JSlider slider = new JSlider(50, 200);
            slider.addChangeListener(new ChangeListener() {

                @Override
                public void stateChanged(ChangeEvent e) {
                    int value = slider.getValue();
                    double scale = value / 100d;
                    transformModel.setScale(scale);
                }
            });
            content.add(slider, gbc);

            transformModel = new DefaultTransformModel();
            transformModel.setScaleToPreferredSize(true);

            Map<RenderingHints.Key, Object> hints = new HashMap<>();
            //hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            //hints.put(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
            //hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

            layer = TransformUtils.createTransformJXLayer(content, transformModel, hints);
            setLayout(new BorderLayout());
            add(layer);


        }

    }

}

This is based on the idea presented here. There is an additional library linked in the answer which you will need to make this work



回答2:

I don't have ready solution, but:

If your elements are in a JPanel in JFrame (standard way), write your own Layout Manager, that scales this root Jpanel to 1/4 of size of JFrame, and make the JFrame bigger four times in size (width x 2, lenght x 2).

Override painting methods in JFrame, and some logic with getGraphics() (you need to investigate that), ensure, that method setScale(2,2) is invoked on graphics before it is given to drawing code, it will make everything draw four times bigger.

Use java.awt.EventQueue, write your own EventQueue that will proxy all mouse events, and translate them to be 1/4 of the original position in window(x/2, y/2), then let them go, they should fire events on proper elements.

The whole idea is to let the components stay in their original size (virtually), but scale drawing code and mouse events, so it should look and work same way as on lower resolution displays.