javaswingbordergraphics2dhidpi

Swing HiDPI Border rendering (Windows 10/OpenJRE 11)


I maintain an old Swing application that uses custom border implementations as window decorations with a look and feel implementation which extends MetalLookAndFeel. The look and feel overrides initComponentDefaults(UIDefaults table) and installs the custom border as "RootPane.frameBorder" and so on.

The custom border itself draws 32 lines of predefined colors horizontally together by implementing paintBorder(Component c, Graphics g, int x, int y, int width, int height). This works fine with Windows 10 2004 or newer when the display scaling is set to 100% in Settings > Display > Scale and layout. When display scaling is set to 125% or more, the border will not be drawn correctly, it has white lines between the lines that should be drawn together.

Fix scaling for apps in Advanced scaling settings seems to not affect the border drawing.

I use AdoptOpenJDK OpenJDK 11.0.4+11 x64. I expect Swing to scale the lines up too, like fonts, etc.

Why this is happening? How this might be fixed?

The border implementation:

public class CustomWindowBorder implements Border{

   public static final int BORDER_THICKNESS = 3;
   
   private static final CustomWindowBorder globalInstance = new CustomWindowBorder();

   public static CustomWindowBorder getInstance(){

      return CustomWindowBorder.globalInstance;
   }

   private CustomWindowBorder(){}
   
   @Override
   public Insets getBorderInsets(Component c){

      return new Insets(1,
                        CustomWindowBorder.BORDER_THICKNESS,
                        CustomWindowBorder.BORDER_THICKNESS,
                        CustomWindowBorder.BORDER_THICKNESS);
   }

   @Override
   public boolean isBorderOpaque(){

      return true;
   }

   @Override
   public void paintBorder(Component c,
                           Graphics g,
                           int x,
                           int y,
                           int width,
                           int height){

      Color[] decorationColors = [ommited]; // 30 colors in total
      Color[] borderColors = [ommited]; // 3 colors in total

      int yStart = 30;

      for(int i = 0; i < decorationColors.length; i++ ){
         // top
         Color clr = decorationColors[i];
         g.setColor(clr);

         g.drawLine(0,
                    i,
                    width,
                    i);
      }

      for(int i = 0; i < borderColors.length; i++ ){
         Color clr = borderColors[i];
         g.setColor(clr);

         // left
         g.drawLine(i,
                    yStart,
                    i,
                    height - i - 1);
         // right
         g.drawLine(width - i - 1,
                    yStart,
                    width - i - 1,
                    height - i - 1);
         // below
         g.drawLine(i,
                    height - i - 1,
                    width - i - 1,
                    height - i - 1);
      }
   }
}

Solution

  • I found out that Swing draws exactly one un-scaled pixel wide lines on the screen when using Graphics::drawLine or Graphics::fillRect with one pixel width or height, independently from Windows's display scaling.

    When I draw the border with Graphics::fillRect and let it fill every 30 horizontal lines of the top and 3 lines on the other sides, the white space will be filled completely.

    My solution is to draw each line over Graphics::fillRect and fill the whole remaining lines with the color, reducing the width/height of the filled rectangle while moving it to the next line. It's not the most efficient way to draw this, but it works for me.

    The modified paintBorder method:

    @Override
    public void paintBorder(Component c,
                            Graphics g,
                            int x,
                            int y,
                            int width,
                            int height){
    
       Color[] decorationColors = [ommited]; // 30 colors in total
       Color[] borderColors = [ommited]; // 3 colors in total
    
       int yStart = 30;
    
       for(int i = 0; i < decorationColors.length; i++ ){
          // top
          Color clr = decorationColors[i];
          g.setColor(clr);
    
          g.fillRect(0,
                     i,
                     width,
                     decorationColors.length - i);
       }
    
       for(int i = 0; i < borderColors.length; i++ ){
          Color clr = borderColors ;
          g.setColor(clr);
    
          // left
          g.fillRect(i,
                     yStart,
                     borderColors.length - i,
                     height - yStart - i);
    
       }
       for(int i = borderColors.length - 1; i >= 0; i-- ){
          Color clr = borderColors[i];
          g.setColor(clr);
    
          // right
          g.fillRect(width - i - 1,
                     yStart,
                     borderColors.length - i,
                     height - yStart - i);
          // below
          g.fillRect(i,
                     height - i - 1,
                     width - i,
                     borderColors.length - i);
       }
    }