Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java - How to visually center a specific string (not just a font) in a rectangle

I am trying to visually center an arbitrary user-supplied string on a JPanel. I have read dozens of other similar questions and answers here on SO but haven't found any that directly address the problem I am having.

In the code sample below, getWidth() and getHeight() refer to the width and height of the JPanel on which I'm placing the text string. I have found that TextLayout.getBounds() does a very good job of telling me the size of a bounding rectangle that encloses the text. So, I figured that it would be relatively simple to center the text rectangle in the JPanel rectangle by calculating the x and y positions on the JPanel of the lower left corner of the text-bounding rectangle:

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

This worked perfectly for strings which were all uppercase. However, when I started testing with strings that contained lowercase letters with descenders (like g, p, y), the text was no longer centered. The descenders on the lower case letters (the parts that extend below the baseline of the font) were being drawn too low on the JPanel to have the text appear to be centered.

That's when I discovered (thanks to SO) that the y parameter passed to drawString() specifies the baseline of the drawn text, not the lower bound. Thus, again with the help of SO, I realized that I needed to adjust the placement of the text by the length of the descenders in my string:

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

I tested this with strings heavy in lowercase letters like g, p, and y and it worked great! WooHoo! But....wait. Ugh. Now when I try with only uppercase letters, the text is way too HIGH on the JPanel to look centered.

That's when I discovered that TextLayout.getDescent() (and all the other getDescent() methods I have found for other classes) returns the maximum descent of the FONT not of the specific string. Thus, my uppercase string was being raised up to account for descenders that didn't even occur in that string.

What am I to do? If I don't adjust the y parameter for drawString() to account for descenders then lowercase strings with descenders are visually too low on the JPanel. If I do adjust the y parameter for drawString() to account for the descenders then strings which do not contain any characters with descenders are visually too high on the JPanel. There doesn't seem to be any way for me to determine where the baseline is in the text-bounding rectangle for a GIVEN string. Thus, I can't figure out exactly what y to pass to drawString().

Thanks for any help or suggestions.

like image 491
MikeW Avatar asked May 19 '14 04:05

MikeW


2 Answers

While I muck about with TextLayout, you could just use the Graphics context's FontMetrics, for example...

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

}

Okay, after some fussing about...

Basically, text rendering occurs at the baseline, this makes the y position of the bounds usually appear above this point, making it look like the text is been painted above the y position

To overcome this, we need to add the font's ascent minus the font's descent to the y position...

For example...

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

... This is why I love rendering text by hand ...

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

}

Take a look at Working with Text APIs for more information...

Updated

As has already been suggested, you could use a GlyphVector...

Each word (Cat and Dog) is calculated separatly to demonstrate the differences

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

}
like image 163
MadProgrammer Avatar answered Oct 27 '22 00:10

MadProgrammer


I think this answer is the correct way to do it however I have had problems in the past with custom fonts and getting their bounds. In one project I had to resort to actually getting the outline of the font and using those bounds. This method is likely more memory intensive however it seems to be a surefire way for getting font bounds.

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

Like I said before try to use the font metrics but if all else fails try this method out.

like image 30
ug_ Avatar answered Oct 27 '22 02:10

ug_