HiDPI support in Java Swing for multiple look and feels

蓝咒 提交于 2021-02-06 12:59:08

问题


I'm looking to add Hi-DPI support to some Swing applications, but I have failed to find a solution sufficient to my needs. I need to support multiple look & feels, so the situation seems rather more complex than other posts I've found (which tend to suggest "adapt your UI sizes to match your font size").

Some experimentation has found the UIManager contains many metrics that can be tweaked to get you off to a good start in making an application Hi-DPI friendly. (The UIManager-Defaults utility has been invaluable for exploring these!) What I found though is that the L&Fs seem to work completely differently to each other:

  • Windows L&F gives you a good (not perfect) default font size, and built-in icons (like checkbox & window icons) are sized appropriately - but many other metrics are still out of whack.

  • In Metal you can update the fonts in the UIManager individually. With a bit of work you can scale the built-in IconUIResources to match.

  • In Nimbus you can just update a single default font, and the other fonts fall into place... but it's beyond me how to scale the built-in icons and have combo-boxes, radio buttons (etc) render successfully!

The feeling I get from playing around is that it should be possible to create a list of specific tweaks for each L&F-specific independently. This would comprise potentially tweaking the defaults for Font, Icon, Integer and Dimensions.

Has anyone come up with a good solution to this?

Can anyone share a definitive list of which UIDefaults need adjustment for the standard L&Fs?

I would be happy with a solution that just supports Metal and Windows consistently.

I imagine such a solution should be quite reusable & could solve the same problem for a range of Swing apps. I'm surprised no such utility yet seems to exist. (Please enlighten me if not!) This approach won't solve everything of course (for example you'd still need to manually scale any calls to setPreferredSize etc. Then again apps that already support mulitple L&Fs should tend to avoid calling that anyway.) Still, I imagine that it could get many apps off to a good start.

I'm aware that JDK-9 promises full Hi-DPI support, but I cannot wait that long - I may not be able to switch for some time even after it's 2017 release.


回答1:


Here's a solution based on my initial prototyping.

I'm not happy with some parts, for example the rules for which 'Integers' and 'Fonts' to modify is very "magical". That's one place I'd like to hear from others with more Swing / L&F experience.

I've made this a community wiki so that if people would prefer to iterate on this and add their knowledge, rather than post their own solutions, then please feel free.

I started with an interface that can modify certain ui-defaults:

public interface Tweaker {
  void initialTweaks();
  Font modifyFont(Object key, Font original);
  Icon modifyIcon(Object key, Icon original);
  Integer modifyInteger(Object key, Integer original);
}

Which can be invoked like this:

public void scaleUiDefaults() {
  float dpiScale = Toolkit.getDefaultToolkit().getScreenResolution() / 96f;
  Tweaker delegate = createTweakerForCurrentLook(dpiScale);
  tweakUiDefaults(delegate, dpiScale);
}

private Tweaker createTweakerForCurrentLook(float dpiScaling) {
  String testString = UIManager.getLookAndFeel().getName().toLowerCase();
  if (testString.contains("windows")) return new WindowsTweaker(dpiScaling);
  if (testString.contains("nimbus")) return new NimbusTweaker(dpiScaling);
  return new BasicTweaker(dpiScaling);
}

private void tweakUiDefaults(Tweaker delegate, float multiplier) {
  UIDefaults defaults = UIManager.getLookAndFeelDefaults();

  delegate.initialTweaks();

  for (Object key: Collections.list(defaults.keys())) {
    Object original = defaults.get(key);
    Object newValue = getUpdatedValue(delegate, key, original);
    if (newValue != null && newValue != original) {
      defaults.put(key, newValue);
    }
  }
}

private Object getUpdatedValue(Tweaker delegate, Object key, Object original) {
  if (original instanceof Font)    return delegate.modifyFont(key, (Font) original);
  if (original instanceof Icon)    return delegate.modifyIcon(key, (Icon) original);
  if (original instanceof Integer) return delegate.modifyInteger(key, (Integer) original);
  return null;
}

I put functionality that looks to be used by the majority of L&Fs in a base-class. This seems to handle Metal without further refinement:

public class BasicTweaker {
  protected final float scaleFactor;
  protected final UIDefaults uiDefaults = UIManager.getLookAndFeelDefaults();

  public BasicTweaker(float scaleFactor) {
    this.scaleFactor = scaleFactor;
  }

  public void initialTweaks() {}

  public Font modifyFont(Object key, Font original) {

    // Ignores title & accelerator fonts (for example)
    if (original instanceof FontUIResource && key.toString().endsWith(".font")) {
        return newScaledFontUIResource(original, scaleFactor);
    }
    return original;
  }

  protected static FontUIResource newScaledFontUIResource(Font original, float scale) {
    int newSize = Math.round(original.getSize() * scale);
    return new FontUIResource(original.getName(), original.getStyle(), newSize);
  }

  public Icon modifyIcon(Object key, Icon original) {
    return new IconUIResource(new ScaledIcon(original, scaleFactor));
  }

  public Integer modifyInteger(Object key, Integer original) {
    if (!endsWithOneOf(lower(key), LOWER_SUFFIXES_FOR_SCALED_INTEGERS)) {
      return original;
    }
    return (int) (original * scaleFactor);
  }

  private boolean endsWithOneOf(String text, String[] suffixes) {
    return Arrays.stream(suffixes).anyMatch(suffix -> text.endsWith(suffix));
  }

  private String lower(Object key) {
    return (key instanceof String) ? ((String) key).toLowerCase() : "";
  }

  private static final String[] LOWER_SUFFIXES_FOR_SCALED_INTEGERS = 
    new String[] { "width", "height", "indent", "size", "gap" };
}

class ScaledIcon used above is probably getting out of scope - but it essentially just calls ImageIcon(image).paintIcon with a modified width & height.

Then override BasicTweaker for other L&Fs...

Windows:

public class WindowsTweaker extends BasicTweaker {

  public WindowsTweaker(float scaleFactor) {

    // Windows already scales fonts, scrollbar sizes (etc) according to the system DPI settings.
    // This lets us adjust to the REQUESTED scale factor, relative to the CURRENT scale factor
    super(scaleFactor / getCurrentScaling());
  }

  private static float getCurrentScaling() {
    int dpi = Toolkit.getDefaultToolkit().getScreenResolution();
    return dpi / 96f;
  }

  public Font modifyFont(Object key, Font original) {
    return super.modifyFont(key, original);
  }

  public Icon modifyIcon(Object key, Icon original) {
    return original;
  }
}

Nimbus:

public class NimbusTweaker extends BasicTweaker {

  public NimbusTweaker(float scaleFactor) {
    super(scaleFactor);
  }

  public void initialTweaks() {
    Font font = uiDefaults.getFont("defaultFont");
    if (font != null) {
      uiDefaults.put("defaultFont", new FontUIResource(
          font.getName(), font.getStyle(), Math.round(font.getSize() * scaleFactor)));
    }
  }

  // Setting "defaultFont" above is sufficient as this will be inherited by all others
  public Font modifyFont(Object key, Font original) {
    return original;
  }

  // Scaling Radio or CheckBox button icons leads to really weird artifacts in Nimbus? Disable
  public Icon modifyIcon(Object key, Icon original) {
    return original;
  }
}



回答2:


  • not an answer, just my insights to this problematics issue, and please don't delete this answer, it can be very informative for the next one, two year,

  • my points, (I'm restrict to lazy Win user, could be interesting the experiences by real power users from LI/(U)NIX or OSX)

    1. tend to returns back use NullLayout, despite the fact that the UIManager (I think that this is still a true) is able handling with getPreferredSize 21k x 48k, but seems like as LayoutManager doesn't know to divide those value properly, you can see it, this issue you can see by using GridBagLayout, there is reall issue with scaling/zoom_in for 4k sceens, I think that AWT/Swing GUI is rendered correclty at 2k monitors,

    2. for 2k/4k screens you have to overide all Keys in UIManager (e.g. AFAIK for Nimbus L&F is possible to use structure stored in xml file directly)

      • have to create own paintIcon (in AWT/Swing are used simple shapes)
      • override Borders
      • override mouse events
      • override Focus events
    3. those steps are partially required for "smooth GUI" in todays aplications too, from old 4:3 screens -> thought laptops with small_screens with low resolutione (HD_screens) -> fullhd screen -> fullhs wide sceen, ends with 2k screen, note I'm never tried by using the sizing varians builts in Nimbus L&F, profesional application must contains test, checking for

      • Native OS (there is difference betweens font, border, sizing)
      • get a screen resolution in pixels from primary display with screen scalling, e.g., especially FullHd screen (16:9) and wide FullHd screen (21:9)
      • to cache all displays in the case that you have the multiple monitors and resolution in pixels is different (problem is in the case that there are two the same monitors, but there is discreasing setting - users settings to GPU, or monitor has active TV card - settings can be modified by TV chip)

      • then GUI can be created with hardcoded matrix for various sizing, to fits all screen standards, by default with success by override getPreferredSize for (parent) container(s), then LayoutManager will accept getPreferredSize form hardcoded matrix as prototype and do own job correctly,

    4. if I remember another interesting facts, bugs, and whatever, ... etc I'll edit this my longer comment, with my remarks



来源:https://stackoverflow.com/questions/38169983/hidpi-support-in-java-swing-for-multiple-look-and-feels

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!