Java - 如何在矩形中视觉居中一个特定的字符串(而非仅为字体)

16

我想在JPanel上视觉居中一个任意用户提供的字符串。我已经阅读了许多其他类似的问题和答案,但没有找到直接解决我遇到的问题的。

在下面的代码示例中,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);

这种方法对于全是大写字母的字符串完美适用。但是,当我开始测试包含有下降字形(如g、p、y等)的小写字母的字符串时,文本就不再居中了。小写字母的下降部分(即延伸到字体基线以下的部分)被绘制得太低,以至于文本看起来没有居中。

这时,我发现(多亏了SO)传递给drawString()的y参数指定的是文本的基线而不是下边界。因此,也多亏了SO,我意识到需要根据我的字符串下降的长度调整文本的位置:

....
    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)进行测试,效果很好!但是…等等,什么情况?现在当我尝试仅使用大写字母时,文本在JPanel上太高,看起来不居中。

这就是我发现 TextLayout.getDescent() (以及我找到的其他类的所有getDescent()方法)返回的是FONT的最大下行距,而不是特定字符串的。因此,我的大写字符串被抬高了,以解释该字符串中根本不存在的下行字符。

我该怎么办呢?如果我不调整drawString()的y参数来解释下行字符,那么带下行字符的小写字符串在JPanel上会显得太低。如果我确实调整drawString()的y参数来解释下行字符,那么没有包含任何字符下行的字符串在JPanel上会显得太高。似乎没有办法确定给定字符串文本边界矩形中基线的位置。因此,我无法确定要传递给drawString()的确切y值。

谢谢您提供的任何帮助或建议。


如果您只想显示一行文本,您可以使用类似此示例的东西。 - MadProgrammer
yString = (int)(((getHeight() + (int) bounds.getHeight()) / 2) - descent); - geowar
2个回答

30
虽然我在处理,但你可以直接使用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();

这就是为什么我喜欢手动渲染文本的原因...

可以运行的示例...

布局

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...

为了演示差异,每个单词(CatDog)都被单独计算。

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

}

1
我总是倾向于直接使用从GlyphVector获取的Shape。这样,我们不仅可以轻松地获得形状大小的双精度值,还可以将其用作图形剪辑或填充/绘制,就像这个例子中所示。在Java中,似乎有十几种方法来实现这一点。 - Andrew Thompson
@AndrewThompson 在这里不是要争论,我个人认为 TextLayout 确实有点奇怪,与其他方法相比似乎不太直观。 - MadProgrammer
感谢MadProgrammer的快速测试响应。 我遇到的问题是,正如您所说,getAscent()和getDescent()提供字体的上升和下降,而不管特定字符串中的任何字符是否实际延伸到完全上升或下降。 因此,如果你有一个涵盖了全部范围的字符串,它看起来接近于居中:[链接](http://i.imgur.com/3teY5GJ.png)但是,如果你有一个基本上没有下降或基本上没有上升的字符串,它可能会出现严重错误:[链接](http://i.imgur.com/DBiKCfn.png)。 您有什么想法吗? - MikeW
2
@user3651208,我用GlyphVector做了一个测试,请看更新。 - MadProgrammer
1
谢谢MadProgrammer扩展您的答案以包括GlyphVector。这正是我需要的!感谢@Andrew Thompson在这方面的贡献。也许GlyphVector不是所有文本居中情况的解决方案,但它解决了我的问题。与上面评论中链接的“AAAA”和“yyyy”示例不居中的方式相反,使用您的GlyphVector代码,这两个不同的示例都完美地居中:linklink - MikeW
显示剩余11条评论

6

我认为这个答案是正确的方法,但是我过去曾遇到自定义字体的问题,无法获取其边界。在一个项目中,我不得不使用字体轮廓并使用这些边界。这种方法可能更加占用内存,但它似乎是获取字体边界的一种稳妥方法。

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

就像我之前说的一样,尽量使用字体度量标准,但是如果其他方法都失败了,请尝试这种方法。


1
“在一个项目中,我不得不求助于获取字体轮廓并使用这些边界。”噢,就像我刚才评论的那样! 我刚刚完成了大约20多个示例,它们都使用字形的Shape。 您可以在我的Facebook页面上看到其中一些结果。” - Andrew Thompson
Ug_,我本来想尝试GlyphVector方法,但后来决定来SO看看是否有关于使用FontMetrics或TextLayout的问题。当我看到你的答案时,我开始觉得这可能是适合我的方法。幸运的是,@MadProgrammer在他的示例代码中添加了GlyphVector,证明了这一点。感谢您首先提到了这个问题,尽管最终我接受了MadProgrammer的答案。我标记了您的答案为有用,因为我认为您和Andrew Thompson帮助推动了尝试这个解决方案。 - MikeW
@user3651208 感谢您的回复,我很感激。我同意疯狂的程序员绝对有一个更好的答案,并且在这个问题上提供了很多很好的细节,值得被采纳 :) - ug_

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接