0

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);
        });
    }
}

enter image description here

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:

enter image description here

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:

enter image description here

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.

George Z.
  • 6,024
  • 4
  • 18
  • 33
  • There are probably good answers to these, but I am curious: 1) Why use HTML formatting? 2) Why not use HTML formatting for all labels? BTW: Swing support for HTML formatted text has always been .. less than ideal. This oddity does not surprise me. – Andrew Thompson May 02 '20 at 20:49
  • 1
    @AndrewThompson I use HTML formatting to take the advantage of line wraping as you said [here](https://stackoverflow.com/a/7861833/6579265). I edited my answer for the 2). – George Z. May 02 '20 at 21:02
  • 1
    I tried a few things locally and utterly failed to come up with anything useful. Sorry. – Andrew Thompson May 05 '20 at 14:03
  • take a look at this https://stackoverflow.com/questions/13228509/jlabel-not-in-line-with-text-in-j/13231510#13231510 – somurat May 05 '20 at 22:07
  • Works fine on Mac though – Tarun Lalwani May 06 '20 at 09:22
  • @TarunLalwani I was wondering how this is working in other OSes. Thanks for letting me know. – George Z. May 06 '20 at 11:20
  • @TarunLalwani I guess you don't have the Calibri font, so it is substituted. – Olivier May 06 '20 at 11:32
  • @Olivier *"I guess you don't have the Calibri font"* Good point. Could you (George) adjust the code to check for & report on the presence of Calibri? – Andrew Thompson May 07 '20 at 04:53
  • 1
    *"Thanks for the bounty as well."* No worries. It's nice to know rep. is good for something other than increased privileges. Mind you, when SE starts offering an exchange of 'cash for rep', all bets are off. I'm not holding my breath. ;) – Andrew Thompson May 07 '20 at 10:44

4 Answers4

3

A line of text consists of 3 parts:

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

without HTML

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

with HTML

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:

with alignOnBaseline

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:

JFixedLabel

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());
Olivier
  • 3,368
  • 1
  • 2
  • 13
  • 1
    George Z.: Glad you got a solution (however much you might feel it is a hack). I awarded the bounty to Olivier. Brilliant explanation of some intricacies related to fonts. :) – Andrew Thompson May 09 '20 at 01:28
  • @AndrewThompson Well, I am glad you did. He deserves it. – George Z. May 09 '20 at 08:32
  • @AndrewThompson Thanks ;-) I edited the answer to add a solution. – Olivier May 09 '20 at 09:51
  • @Olivier I really like this, and my answer cannot contest it. Unfortunately this will screw icon alignment: https://i.imgur.com/WyUqRjb.png . Is there a fix for this? – George Z. May 09 '20 at 10:14
  • @Olivier Btw, if you find/share a fix for the icon too, I will instant mark your answer, since it will be the one I am going to use and way better than mine. – George Z. May 09 '20 at 10:19
  • @Olivier What about this one? (Look the code too, the right label is a `JFixedLabel`) https://i.imgur.com/k7eGksD.png .Sorry for posting code as an image. I just do not know how else I can show it to you... – George Z. May 09 '20 at 10:33
  • @GeorgeZ. I added a new solution. – Olivier May 09 '20 at 11:44
  • @Olivier Unfortunately, changing the UI is not a solution I can follow. That's actually something I am not mentioning in my question (my bad), but I am looking for a fix that will work in Windows L&F as well. My labels have `com.sun.java.swing.plaf.windows.WindowsLabelUI`. A class I cannot override. – George Z. May 09 '20 at 11:46
  • @Olivier Scratch that. Actally, extending is possible (I thought it was not). `FixedLabelUI extends WindowsLabelUI`. It is an `access restriction` warning I get though. But who cares, it is a 3 lines fix. Thank you for your time & effort. – George Z. May 09 '20 at 11:55
1

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):

Font size 14 font size 22 font size 60

Even with other text-based components next to it:

with textfiedl

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):

bold 18bold 60

Look closer:

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: comparison

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);
        });
    }
}
George Z.
  • 6,024
  • 4
  • 18
  • 33
  • Be aware that your solution will not work with recent versions of Java because it's no longer possible to call `setAccessible(true)` on JDK classes (see [here](https://stackoverflow.com/questions/60241857/java-lang-unsupportedoperationexception-reflective-setaccessibletrue-disabled) for an example). I have updated my answer to provide an alternative, simpler solution. – Olivier May 09 '20 at 09:50
-1

<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

ultraGentle
  • 2,562
  • 1
  • 9
  • 23
-2

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;">