Calibri 字体在 <html> 文本移动到组件的底部

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

没有太多要解释的。请看下面的 MCVE/image:

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 Whosebug");
        withoutHtml.setFont(FONT);
        withoutHtml.setBorder(BorderFactory.createLineBorder(Color.red));
        add(withoutHtml);

        JLabel withHtml = new JLabel("<html><body style='vertical-align:top;'>hello Whosebug");
        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);
        });
    }
}

绿色的带有 <html> 标签。 有没有办法修复它? 并且通过修复,我的意思是让它像左边那个一样,没有这个愚蠢的东西space?

其他任何字体似乎都不会发生这种情况(我又测试了 2-3 种)。我在 Java 8 和 Windows 7 和 Windows 10.

我尝试在底部添加填充:

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

正如预期的那样,我得到的是:

其中 a) 将拧紧同一容器中其他组件的对齐方式(不利于 UI 目的)并且 b) 我将不得不对自 5 以来的许多值进行硬编码,因为这是正确的字体大小14。但对于其他字体大小,它需要另一个值。

@Andrew Thomson 在评论中说对所有 JLabel 使用 HTML 格式。但是,如果它们紧挨着另一个基于文本的组件,例如 JTextField,我会得到:

这显然也很糟糕。

更新

此外,我尝试从网上下载 Calibri 字体(以及 "Calibri Light" 等变体)并按照 in this question 所述进行安装。我不知道 "Overrides" 是否是现有的,但我得到了相同的结果。

有两种方法可以解决这个问题,添加

html {
    margin:0;
}

或向文本的两个位添加填充。 :) 当然你可以试试 <html style="margin:0;">

<body style='vertical-align:text-bottom;' 对我有用,但如果我误解了你的问题,你可以在 https://developer.mozilla.org/en-US/docs/Web/CSS/vertical-align

找到其他值

一行文字由三部分组成:

为了看得更清楚,我用了50号的Calibri,没有HTML的标签是:

在HTML模式下,情况有所不同。 HTML 渲染器将前导 放在第一位 (出于某种原因):

这给出了您观察到的令人不快的结果。

现在你会问"But why do I see that effect only with Calibri?"其实所有的字体都有这个效果,只是通常小很多,所以你没有注意到。

这是一个输出一些常见 Windows 字体指标的程序:

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

对于 50 码,结果为:

                  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

如您所见,与其他字体相比,Calibri 的行距很大。

对于尺寸 14,结果为:

                  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

Calibri 的前导仍然是 3 个像素。其他字体有0或1,表示对它们的影响不明显或很小。

似乎无法更改 HTML 渲染器的行为。但是,如果目标是对齐相邻组件的基线,那么这是可能的。您使用的 FlowLayout 有一个 alignOnBaseline 属性。如果启用它,它会正确对齐组件:

更新 1

这里的 JFixedLabel class 给出了相同的结果,无论它包含 HTML 还是纯文本。它在 HTML 模式下将 Graphics 转换为主值:

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

结果:

更新 2

之前的解决方案存在图标问题,因此这里有一个新的解决方案可以同时处理文本和图标。这里我们不扩展JLabel,而是定义一个新的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;
    }
}

要将 UI 分配给标签,请执行以下操作:

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

Olivier's answer 建议使用 flowLayout.setAlignOnBaseline(true); 但它不会在其他 Layoutmanagers 中工作,例如 GridLayout。但是,它帮助我 很多 找到了我正在寻找的确切解决方案。即使它是 messy/hacky 个。

这里是:

如果你 System.out.println(label.getFontMetrics(label.getFont())),你会看到 FontMetrics 的实际 class 是 FontDesignMetrics。对我们来说幸运的是,值 ascentdescentleading 的 getter 依赖于字段而没有进行一些疯狂的计算。对我们 vol.2 来说幸运的是,对于相同的字体,这些字体指标是相同的 (equals)。这意味着,对于每个 Font style-size 组合(显然是它的家族),我们有一个 FontDesignMetrics 实例。

换句话说:

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

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

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

即使在不同的标签中调用了 getFontMetrics,它也会打印 true。如果你 withHtml.setFont(FONT.deriveFont(Font.BOLD)); 你会看到它打印 false。因为字体不同,我们有不同的字体指标实例。

修复

(免责声明:危急时刻需要采取危急措施

正如我已经提到的,它有点老套并且依赖于 reflection。使用 reflection 我们可以操纵这 3 个值。类似于:

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

但是您要为每种字体 size/style 硬编码值,或者您可以按照我的做法进行操作。

我所做的是从其他字体的 FontMetrics 复制这些值。看起来在 Calibri 字体的情况下,Tahoma 就是那个。

首先,创建更改字段值的方法,取自 Tahoma 字体指标:

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

现在,用 copyTahomaFontMetricsTo(withHtml); 来称呼它,而不关心它是 withHtml 标签还是 withoutHtml,因为 它们都具有相同的字体。

结果(框架标题中的字体大小):

即使旁边还有其他 text-based 个组件:

如您所见,它成功了!再加上布局对齐没有搞砸。

看起来很完美,其实不然。

同样,如前所述,对于每种字体(familysizestyle 的组合),都有一个 FontMetrics 实例。将其中一个标签的字体更改为 Font.BOLD 将阻止我们获得完美对齐。可能错过一个(或两个)像素。另外,对于 Bold,我们还必须 copyTahomaFontMetricsTo

copyTahomaFontMetricsTo(withoutBoldFont);
copyTahomaFontMetricsTo(withBoldFont);

和结果(框架标题上的字体大小):

仔细看:

有一个像素的差异。但我想我会接受它,因为这比 Swing's/Windows 默认 Calibri-HTML 行为好(方式):

完整示例:

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 Whosebug");
        withoutHtml.setBorder(BorderFactory.createLineBorder(Color.GREEN));
        withoutHtml.setFont(FONT.deriveFont(Font.BOLD));
        add(withoutHtml);

        JLabel withHtml = new JLabel("<html>hello Whosebug");
        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);
        });
    }
}