Sunday, February 13, 2011

How to output a String on multiple lines using Graphics

My Program overrides public void paint(Graphics g, int x, int y); in order to draw some stings using g.drawString(someString, x+10, y+30);

Now someString can be quite long and thus, it may not fit on one line.

What is the best way to write the text on multiple line.
For instance, in a rectangle (x1, y1, x2, y2)?

  • java.awt.font.TextLayout might be helpful. Here's a snippet of their example code:

       Graphics2D g = ...;
       Point2D loc = ...;
       Font font = Font.getFont("Helvetica-bold-italic");
       FontRenderContext frc = g.getFontRenderContext();
       TextLayout layout = new TextLayout("This is a string", font, frc);
       layout.draw(g, (float)loc.getX(), (float)loc.getY());
    
       Rectangle2D bounds = layout.getBounds();
       bounds.setRect(bounds.getX()+loc.getX(),
                      bounds.getY()+loc.getY(),
                      bounds.getWidth(),
                      bounds.getHeight());
       g.draw(bounds);
    

    Otherwise you could always use a Swing text element to do the job for you, just pass in the Graphics you want it to paint into.

    Burkhard : I got that one already. My problem is, I don't want a rectangle around the text, but the text inside a given rectangle.
    From Epaga
  • Incrementally build your string, one word at a time, using Epaga's method to find the length of your string. Once the length is longer than your rectangle, remove the last word and print. Repeat until you run out of words.

    It sounds like a bad algorithm, but for each line, it's really O(screenWidth/averageCharacterWidth) => O(1).

    Still, use a StringBuffer to build your string!

    jamesh : This is not going to have a time complexity of O(1); O(n), perhaps, as you need to (at least) add a word (i.e. add a number of chars in the StringBuilder.append) until all the chars have been used up.
    Ryan Fox : I seem to be bad at expressing myself properly... I meant that each line is O(1). You can throw 1000 words at it, but, for each line, it'll stop adding words at the edge of the screen. Overall, yes, O(n).
    From Ryan Fox
  • Thanks to Epaga's hint and a couple of examples on the Net (not so obvious to find! I used mainly Break a Line for text layout), I could make a component to display wrapped text. It is incomplete, but at least it shows the intended effect.

    class TextContainer extends JPanel
    {
        private int m_width;
        private int m_height;
        private String m_text;
        private AttributedCharacterIterator m_iterator;
        private int m_start;
        private int m_end;
    
        public TextContainer(String text, int width, int height)
        {
            m_text = text;
            m_width = width;
            m_height = height;
    
            AttributedString styledText = new AttributedString(text);
            m_iterator = styledText.getIterator();
            m_start = m_iterator.getBeginIndex();
            m_end = m_iterator.getEndIndex();
        }
    
        public String getText()
        {
            return m_text;
        }
    
        public Dimension getPreferredSize()
        {
            return new Dimension(m_width, m_height);
        }
    
        public void paint(Graphics g)
        {
            super.paintComponent(g);
    
            Graphics2D g2 = (Graphics2D) g;
            FontRenderContext frc = g2.getFontRenderContext();
    
            LineBreakMeasurer measurer = new LineBreakMeasurer(m_iterator, frc);
            measurer.setPosition(m_start);
    
            float x = 0, y = 0;
            while (measurer.getPosition() < m_end)
            {
                TextLayout layout = measurer.nextLayout(m_width);
    
                y += layout.getAscent();
                float dx = layout.isLeftToRight() ?
                        0 : m_width - layout.getAdvance();
    
                layout.draw(g2, x + dx, y);
                y += layout.getDescent() + layout.getLeading();
            }
        }
    }
    

    Just for fun, I made it fitting a circle (alas, no justification, it seems):

    public void paint(Graphics g)
    {
        super.paintComponent(g);
    
        Graphics2D g2 = (Graphics2D) g;
        FontRenderContext frc = g2.getFontRenderContext();
    
        LineBreakMeasurer measurer = new LineBreakMeasurer(m_iterator, frc);
        measurer.setPosition(m_start);
    
        float y = 0;
        while (measurer.getPosition() < m_end)
        {
            double ix = Math.sqrt((m_width / 2 - y) * y);
            float x = m_width / 2.0F - (float) ix;
            int width = (int) ix * 2;
    
            TextLayout layout = measurer.nextLayout(width);
    
            y += layout.getAscent();
            float dx = layout.isLeftToRight() ?
                    0 : width - layout.getAdvance();
    
            layout.draw(g2, x + dx, y);
            y += layout.getDescent() + layout.getLeading();
        }
    }
    

    I am not too sure about dx computation, though.

    From PhiLho
  • You can use a JLabel and embed the text with html.

    JLabel.setText("<html>"+line1+"<br>"+line2);
    
    Michael Myers : Your HTML is getting stripped. I'm assuming the first string literal is and the second one is
    .
    Ryan Fox : Doesn't it defeat the purpose if you have to add your own linebreaks?

0 comments:

Post a Comment