Шрифт Calibri, когда в <html>текст перемещается в нижнюю часть компонента - PullRequest
0 голосов
/ 02 мая 2020

Там не так много, чтобы объяснить. Просто посмотрите MCVE / изображение ниже:

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

Зеленый - с тегом <html>. Есть ли способ исправить это? И под исправлением я имею в виду сделать его как левый, без этого тупого пространства?

Кажется, что это не происходит с любым другим шрифтом ( Я тестировал еще 2-3). Я на Java 8 с Windows 7 и Windows 10.

Я попытался добавить заполнение внизу:

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

и, как и ожидалось, я получаю следующее:

enter image description here

, которые а) привинчивают выравнивание других компонентов в том же контейнере (плохо для целей пользовательского интерфейса) и б) мне придется жестко кодировать много значений, начиная с 5, поскольку он подходит для размера шрифта 14. Но для другого размера шрифта ему нужно другое значение.

@ Эндрю Томсон в комментариях сказал, что использует формат HTML для всех JLabels. Но затем, если они находятся рядом с другим текстовым компонентом, таким как JTextField, я получаю это:

enter image description here

, что, очевидно, тоже плохо .

ОБНОВЛЕНИЕ

Кроме того, я попытался скачать шрифт Calibri (среди вариантов, таких как "Calibri Light", et c) где-нибудь из Интернета и установить его как описано в этом вопросе . Я не знаю, переопределяет ли это существующий, но у меня был тот же результат.

Ответы [ 4 ]

2 голосов
/ 08 мая 2020

Строка текста состоит из 3 частей:

Чтобы увидеть более четко, я использовал Calibri с размером 50. Этикетка без HTML:

without HTML

В режиме HTML все по-другому. Рендерер HTML ставит ведущий первым (по какой-то причине):

with HTML

Это дает неприятный результат, который вы наблюдали .

Теперь вы спросите: "Но почему я вижу этот эффект только с 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. Если вы включите его, он правильно выровняет компоненты:

with alignOnBaseline

ОБНОВЛЕНИЕ 1

Вот JFixedLabel класс, который дает тот же результат, содержит ли он HTML или простой текст. Он переводит Graphics на ведущее значение в режиме HTML:

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

Результат:

JFixedLabel

ОБНОВЛЕНИЕ 2

Предыдущее решение имело проблему с иконками, поэтому вот новое, которое обрабатывает как текст, так и значки. Здесь мы не расширяем JLabel, вместо этого мы определяем новый класс пользовательского интерфейса:

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

Чтобы назначить пользовательский интерфейс метке, сделайте так:

JLabel label = new JLabel();
label.setUI(new FixedLabelUI());
1 голос
/ 08 мая 2020

Ответ Оливье предлагает использовать flowLayout.setAlignOnBaseline(true);, но он не будет работать в других менеджерах по разметке, например, GridLayout. Тем не менее, это помогло мне найти точное решение, которое я искал. Даже если это грязный / хакерский.

Вот оно:

Если вы System.out.println(label.getFontMetrics(label.getFont())), вы увидите, что фактический класс FontMetrics равен FontDesignMetrics . К счастью для нас, методы получения значений ascent, descent и leading полагаются на поля без каких-либо сумасшедших вычислений. К счастью для нас, vol.2, эти метрики шрифта одинаковы (equals) для одного шрифта. Это означает, что у нас есть один экземпляр FontDesignMetrics для каждой комбинации стиля и размера шрифта (и, очевидно, ее семейства).

Другими словами:

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

Он печатает true даже если getFontMetrics был вызван в разных ярлыках. Если вы 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);

Но вы будете либо жестко кодировать значения для каждого размера / стиля шрифта, либо можете делать то, что я сделал.

Я скопировал эти значения из других шрифтов 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, так как они оба имеют одинаковый шрифт.

Результат (шрифт размер в заголовке кадра):

Font size 14 font size 22 font size 60

Даже с другим текстом На основе компонентов рядом с ним:

with textfiedl

Как видите, это работает! Плюс выравнивание макета не прикручено.

Выглядит идеально, но это не так.

Опять же, как упоминалось ранее, для каждого шрифта (сочетание family, size и style), есть один экземпляр FontMetrics. Изменение шрифта одного из этих ярлыков на Font.BOLD помешает нам получить идеальное выравнивание. Вероятно, пропущен один (или два) пикселя. Плюс нам нужно будет copyTahomaFontMetricsTo для Bold:

copyTahomaFontMetricsTo(withoutBoldFont);
copyTahomaFontMetricsTo(withBoldFont);

и результат (опять же размер шрифта в заголовке фрейма):

bold 18bold 60

Посмотрите ближе:

closer

Разница в один пиксель. Но я думаю, я возьму это, так как это намного лучше, чем поведение Swing / Windows по умолчанию Calibri- HTML: comparison

Полный пример:

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);
        });
    }
}
0 голосов
/ 07 мая 2020

<body style='vertical-align:text-bottom;' работал для меня, но если я неправильно понимаю ваш вопрос, вы можете найти другие значения в https://developer.mozilla.org/en-US/docs/Web/CSS/vertical-align

0 голосов
/ 02 мая 2020

Вы можете справиться с этим двумя способами: добавить

html {
    margin:0;
}

или добавить отступы к обоим битам текста. :) Конечно, вы можете попробовать <html style="margin:0;">

...