Calibri Font when in <html> text moves to the bottom part of the component

只谈情不闲聊 提交于 2021-02-11 14:00:28

问题


There is not a lot to explain. Just see the MCVE/image below:

public class FontExample extends JFrame {
    private static final Font FONT = new Font("Calibri", Font.PLAIN, 14);

    public FontExample() {
        super("");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLayout(new FlowLayout());

        JLabel withoutHtml = new JLabel("hello stackoverflow");
        withoutHtml.setFont(FONT);
        withoutHtml.setBorder(BorderFactory.createLineBorder(Color.red));
        add(withoutHtml);

        JLabel withHtml = new JLabel("<html><body style='vertical-align:top;'>hello stackoverflow");
        withHtml.setBorder(BorderFactory.createLineBorder(Color.green));
        withHtml.setFont(FONT);
        add(withHtml);

        setLocationByPlatform(true);
        pack();
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            //Make sure Calibri font is installed
            if (!"Calibri".equals(FONT.getFamily())) {
                System.err.println("Font calibri is not installed.");
                System.exit(1);
            }
            new FontExample().setVisible(true);
        });
    }
}

The green one is with the <html> tag. Is there a way to fix it? And by fix, I mean to make it like the left one, without this stupid space?

It does not seem to happen with any other font (I tested 2-3 more). I am on Java 8 with Windows 7 and Windows 10.

I tried to add padding at bottom:

JLabel withHtml = new JLabel("<html><body style='padding-bottom:5px'>hello stackoverflow");

and as expected what I get is this:

which a) will screw the alignment of other components in the same container (bad for UI purposes) and b) I will have to hard code a lot of values since 5 since to be the proper for font size 14. But for other font size, it needs another value.

@Andrew Thomson in comments said to use the HTML format for all JLabels. But then, if they are next to another text-based component like a JTextField, I get this:

which obviously, is bad too.

UPDATE

Also, I tried to download Calibri font (among with variations like "Calibri Light", etc) somewhere from the web and install it as described in this question. I do not know if that "Overrides" the existing one, but I had the same result.


回答1:


A line of text consists of 3 parts:

  • The ascent
  • The descent
  • The leading

To see more clearly, I used Calibri with size 50. The label without HTML is:

In HTML mode, things are different. The HTML renderer puts the leading first (for some reason):

This gives the unpleasant result you have observed.

Now you will ask "But why do I see that effect only with Calibri?" In fact the effect exists with all fonts, but it's usually much smaller, so you don't notice it.

Here is a program that outputs the metrics for some common Windows fonts:

import java.awt.*;
import javax.swing.JLabel;

public class FontInfo
{
    static void info(String family, int size)
    {
        Font font = new Font(family, Font.PLAIN, size);
        if(!font.getFamily().equals(family))
            throw new RuntimeException("Font not available: "+family);
        FontMetrics fm = new JLabel().getFontMetrics(font);
        System.out.printf("%-16s %2d %2d %2d\n", family, fm.getAscent(), fm.getDescent(), fm.getLeading());
    }

    public static void main(String[] args)
    {
        String[] fonts = {"Arial", "Calibri", "Courier New", "Segoe UI", "Tahoma", "Times New Roman", "Verdana"};
        System.out.printf("%-16s %s\n", "", " A  D  L");
        for(String f : fonts)
            info(f, 50);
    }
}

For size 50, the results are:

                  A  D  L
Arial            46 11  2
Calibri          38 13 11
Courier New      42 15  0
Segoe UI         54 13  0
Tahoma           50 11  0
Times New Roman  45 11  2
Verdana          51 11  0

As you can see, the leading for Calibri is huge compared to the other fonts.

For size 14, the results are:

                  A  D  L
Arial            13  3  1
Calibri          11  4  3
Courier New      12  5  0
Segoe UI         16  4  0
Tahoma           14  3  0
Times New Roman  13  3  1
Verdana          15  3  0

The leading for Calibri is still 3 pixels. Other fonts have 0 or 1, which means the effect for them is invisible or very small.

It doesn't seem possible to change the behavior of the HTML renderer. However, if the goal is to align the baselines of adjacent components, then it is possible. The FlowLayout you have used has an alignOnBaseline property. If you enable it, it does align the components correctly:

UPDATE 1

Here's a JFixedLabel class that gives the same result, whether it contains HTML or plain text. It translates the Graphics by the leading value when in HTML mode:

import java.awt.Graphics;
import javax.swing.JLabel;
import javax.swing.plaf.basic.BasicHTML;

public class JFixedLabel extends JLabel
{
    public JFixedLabel(String text)
    {
        super(text);
    }

    @Override
    protected void paintComponent(Graphics g)
    {
        int dy;
        if(getClientProperty(BasicHTML.propertyKey)!=null)
            dy = getFontMetrics(getFont()).getLeading();
        else
            dy = 0;
        g.translate(0, -dy);
        super.paintComponent(g);
        g.translate(0, dy);
    }
}

Result:

UPDATE 2

The previous solution had an issue with icons, so here's a new one that handles both text and icons. Here we don't extend JLabel, instead we define a new UI class:

import java.awt.*;
import javax.swing.*;
import javax.swing.plaf.basic.BasicHTML;
import javax.swing.plaf.metal.MetalLabelUI;

public class FixedLabelUI extends MetalLabelUI
{
    @Override
    protected String layoutCL(JLabel label, FontMetrics fontMetrics, String text, Icon icon,
        Rectangle viewR, Rectangle iconR, Rectangle textR)
    {
        String res = super.layoutCL(label, fontMetrics, text, icon, viewR, iconR, textR);
        if(label.getClientProperty(BasicHTML.propertyKey)!=null)
            textR.y -= fontMetrics.getLeading();
        return res;
    }
}

To assign the UI to a label, do like this:

JLabel label = new JLabel();
label.setUI(new FixedLabelUI());



回答2:


Olivier's answer suggests to use flowLayout.setAlignOnBaseline(true); but it will not work in another Layoutmanagers, e.g GridLayout. However, it helped me a lot to find the exact solution I was looking for. Even if it is a messy/hacky one.

Here it is:

If you System.out.println(label.getFontMetrics(label.getFont())), you will see that the actual class of the FontMetrics is FontDesignMetrics. Luckily for us, the getters for the values ascent, descent and leading rely on the fields without some crazy calculations. Luckily for us vol.2, These font metrics are the same (equals) for the same font. That means, we have a single FontDesignMetrics instance of for each Font style-size combination (and obviously its family).

With other words:

private static final Font FONT = new Font("Calibri", Font.PLAIN, 50);

JLabel withoutHtml = new JLabel("hello stackoverflow");
withoutHtml.setFont(FONT);
add(withoutHtml);

JLabel withHtml = new JLabel("<html>hello stackoverflow");
withHtml.setFont(FONT);
FontMetrics withHtmlFontMetrics = withHtml.getFontMetrics(withHtml.getFont());
FontMetrics withoutHtmlFontMetrics = withoutHtml.getFontMetrics(withoutHtml.getFont());
boolean equals = withHtmlFontMetrics.equals(withoutHtmlFontMetrics);
System.out.println(equals);

It prints true even if the getFontMetrics was called in different labels. If you withHtml.setFont(FONT.deriveFont(Font.BOLD)); you will see that it prints false. Because the font is different, we have different font metrics instance.

The fix

(Disclaimer: Desperate times call for desperate measures)

As I already mentioned, it's some sort of hacky and it relies on reflection. With reflection we can manipulate these 3 values. Something like:

FontMetrics fontMetrics = label.getFontMetrics(label.getFont());
Field descentField = fontMetrics.getClass().getDeclaredField("descent");
descentField.setAccessible(true);
descentField.set(fontMetrics, 0);

But you are going to either hard code values for each font size/style, or you can do what I did.

What I did is to copy these values from other font's FontMetrics. It looks that in case of Calibri font, Tahoma is the one.

First, create the method that change the values in the fields, taken from Tahoma font metrics:

private static void copyTahomaFontMetricsTo(JComponent component) {
    try {
        FontMetrics calibriMetrics = component.getFontMetrics(component.getFont());

        // Create a dummy JLabel with tahoma font, to obtain tahoma font metrics
        JLabel dummyTahomaLabel = new JLabel();
        dummyTahomaLabel.setFont(new Font("Tahoma", component.getFont().getStyle(), component.getFont().getSize()));
        FontMetrics tahomaMetrics = dummyTahomaLabel.getFontMetrics(dummyTahomaLabel.getFont());

        Field descentField = calibriMetrics.getClass().getDeclaredField("descent");
        descentField.setAccessible(true);
        descentField.set(calibriMetrics, tahomaMetrics.getDescent());

        Field ascentField = calibriMetrics.getClass().getDeclaredField("ascent");
        ascentField.setAccessible(true);
        ascentField.set(calibriMetrics, tahomaMetrics.getAscent());

        Field leadingField = calibriMetrics.getClass().getDeclaredField("leading");
        leadingField.setAccessible(true);
        leadingField.set(calibriMetrics, tahomaMetrics.getLeading());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Now, call it by: copyTahomaFontMetricsTo(withHtml); without caring if its the withHtml label or the withoutHtml, since they both have the same font.

The result (font size in frame title):

Even with other text-based components next to it:

As you see, it is works! Plus the layout alignment is not screwed.

It looks perfect, but it's not.

Again, as mentioned earlier, for each font (combination of family, size and style), there is one instance of FontMetrics. Changing one of these label's font to Font.BOLD will stop us from getting perfect alignment. Probably a one (or two) pixels miss. Plus we will have to copyTahomaFontMetricsTo for the Bold as well:

copyTahomaFontMetricsTo(withoutBoldFont);
copyTahomaFontMetricsTo(withBoldFont);

and the result (again font size on frame's title):

Look closer:

There is one pixel difference. But I guess I will take it since this is way (way) better than Swing's/Windows default Calibri-HTML behavior:

The complete example:

public class FontExample extends JFrame {
    private static final Font FONT = new Font("Calibri", Font.PLAIN, 20);

    public FontExample() {
        super("Font: " + FONT.getSize());
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLayout(new FlowLayout());

        JLabel withoutHtml = new JLabel("hello stackoverflow");
        withoutHtml.setBorder(BorderFactory.createLineBorder(Color.GREEN));
        withoutHtml.setFont(FONT.deriveFont(Font.BOLD));
        add(withoutHtml);

        JLabel withHtml = new JLabel("<html>hello stackoverflow");
        withHtml.setBorder(BorderFactory.createLineBorder(Color.RED));
        withHtml.setFont(FONT);

        copyTahomaFontMetricsTo(withoutHtml);
        copyTahomaFontMetricsTo(withHtml);
        add(withHtml);

        setLocationByPlatform(true);
        pack();
    }

    private static void copyTahomaFontMetricsTo(JLabel label) {
        try {
            FontMetrics calibriMetrics = label.getFontMetrics(label.getFont());

            // Create a dummy JLabel with tahoma font, to obtain tahoma font metrics
            JLabel dummyTahomaLabel = new JLabel();
            dummyTahomaLabel.setFont(new Font("Tahoma", label.getFont().getStyle(), label.getFont().getSize()));
            FontMetrics tahomaMetrics = dummyTahomaLabel.getFontMetrics(dummyTahomaLabel.getFont());

            Field descentField = calibriMetrics.getClass().getDeclaredField("descent");
            descentField.setAccessible(true);
            descentField.set(calibriMetrics, tahomaMetrics.getDescent());

            Field ascentField = calibriMetrics.getClass().getDeclaredField("ascent");
            ascentField.setAccessible(true);
            ascentField.set(calibriMetrics, tahomaMetrics.getAscent());

            Field leadingField = calibriMetrics.getClass().getDeclaredField("leading");
            leadingField.setAccessible(true);
            leadingField.set(calibriMetrics, tahomaMetrics.getLeading());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            new FontExample().setVisible(true);
        });
    }
}



回答3:


<body style='vertical-align:text-bottom;' worked for me, but if I'm misunderstanding your question, you can find other values at https://developer.mozilla.org/en-US/docs/Web/CSS/vertical-align




回答4:


Two ways you can probably handle this, add

html {
    margin:0;
}

or add padding to both bits of text. :) Of course you can try <html style="margin:0;">



来源:https://stackoverflow.com/questions/61565649/calibri-font-when-in-html-text-moves-to-the-bottom-part-of-the-component

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