Vertical Swing Labels

One limitation of Java AWT/Swing is the inability to rotate controls such as labels, although you can freely rotate elements in a custom-drawn graphics context. I did try invoking a label’s paint method in a paintComponent override after rotating the graphics context but couldn’t get it to work, probably because of the label’s built-in assumptions of how to draw itself.

So I wrote a small class using custom string drawing that simulates a vertically rotated JLabel. You can find the entire code below. Only simple text strings are supported. Another of Swing’s limitations is the lack of any public interface to its HTML parsing, so the usual ability of Swing text controls to interpret HTML formatting tags is absent here. Nor did I bother with images or positioning properties as I don’t need them at present.

Lastly, some items of interest regarding custom controls.

  • I extend JPanel rather than JComponent or JLabel. The former is too primitive – it comes without any Look & Feel styling, so most of its properties are simply null and would have to be set manually. But JLabel is too complex, with many additional properties I don’t support and its own display behavior. JPanel is a good middle ground, coming fully initialized with the current Look & Feel but without any behavioral baggage. (Hence its popularity as a superclass!)
  • There is a problem regarding the font, though. Labels use the default GUI font in most frameworks and styles, but this is not the case for Swing Metal which uses a bold variant. So we fetch the font of a temporary text label, falling back on the JPanel font if that fails.
  • The JRotateLabel is remeasured whenever its font or text changes. We use the current FontMetrics for that purpose, adding 1 pixel in each dimension which is later also added as an offset during drawing. Together with the font’s advance and leading this ensures we stay a pixel away from the actual edge all-around.
  • Rather than overriding the methods for minimum, maximum, and preferred size I just set these properties directly. JRotateLabel does not need to compute them dynamically and a JPanel without children won’t touch them, so there’s no need for overrides.

import java.awt.*;
import java.awt.geom.AffineTransform;
import javax.swing.*;

/**
 * Provides a {@link JLabel} variant that is rotated 90° to the left or right.
 * Supports only simple text, i.e. neither HTML content nor images,
 * and no relative positioning of the text within the label bounds.
 * Properties such as custom fonts and colors are respected, however.
 *
 * @author Christoph Nahr
 * @version 1.0.0
 */
public class JRotateLabel extends JPanel {
    private static final long serialVersionUID = 0L;

    private static Font _defaultFont;
    {
        if (_defaultFont == null) {
            _defaultFont = new JLabel("text").getFont();
            if (_defaultFont == null) _defaultFont = getFont();
        }
        setFont(_defaultFont);
        addPropertyChangeListener("font", e -> remeasure());
    }

    private boolean _faceLeft;
    private Dimension _size;
    private String _text;

    /**
     * Creates a {@link JRotateLabel} with no initial text.
     */
    public JRotateLabel() {
        setText(null);
    }

    /**
     * Creates a {@link JRotateLabel} with the specified initial text.
     * @param text the text to display
     */
    public JRotateLabel(String text) {
        setText(text);
    }

    /**
     * Creates a {@link JRotateLabel} with the specified initial text and facing.
     * @param text the text to display
     * @param faceLeft {@code true} if the baseline of the {@link JRotateLabel}
     *                 should face left, {@code false} if it should face right
     */
    public JRotateLabel(String text, boolean faceLeft) {
        _faceLeft = faceLeft;
        setText(text);
    }

    /**
     * Indicates whether the {@link JRotateLabel} is facing left.
     * The default is {@code false}.
     * @return {@code true} if the baseline of the {@link JRotateLabel}
     *         is facing left, {@code false} if it is facing right
     */
    public boolean getFaceLeft() {
        return _faceLeft;
    }

    /**
     * Determines whether the {@link JRotateLabel} is facing left.
     * @param faceLeft {@code true} if the baseline of the {@link JRotateLabel}
     *                 should face left, {@code false} if it should face right
     */
    public void setFaceLeft(boolean faceLeft) {
        _faceLeft = faceLeft;
        repaint(); // same size, no remeasure needed
    }

    /**
     * Gets the text displayed in the {@link JRotateLabel}.
     * @return the text displayed in the {@link JRotateLabel}
     */
    public String getText() {
        return _text;
    }

    /**
     * Sets the text to display in the {@link JRotateLabel}.
     * @param text the text to display in the {@link JRotateLabel}
     */
    public void setText(String text) {
        _text = text;
        remeasure();
    }

    /**
     * Remeasures and repaints the {@link JRotateLabel}.
     */
    private void remeasure() {
        if (_text == null || _text.isEmpty())
            _size = new Dimension();
        else {
            final FontMetrics metrics = getFontMetrics(getFont());
            _size = new Dimension(metrics.getHeight() + 1,
                    metrics.stringWidth(_text) + 1);
        }
        setMinimumSize(_size);
        setMaximumSize(_size);
        setPreferredSize(_size);

        repaint();
    }

    /**
     * Invoked by Swing to draw the content area of the {@link JRotateLabel}.
     * @param g the {@link Graphics2D} context in which to paint
     */
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        if (_text == null || _text.isEmpty())
            return;

        final Graphics2D g2 = (Graphics2D) g;
        final AffineTransform transform = g2.getTransform();

        if (_faceLeft) {
            g2.rotate(Math.toRadians(90));
            g2.drawString(_text, 1,
                    -1 - g2.getFontMetrics().getDescent());
        } else {
            g2.rotate(Math.toRadians(-90));
            g2.drawString(_text, 1 - _size.height,
                    g2.getFontMetrics().getAscent());
        }
        g2.setTransform(transform);
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.