Zweibieren
Zweibieren

Reputation: 412

Getting the bounds of the text inside a jlabel

I want to draw an arrow from the text in a JLabel to a point outside the JLabel. To start the arrow at an appropriate place, I need the bounds of the actual text inside the JLabel. My answer below will show how to get those bounds.

Upvotes: 1

Views: 1554

Answers (3)

MadProgrammer
MadProgrammer

Reputation: 347194

The Swing API already provides, if rather verbose, API to perform this operation - SwingUtilities#layoutCompoundLabel

protected TextBounds calculateTextBounds(JLabel label) {

    Rectangle paintIconR = new Rectangle();
    Rectangle paintTextR = new Rectangle();

    Dimension size = label.getSize();
    Insets insets = label.getInsets(null);
    String text = label.getText();
    Icon icon = (label.isEnabled()) ? label.getIcon()
                    : label.getDisabledIcon();
    Rectangle paintViewR = new Rectangle();
    paintViewR.x = insets.left;
    paintViewR.y = insets.top;
    paintViewR.width = size.width - (insets.left + insets.right);
    paintViewR.height = size.height - (insets.top + insets.bottom);
    paintIconR.x = paintIconR.y = paintIconR.width = paintIconR.height = 0;
    paintTextR.x = paintTextR.y = paintTextR.width = paintTextR.height = 0;

    String result = SwingUtilities.layoutCompoundLabel(
                    (JComponent) label,
                    label.getFontMetrics(label.getFont()),
                    text,
                    icon,
                    label.getVerticalAlignment(),
                    label.getHorizontalAlignment(),
                    label.getVerticalTextPosition(),
                    label.getHorizontalTextPosition(),
                    paintViewR,
                    paintIconR,
                    paintTextR,
                    label.getIconTextGap());

    TextBounds bounds = new TextBounds(paintViewR, paintTextR, paintIconR);

    return bounds;
}

Basically this will provide the "view" bounds (basically the area in which the label can be painted), the "text" bounds and the "icon" bounds, I just wrapped them in an easy to use class

So why would you use this approach?

  • This is actually the same work flow that is used to layout the label by the look and feel delegates
  • It doesn't require you to create a custom class, meaning that it can be applied to any instance of JLabel

Label text bounds

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Test {

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

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

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

    public class TextBounds {

        private Rectangle paintBounds;
        private Rectangle textBounds;
        private Rectangle iconBounds;

        public TextBounds(Rectangle paintBounds, Rectangle textBounds, Rectangle iconBounds) {
            this.paintBounds = paintBounds;
            this.textBounds = textBounds;
            this.iconBounds = iconBounds;
        }

        public Rectangle getPaintBounds() {
            return paintBounds;
        }

        public Rectangle getTextBounds() {
            return textBounds;
        }

        public Rectangle getIconBounds() {
            return iconBounds;
        }

    }

    public class TestPane extends JPanel {

        private JLabel label = new JLabel("Hello");

        public TestPane() {
            setLayout(new BorderLayout());
            label.setHorizontalAlignment(JLabel.CENTER);
            label.setVerticalAlignment(JLabel.CENTER);
            add(label);
        }

        protected TextBounds calculateTextBounds(JLabel label) {

            Rectangle paintIconR = new Rectangle();
            Rectangle paintTextR = new Rectangle();

            Dimension size = label.getSize();
            Insets insets = label.getInsets(null);
            String text = label.getText();
            Icon icon = (label.isEnabled()) ? label.getIcon()
                            : label.getDisabledIcon();
            Rectangle paintViewR = new Rectangle();
            paintViewR.x = insets.left;
            paintViewR.y = insets.top;
            paintViewR.width = size.width - (insets.left + insets.right);
            paintViewR.height = size.height - (insets.top + insets.bottom);
            paintIconR.x = paintIconR.y = paintIconR.width = paintIconR.height = 0;
            paintTextR.x = paintTextR.y = paintTextR.width = paintTextR.height = 0;

            String result = SwingUtilities.layoutCompoundLabel(
                            (JComponent) label,
                            label.getFontMetrics(label.getFont()),
                            text,
                            icon,
                            label.getVerticalAlignment(),
                            label.getHorizontalAlignment(),
                            label.getVerticalTextPosition(),
                            label.getHorizontalTextPosition(),
                            paintViewR,
                            paintIconR,
                            paintTextR,
                            label.getIconTextGap());

            TextBounds bounds = new TextBounds(paintViewR, paintTextR, paintIconR);

            System.out.println(result);
            System.out.println("view " + paintViewR);
            System.out.println("paint " + paintIconR);
            System.out.println("text " + paintTextR);

            return bounds;
        }

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

        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            TextBounds bounds = calculateTextBounds(label);
            g2d.setColor(Color.RED);
            g2d.draw(bounds.getPaintBounds());
            g2d.setColor(Color.GREEN);
            g2d.draw(bounds.getTextBounds());
            g2d.setColor(Color.BLUE);
            g2d.draw(bounds.getIconBounds());
            g2d.dispose();
        }

    }

}

Other solutions have the potential advantage of not creating three new Rectangle objects per call. This could be a problem if getTextBounds is called in a MouseMoved event listener. At some cost in complexity, the final Rectangle could be cached along with the JLabel width and height.

The Rectangles only need to be created once, they are passed into the API and the their values applied directly, so you're not creating new objects each time.

You also don't need to make use of a specialised component and can make use of the workflow on an pre-existing label

Upvotes: 1

Zweibieren
Zweibieren

Reputation: 412

To get the bounds of text in a JLabel, we need the accessible context, a protected field. So we extend JLabel. The accessible context only knows about the text if it is HTML, so I prepend "<html>" in the constructor; a more general version would first check to see if the text already begins with that string.

   class FieldLabel extends JLabel {
        FieldLabel(String text) {
            super("<html>"+text);
        }
        Rectangle getTextBounds() {
            JLabel.AccessibleJLabel acclab 
                = (JLabel.AccessibleJLabel) getAccessibleContext();
            if (acclab.getCharCount() <= 0)
                return null;
            Rectangle r0 = acclab.getCharacterBounds(0);
            Rectangle rn = acclab
                .getCharacterBounds(acclab.getCharCount()-1);

            return new Rectangle(r0.x, r0.y, 
                    rn.x+rn.width-r0.x, Math.max(r0.height, rn.height));
        }
    }

Other solutions have the potential advantage of not creating three new Rectangle objects per call. This could be a problem if getTextBounds is called in a MouseMoved event listener. At some cost in complexity, the final Rectangle could be cached along with the JLabel width and height.

Upvotes: 0

camickr
camickr

Reputation: 324108

You can use the SwingUtilities.layoutCompoundLabel

import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;

public class LabelLayout extends JLabel
{
    @Override
    protected void paintComponent(Graphics g)
    {
        super.paintComponent(g);

        Graphics grid = g.create();
        grid.setColor( Color.ORANGE );

        Rectangle viewR = new Rectangle();
        viewR.width = getSize().width;
        viewR.height = getSize().height;
        Rectangle iconR = new Rectangle();
        Rectangle textR = new Rectangle();

        String clippedText = SwingUtilities.layoutCompoundLabel
        (
            this,
            grid.getFontMetrics(),
            getText(),
            getIcon(),
            getVerticalAlignment(),
            getHorizontalAlignment(),
            getVerticalTextPosition(),
            getHorizontalTextPosition(),
            viewR,
            iconR,
            textR,
            getIconTextGap()
        );

        int gridSize = 10;
        int start = iconR.x;
        int end = iconR.x + iconR.width;

        System.out.println("Text bounds: " + textR );
        System.out.println("Icon bounds: " + iconR );

        for (int i = start; i < end; i += gridSize)
        {
            grid.drawLine(i, iconR.y, i, iconR.y + iconR.height);
        }

        grid.dispose();
    }

    private static void createAndShowGUI()
    {
        LabelLayout label = new LabelLayout();
        label.setBorder( new LineBorder(Color.RED) );
        label.setText( "Some Text" );
        label.setIcon( new ImageIcon( "DukeWaveRed.gif" ) );
        label.setVerticalAlignment( JLabel.CENTER );
        label.setHorizontalAlignment( JLabel.CENTER );
//      label.setVerticalTextPosition( JLabel.BOTTOM );
        label.setVerticalTextPosition( JLabel.TOP );
        label.setHorizontalTextPosition( JLabel.CENTER );

        JFrame frame = new JFrame("SSCCE");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add( label );
        frame.setLocationByPlatform( true );
        frame.pack();
        frame.setSize(300, 200);
        frame.setVisible( true );
    }

    public static void main(String[] args)
    {
        EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                createAndShowGUI();
            }
        });
    }
}

This example paints extra lines above the icon in the label.

Upvotes: 1

Related Questions