Java - Как визуально центрировать определенную строку (а не только шрифт) в прямоугольнике

Я пытаюсь визуально центрировать произвольную строку, предоставленную пользователем, на JPanel. Я прочитал десятки других подобных вопросов и ответов здесь, на SO, но не нашел того, что непосредственно решает проблему, которую я имею.

В примере кода ниже getWidth() и getHeight() ссылаются на ширину и высоту JPanel, на которую я помещаю текстовую строку. Я обнаружил, что TextLayout.getBounds() очень хорошо говорит мне размер ограничивающего прямоугольника, который заключает в себе текст. Итак, я решил, что было бы относительно просто центрировать текстовый прямоугольник в прямоугольнике JPanel, вычисляя позиции x и y на JPanel нижнего левого угла прямоугольника, ограничивающего текст:

FontRenderContext context = g2d.getFontRenderContext();
messageTextFont = new Font("Arial", Font.BOLD, fontSize);
TextLayout txt = new TextLayout(messageText, messageTextFont, context);
Rectangle2D bounds = txt.getBounds();
xString = (int)((getWidth() - (int)bounds.getWidth()) / 2 );
yString = (int)((getHeight()/2) + (int)(bounds.getHeight()/2));

g2d.setFont(messageTextFont);
g2d.setColor(rxColor);
g2d.drawString(messageText, xString, yString);

Это отлично работало для строк, которые были прописными. Однако, когда я начал тестировать строки, содержащие строчные буквы с descenders (например, g, p, y), текст больше не был центрирован. Опускатели на строчных буквах (части, которые простираются ниже базовой линии шрифта) были слишком низки на JPanel, чтобы текст выглядел центрированным.

Что, когда я обнаружил (благодаря SO), что параметр y, переданный в drawString(), указывает базовую нарисованного текста, а не нижнюю границу. Таким образом, снова с помощью SO я понял, что мне нужно настроить размещение текста по длине descenders в моей строке:

....
    TextLayout txt = new TextLayout(messageText, messageTextFont, context);
    Rectangle2D bounds = txt.getBounds();
    int descent = (int)txt.getDescent();
    xString = (int)((getWidth() - (int)bounds.getWidth()) / 2 );
    yString = (int)((getHeight()/2) + (int)(bounds.getHeight()/2) - descent);
....

Я тестировал это со строками, тяжелыми строчными буквами, такими как g, p и y, и он отлично работает! WooHoo! Но ждать. Тьфу. Теперь, когда я пытаюсь только заглавными буквами, текст слишком высокий для JPanel, чтобы выглядеть центрированным.

Что, когда я обнаружил, что TextLayout.getDescent() (и все остальные методы getDescent(), которые я нашел для других классов), возвращает максимальный спуск FONT а не конкретной строки. Таким образом, моя верхняя строка была поднята для учета descenders, которые даже не встречались в этой строке.

Что мне делать? Если я не настрою параметр y для drawString() для учетной записи для descenders, то строчные строки с descenders визуально слишком низки на JPanel. Если я отредактирую параметр y для drawString() для учета descenders, тогда строки, которые не содержат никаких символов с descenders, визуально слишком высоки на JPanel. Кажется, мне не удалось определить, где базовая линия находится в прямоугольнике, ограничивающем текст, для строки GIVEN. Таким образом, я не могу точно определить, что y передать drawString().

Спасибо за любую помощь или предложения.

Ответы

Ответ 1

Пока я обманываю TextLayout, вы можете просто использовать контекст Graphics FontMetrics, например...

Text

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class LayoutText {

    public static void main(String[] args) {
        new LayoutText();
    }

    public LayoutText() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private String text;

        public TestPane() {
            text = "Along time ago, in a galaxy, far, far away";
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();

            g2d.setColor(Color.RED);
            g2d.drawLine(getWidth() / 2, 0, getWidth() / 2, getHeight());
            g2d.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2);

            Font font = new Font("Arial", Font.BOLD, 48);
            g2d.setFont(font);
            FontMetrics fm = g2d.getFontMetrics();
            int x = ((getWidth() - fm.stringWidth(text)) / 2);
            int y = ((getHeight() - fm.getHeight()) / 2) + fm.getAscent();

            g2d.setColor(Color.BLACK);
            g2d.drawString(text, x, y);

            g2d.dispose();
        }
    }

}

Хорошо, после некоторого беспокойства о...

В основном, рендеринг текста происходит на базовой линии, это делает положение границы y, как правило, выше над этой точкой, что делает его похожим на то, что текст был окрашен над позицией y

Чтобы преодолеть это, нам нужно добавить всплывание шрифта минус спуск шрифта в положение y...

Например...

FontRenderContext context = g2d.getFontRenderContext();
Font font = new Font("Arial", Font.BOLD, 48);
TextLayout txt = new TextLayout(text, font, context);

Rectangle2D bounds = txt.getBounds();
int x = (int) ((getWidth() - (int) bounds.getWidth()) / 2);
int y = (int) ((getHeight() - (bounds.getHeight() - txt.getDescent())) / 2);
y += txt.getAscent() - txt.getDescent();

... Вот почему я люблю рисовать текст вручную...

Runnable example...

Layout

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class LayoutText {

    public static void main(String[] args) {
        new LayoutText();
    }

    public LayoutText() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private String text;

        public TestPane() {
            text = "Along time ago, in a galaxy, far, far away";
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();

            g2d.setColor(Color.RED);
            g2d.drawLine(getWidth() / 2, 0, getWidth() / 2, getHeight());
            g2d.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2);

            FontRenderContext context = g2d.getFontRenderContext();
            Font font = new Font("Arial", Font.BOLD, 48);
            TextLayout txt = new TextLayout(text, font, context);

            Rectangle2D bounds = txt.getBounds();
            int x = (int) ((getWidth() - (int) bounds.getWidth()) / 2);
            int y = (int) ((getHeight() - (bounds.getHeight() - txt.getDescent())) / 2);
            y += txt.getAscent() - txt.getDescent();

            g2d.setFont(font);
            g2d.setColor(Color.BLACK);
            g2d.drawString(text, x, y);

            g2d.setColor(Color.BLUE);
            g2d.translate(x, y);
            g2d.draw(bounds);

            g2d.dispose();
        }
    }

}

Взгляните на Работа с текстовыми API для получения дополнительной информации...

Обновление

Как уже было предложено, вы можете использовать GlyphVector...

Каждое слово (Cat и Dog) вычисляется отдельно, чтобы продемонстрировать различия

CatDog

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class LayoutText {

    public static void main(String[] args) {
        new LayoutText();
    }

    public LayoutText() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private String text;

        public TestPane() {
            text = "A long time ago, in a galaxy, far, far away";
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();

            g2d.setColor(Color.RED);
            g2d.drawLine(getWidth() / 2, 0, getWidth() / 2, getHeight());
            g2d.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2);

            Font font = new Font("Arial", Font.BOLD, 48);
            g2d.setFont(font);

            FontRenderContext frc = g2d.getFontRenderContext();
            GlyphVector gv = font.createGlyphVector(frc, "Cat");
            Rectangle2D box = gv.getVisualBounds();

            int x = 0;
            int y = (int)(((getHeight() - box.getHeight()) / 2d) + (-box.getY()));
            g2d.drawString("Cat", x, y);

            x += box.getWidth();

            gv = font.createGlyphVector(frc, "Dog");
            box = gv.getVisualBounds();

            y = (int)(((getHeight() - box.getHeight()) / 2d) + (-box.getY()));
            g2d.drawString("Dog", x, y);

            g2d.dispose();
        }
    }

}

Ответ 2

Я думаю, этот ответ - правильный способ сделать это, однако у меня были проблемы в прошлом с пользовательскими шрифтами и их границами. В одном проекте мне приходилось прибегать к тому, чтобы получить контур шрифта и использовать эти границы. Этот метод, скорее всего, будет более интенсивным в памяти, но, похоже, это верный способ получения ограничений шрифта.

@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    Font font = new Font("Arial", Font.BOLD, 48);
    String text = "Along time ago, in a galaxy, far, far away";

    Shape outline = font.createGlyphVector(g.getFontMetrics().getFontRenderContext(), text).getOutline();
    // the shape returned is located at the left side of the baseline, this means we need to re-align it to the top left corner. We also want to set it the the center of the screen while we are there
    AffineTransform transform = AffineTransform.getTranslateInstance(
                -outline.getBounds().getX() + getWidth()/2 - outline.getBounds().width / 2, 
                -outline.getBounds().getY() + getHeight()/2 - outline.getBounds().height / 2);
    outline = transform.createTransformedShape(outline);
    g2d.fill(outline);
}

Как я уже сказал, прежде чем пытаться использовать метрики шрифта, но если все остальное не удается, попробуйте этот метод.