Click here to Skip to main content
12,634,117 members (26,467 online)
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

7.4K views
255 downloads
2 bookmarked
Posted

Drawing Text with Wrapping and Text Alignment

, 19 May 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
A simple class which provides drawing support for formatted text

Introduction

This is a simple class implementation which provides automatic formatting for text, when rendering onto a Java Graphics handle. The Java Graphics drawString method does not natively support the ability to word-wrap text when drawing, nor does it support automatic text alignment. The purpose of the class is to fill these gaps to simplify textual graphic operations.

Implementation

In order to implement this formatting utility, several important classes were used. Some of these classes determine information about the text, such as the dimensions and offsets of the target Font, while others provide the functions necessary for measuring and rendering the text.

The alignment of the text is determined using an enumerator, which contains a single entry for each of the regions within rectangular bounds:

public enum TextAlignment
{
    TOP_LEFT,
    TOP,
    TOP_RIGHT,
    MIDDLE_LEFT,
    MIDDLE,
    MIDDLE_RIGHT,
    BOTTOM_LEFT,
    BOTTOM,
    BOTTOM_RIGHT
};

The alignment enumerations are simple enough to understand. Any enumerations which are not suffixed with a 'left' or 'right' identifier are simply center aligned. These enumerations cover the sections of rectangular bounds when the bounds are divided into a 3x3 grid (similar to the .NET Framework ContentAlignment enumeration.)

Additional formatting options are available through an additional class, which contains several static fields which determine extra formatting solutions which should be applied when rendering the text.

  • TextFormat.NONEIndicates that no additional formatting should be applied.
  • TextFormat.NO_ANTI_ALIASINGIndicates that the text should be rendered without anti-aliasing.
  • TextFormat.FIRST_LINE_VISIBLEIndicates that the first line which is rendered should always be visible. This applies, particularly, to text alignments in the 'middle' or 'bottom' of the bounds.
These additional flags need not be supplied to the rendering method, as the value TextFormat.NONE will be used by default. The flags also function as bit-masks, so or'ing the flags will result in multiple formats being applied, i.e., TextFormat.NO_ANTI_ALIASING | TextFormat.FIRST_LINE_VISIBLE

Using the above classes for formatting options, the rendering method utilizes the main classes and handles the text drawing operation.

public static Rectangle drawString(Graphics g, String text, 
Font font, Color color, Rectangle bounds, TextAlignment align, int format)
{

The above is the main declaration of the method, with all optional parameters supplied. Each parameter is pretty self explanatory, and documentation is supplied with the class.

if (g == null)
    throw new NullPointerException("The graphics handle cannot be null.");
if (text == null)
    throw new NullPointerException("The text cannot be null.");
if (font == null)
    throw new NullPointerException("The font cannot be null.");
if (color == null)
    throw new NullPointerException("The text color cannot be null.");
if (bounds == null)
    throw new NullPointerException("The text bounds cannot be null.");
if (align == null)
    throw new NullPointerException("The text alignment cannot be null."); 

All arguments are checked to ensure that no null-values are referenced while processing the method. This is to prevent future exceptions when handling the arguments.

if (text.length() == 0)
    return new Rectangle(bounds.x, bounds.y, 0, 0); 

Should the text being rendered be empty, the method does not attempt to paint the text. This helps with increasing the rendering speed, where calculating the bounds of the text would be overhead calculations for no result.

Graphics2D g2D = (Graphics2D)g;

AttributedString attributedString = new AttributedString(text);
attributedString.addAttribute(TextAttribute.FOREGROUND, color);
attributedString.addAttribute(TextAttribute.FONT, font); 

The Graphics object is cast into a Graphics2D object for future use with the TextLayout.draw method, which requires such an object.

The text is also bound to an AttributeString object which is used for the LineBreakMeasurer later in the method. The AttributedString class functions as both a data storage utility (for holding the text) and for storing attribute information which can be used to alter the rendering behaviour. As such, both the foreground color and the font of the text are retained as attributes in the class.

AttributedCharacterIterator attributedCharIterator = attributedString.getIterator();

FontRenderContext fontContext = new FontRenderContext(null, !TextFormat.isEnabled(format, TextFormat.NO_ANTI_ALIASING), false);
LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(attributedCharIterator, fontContext); 

The code retrieves an iterator object for the AttributedString textual data, for parsing the content, and constructs a FontRenderContext which provides additional formatting options for the rendering operation. In the constructor, we read the TextFormat.NO_ANTI_ALIASING flag to enable or disable anti-aliasing on the text.

A LineBreakMeasurer class is constructed which functions as the measurer for widths of individual lines. The class constructor accepts the AttributedCharacterIterator, for parsing individual characters in the string when calculating, and the FontRenderContext for the additional formatting options when calculating the bounds (when anti-aliasing is enabled, the bounds of the text may be adjusted.)

Point targetLocation = new Point(bounds.x, bounds.y);
int nextOffset = 0;

if (align.isMiddle() || align.isBottom())
{
    if (align.isMiddle())
        targetLocation.y = bounds.y + (bounds.height / 2);
    if (align.isBottom())
        targetLocation.y = bounds.y + bounds.height;

    while (lineMeasurer.getPosition() < text.length())
    {
        nextOffset = lineMeasurer.nextOffset(bounds.width);
        nextOffset = nextTextIndex(nextOffset, lineMeasurer.getPosition(), text);
        
        TextLayout textLayout = lineMeasurer.nextLayout(bounds.width, nextOffset, false);
        
        if (align.isMiddle())
            targetLocation.y -= (textLayout.getAscent() + textLayout.getLeading() + textLayout.getDescent()) / 2;
        if (align.isBottom())
            targetLocation.y -= (textLayout.getAscent() + textLayout.getLeading() + textLayout.getDescent());
    }

    if (TextFormat.isEnabled(format, TextFormat.FIRST_LINE_VISIBLE))
        targetLocation.y = Math.max(0, targetLocation.y);

    lineMeasurer.setPosition(0);
} 

The next stage calculates the initial position that the text will be rendered. This is applicable to any alignments at the 'middle' or 'bottom' of the bounds. In order to render the text in the correct position, the location must take account of the height of the bounds, and subtract according to the bounds the text will consume. For the locations, the following calculations can be used:

Middle Y = (Bounds.Height / 2) - (TextBounds.Height / 2) 
Bottom Y = (Bounds.Height - TextBounds.Height) 

If the TextFormat.FIRST_LINE_VISIBLE has been assigned as a format flag, the target Y location is updated to ensure that the text never falls below the total bounds. Therefore, if the target bounds have a height of 100, while the bounds of the text consume 120 bounds, and 'middle' or 'bottom' are the alignment factors, the position of the text would begin at Y -20. If the flag is enabled, the text will always begin at Y 0 when below the bounds.

if (align.isRight() || align.isCenter())
    targetLocation.x = bounds.x + bounds.width;
        Rectangle consumedBounds = new Rectangle(targetLocation.x, targetLocation.y, 0, 0); 

The initial consumed bounds are allocated, which involves recording the starting X and Y locations. If the alignment of the text is to the right or center, then the location is set to the right-most bound, so that the left-most location can be calculated later.

while (lineMeasurer.getPosition() < text.length())
{
    nextOffset = lineMeasurer.nextOffset(bounds.width);
    nextOffset = nextTextIndex(nextOffset, lineMeasurer.getPosition(), text);

    TextLayout textLayout = lineMeasurer.nextLayout(bounds.width, nextOffset, false);
    Rectangle2D textBounds = textLayout.getBounds();

    targetLocation.y += textLayout.getAscent();
    consumedBounds.width = Math.max(consumedBounds.width, (int)textBounds.getWidth());

Next, the method begins the rendering operation for the text. The same methodology as the pre-rendering calculation is used to determine the text being rendered. The width of the consumed bounds is updated if the width of the current line is wider than the previous.

switch (align)
{
    case TOP_LEFT:
    case MIDDLE_LEFT:
    case BOTTOM_LEFT:
        textLayout.draw(g2D, targetLocation.x, targetLocation.y);
        break;

    case TOP:
    case MIDDLE:
    case BOTTOM:
        targetLocation.x = bounds.x + (bounds.width / 2) - (int)(textBounds.getWidth() / 2);
        consumedBounds.x = Math.min(consumedBounds.x, targetLocation.x);
        textLayout.draw(g2D, targetLocation.x, targetLocation.y);
        break;

    case TOP_RIGHT:
    case MIDDLE_RIGHT:
    case BOTTOM_RIGHT:
        targetLocation.x = bounds.x + bounds.width - (int)textBounds.getWidth();
        textLayout.draw(g2D, targetLocation.x, targetLocation.y);
        consumedBounds.x = Math.min(consumedBounds.x, targetLocation.x);
        break;
} 

Depending on the alignment of the content, the position of the text is updated accordingly. The horizontal location is calculated using the width of the current line, such that center aligned text is rendered according to half the total bounds width minus half the text bounds width, while right aligned text is simply the total bounds minus the text bounds width.

Additionally, for center and right aligned text, the left-most position of the consumed bounds is checked, to ensure that the consumed bounds begins at the lowest possible left-most position. This is due to different lines consuming different widths depending on the total content of that line.

    targetLocation.y += textLayout.getLeading() + textLayout.getDescent();
} 

The vertical position of the next text block is updated, taking into account the leading height of the font, plus the descent.

    consumedBounds.height = targetLocation.y - consumedBounds.y;

    return consumedBounds;
}

Finally, the total height consumed by the text is calculated based on the final vertical position minus the starting vertical position. After which, the consumed bounds are returned from the method.

private static int nextTextIndex(int nextOffset, int measurerPosition, String text)
{
    for (int i = measurerPosition + 1; i < nextOffset; ++i)
    {
        if (text.charAt(i) == '\n')
            return i;
    }

    return nextOffset;
}

The purpose of the nextTextIndex method is to calculate the index, within the text line, where the text layout will render to. By default, if the text "hello world" fits within a line, then the lineMeasurer.nextOffset method will return 0 to 11. However, if the text "hello\nworld" is provided (where the \n character is a line-break), then the method would return 0 to 5, resulting in the word "hello" only being printed on a single line, and the word "world" being printed on the next.

Using the Code

Using the code is exceptionally simple. The class TextRenderer contains static methods for rendering text, which need a few standard parameters for rendering.

public void paint(Graphics g)
{
    Rectangle bounds = new Rectangle(0, 0, 100, 100);
    TextRenderer.drawString(
        g,
        "Hello world",
        getFont(),
        getForeground(),
        bounds,
        TextAlignment.TOP_LEFT
    );
}

The above method would render the text in the top-left corner of the graphics handle, with maximum bounds of width 100, height 100.

For a JPanel component, to render text in the center of the component always, while ensuring that the first line of the text is always visible, the following method would suffice:

public void paint(Graphics g)
{
    Rectangle bounds = new Rectangle(0, 0, getWidth(), getHeight());

    TextRenderer.drawString(
        g,
        "This is some long content which would be displayed.\n" +
        "This line here would be rendered on the next line.",
        getFont(),
        getForeground(),
        bounds,
        TextAlignment.MIDDLE,
        TextFormat.FIRST_LINE_VISIBLE
    );
}

The use of the code is fairly simple. Playing with the code will provide different results, so feel free to try different positioning.

Included in the archive attached are the three main classes responsible for the rendering. Additionally, there is a test class which was generated for testing the text renderer (see the primary image.)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Chris Copeland
Software Developer (Senior)
United Kingdom United Kingdom
I began developing at age 12, in 2002, when I learned to code basic HTML, CSS and JavaScript. I soon moved on to PHP web-development with MySQL to create interactive websites for personal projects and clients.

Later, in 2005, I explored developing in VB6 before advancing to VB.NET. After much resistance, I picked up C# development, and never turned back. Shortly afterward, I was introduced to C and C++ after working on an open-source game project. I contributed to this for several years, before hanging up my pointer->pants.

In 2008 I decided to join the growing community of Java developers, and began learning to code in this alternative model for cross-platform application development. Eventually, I would end up entering a job comprised of both Java and C# development.

Continue? Insert 20p..

You may also be interested in...

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.161208.2 | Last Updated 19 May 2014
Article Copyright 2014 by Chris Copeland
Everything else Copyright © CodeProject, 1999-2016
Layout: fixed | fluid