如何计算文本在JTextArea中占用的行数(以及每行中的列数)?

12

阅读此问题后,我尝试过几次但都失败了,我不喜欢这样 :)

我认为如果将问题分为子问题可能有助于解决它。

为简单起见,假设JTextArea的大小不会改变,因此我们不需要担心重新评估等问题。我认为重要的问题是:

1.如何计算某个文本在JTextArea中占据多少行?

2.JTextArea中列数与其可以容纳的字符数的关系是什么?以便我们可以计算行长。

请查看下面包含要处理的文本区域的示例代码:

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;

public class TextAreaLines
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                JPanel p = new JPanel();
                JFrame f = new JFrame();
                JTextArea ta = new JTextArea("dadsad sasdasdasdasdasd");
                ta.setWrapStyleWord(true);
                ta.setLineWrap(true);
                ta.setRows(5);
                ta.setColumns(5);
                p.add(ta);
                f.setContentPane(p);
                f.setSize(400, 300);
                f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                f.setVisible(true);             
                //BTW the code below prints 1
                System.out.println("ta.getLineCount()="+ta.getLineCount());
            }
        });
    }
}

编辑1:所以我已经想出了以下代码,但问题是输出结果与您看到的不同,即

//for input
//JTextArea ta = new JTextArea("alfred abcdefghijklmnoprstuwvxyz abcdefg");

//we have output    
//s=alfred abcdefghijk
//s=lmnoprstuwvxyz a
//s=bcdefg



        FontMetrics fm = ta.getFontMetrics(ta.getFont());
        String text = ta.getText();
        List<String> texts = new ArrayList<String>();               
        String line = "";
        //no word wrap
        for(int i = 0;i < text.length(); i++)  
        {       
            char c = text.charAt(i);
            if(fm.stringWidth(line +c)  <= ta.getPreferredSize().width)
            {                       
                //System.out.println("in; line+c ="+(line + c));
                line += c;
            }
            else
            {
                texts.add(line);//store the text
                line = ""+c;//empty the line, add the last char
            }
        }
        texts.add(line);                
        for(String s: texts)
            System.out.println("s="+s);

我做错了什么,我忘记了什么?文本区域没有换行。

EDIT2: @trashgod 这是我得到的输出。显然,我们有不同的默认字体。问题实际上可能是字体或甚至于系统相关。(PS:我在Win7上)。

line: Twas brillig and the slithy tovesD
line: id gyre and gimble in the wabe;
line count: 2
preferred: java.awt.Dimension[width=179,height=48]
bounds1: java.awt.geom.Rectangle2D$Float[x=0.0,y=-12.064453,w=170.0,h=15.09375]
layout1: java.awt.geom.Rectangle2D$Float[x=0.28125,y=-8.59375,w=168.25,h=11.125]
bounds2: java.awt.geom.Rectangle2D$Float[x=0.0,y=-12.064453,w=179.0,h=15.09375]
layout2: java.awt.geom.Rectangle2D$Float[x=0.921875,y=-8.59375,w=177.34375,h=11.125]

在我脑海中总结了你们所有人所说的后,我认为可能可靠的解决方案是入侵文本区域设置其文本的方式,并完全控制它。通过在区域的setText中运行算法(上面那个,请注意,如@trashgod建议'<'被更改为'< ='),可以实现这一点。

让我想到这样做的原因是…例如,在我提供的示例中,如果您将textarea的文本更改为JTextArea ta = new JTextArea("alfred abcdefghijkl\nmnoprstuwvxyz ab\ncdefg"); 与我计算的情况相同,那么它将完美地适合于textarea中。

编辑3:这是我快速破解的一种解决方案,至少现在显示的字符和计算出来的字符完全相同。请其他人测试一下,并告诉我,在Win7之外的其他机器上可能如何工作?下面的示例已准备好使用,你应该能够调整窗口大小并得到与打印线相同的输出。

import java.awt.BorderLayout;
import java.awt.FontMetrics;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;

public class TextAreaLines
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                JPanel p = new JPanel(new BorderLayout());
                JFrame f = new JFrame();
                final JTextArea ta = new JTextArea("alfred abcdefghijklmnoprstuwvxyz abcdefg");
                ta.addComponentListener(new ComponentAdapter()
                {
                    @Override
                    public void componentResized(ComponentEvent e)
                    {
                        super.componentResized(e);
                        System.out.println("ta componentResized");
                        reformatTextAreaText(ta);
                    }
                });
                //ta.setWrapStyleWord(true);
                ta.setLineWrap(true);
                p.add(ta);
                f.setContentPane(p);
                f.setSize(200, 100);
                f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                f.setVisible(true);
            }

            private void reformatTextAreaText(JTextArea ta)
            {
                String text = ta.getText();
                //remove all new line characters since we want to control line braking
                text = text.replaceAll("\n", "");
                FontMetrics fm = ta.getFontMetrics(ta.getFont());
                List<String> texts = new ArrayList<String>();
                String line = "";
                //no word wrap
                for(int i = 0; i < text.length(); i++)
                {
                    char c = text.charAt(i);
                    if(fm.stringWidth(line + c) <= ta.getPreferredSize().width)
                    {
                        //System.out.println("in; line+c ="+(line + c));
                        line += c;
                    }
                    else
                    {
                        texts.add(line);//store the text
                        line = "" + c;//empty the line, add the last char
                    }
                }
                texts.add(line);
                //print out of the lines
                for(String s : texts)
                    System.out.println("s=" + s);
                //build newText for the
                String newText = "";
                for(String s : texts)
                    newText += s + "\n";
                ta.setText(newText);
            }
        });
    }
}

提前致谢。


我用C#做了这个[或类似的东西]...费了些功夫。我会看看能否找到代码。 - Doug Chamberlain
@Doug Chamberlain,非常感谢您提供的所有输入。希望您能找到它,并且在Java实现中遇到的问题也会类似 :) - Boro
@camickr @jjnguy @Aleadam @Doug Chamberlain 请查看我的编辑,期待您的建议。 - Boro
感谢所有的参与者。所有的回答都非常有帮助,虽然我只能接受一个。我选择了@trashgod的回答,因为它对我来说是最有用的,它向我提供了通往我想出的“hack”解决方案的途径。再次感谢。 - Boro
7个回答

10

没有什么问题。我修改了你的示例,使用"<="作为宽度,并突出了一些特点:

  1. FontMetrics注意到,“一个String的前进不一定是其单独测量字符的前进之和…”

  2. 文本组件的首选大小与最宽行的度量范围相当吻合。由于比例间距的原因,这因字体而异。

  3. TextLayout显示更紧密的边界,但请注意“基线相对坐标”。

  4. getLineCount()方法计算line.separator分隔的行数,而不是换行包装的行数。

行: Twas brillig and the slithy toves
行: Did gyre and gimble in the wabe;
行数: 2
首选: java.awt.Dimension[width=207,height=48]
bounds1: java.awt.geom.Rectangle2D$Float[x=0.0,y=-12.568359,w=205.0,h=15.310547]
布局1: java.awt.geom.Rectangle2D$Float[x=0.0,y=-10.0,w=200.0,h=13.0]
bounds2: java.awt.geom.Rectangle2D$Float[x=0.0,y=-12.568359,w=207.0,h=15.310547]
布局2: java.awt.geom.Rectangle2D$Float[x=1.0,y=-10.0,w=205.0,h=13.0]
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;

/** #see https://dev59.com/zeo6XIcBkEYKwwoYTSou */
public class TextAreaLine {

    private static final String text1 =
        "Twas brillig and the slithy toves\n";
    private static final String text2 =
        "Did gyre and gimble in the wabe;";
    private static final JTextArea ta = new JTextArea(text1 + text2);

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                display();
            }
        });
    }

    static void display() {
        JFrame f = new JFrame();
        ta.setWrapStyleWord(false);
        ta.setLineWrap(false);
        ta.setRows(3);
        f.add(ta);
        f.pack();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
        FontMetrics fm = ta.getFontMetrics(ta.getFont());
        List<String> texts = new ArrayList<String>();
        Dimension d = ta.getPreferredSize();
        String text = ta.getText();
        String line = "";
        for (int i = 0; i < text.length(); i++) {
            char c = text.charAt(i);
            if (c != '\n') {
                if (fm.stringWidth(line + c) <= d.width) {
                    line += c;
                } else {
                    texts.add(line);
                    line = "" + c;
                }
            }
        }
        texts.add(line);
        for (String s : texts) {
            System.out.println("line: " + s);
        }
        System.out.println("line count: " + ta.getLineCount());
        System.out.println("preferred: " + d);
        System.out.println("bounds1: " + fm.getStringBounds(text1, null));
        FontRenderContext frc = new FontRenderContext(null, false, false);
        TextLayout layout = new TextLayout(text1, ta.getFont(), frc);
        System.out.println("layout1: " + layout.getBounds());
        System.out.println("bounds2: " + fm.getStringBounds(text2, null));
        layout = new TextLayout(text2, ta.getFont(), frc);
        System.out.println("layout2: " + layout.getBounds());
    }
}

1
感谢您提供的示例和您的时间(+1)。在进行了一些初步测试后,这对于确定控制区域文本设置可能是唯一的解决方法非常有帮助。:) 请检查我的EDIT2。 - Boro
1
编辑2:与我的一样,您的文本区域的首选宽度与最宽行的宽度相匹配。编辑3:我在Mac OS X上看到匹配的行。 - trashgod

5

你可以使用FontMetrics来实现。我编写了一些代码,用于在特定行数处拆分JTextArea。设置代码如下:

Graphics2D g = (Graphics2D) g2;
FontMetrics m = g.getFontMetrics();
int lineHeight = m.getHeight();

这将告诉您文本行有多高。
不幸的是,大多数字体中字母具有不同的宽度。但是,您可以使用以下代码确定字符串的宽度。
int width = m.getStringBounds("Some String", g).getWidth();

我知道这并不能完全回答你的问题,但我希望它能有所帮助。
如果您没有使用自动换行功能,那么可以使用以下一般算法(在绘制组件方法中):
String text[] = getText().split("\n");
String newText = "";
for (String line: text) {
    newText = line + "| " + line.length() + "\n";
}
setText(newText);

这是一个大致的想法,不确定它的效果如何。如果你尝试了,请告诉我。


@jjnguy 感谢您的意见。是的,我正在考虑使用FontMetrics。不过我不确定字符串宽度与JTextArea具有的列数之间的关系。 - Boro
@Boro,如果wordwrapfalse,这将简单得多。开启自动换行的原因是什么? - jjnguy
@jjnguy 不是很确定。我只是认为一个包括自动换行的解决方案可能更通用一些。但是如果你有一个不需要自动换行的解决方案,那就继续吧,我想看看它。 - Boro
@Boro,实际上,我认为那可能会导致无限递归...糟糕。 - jjnguy
@jjnguy 谢谢,我喜欢它。看起来它能够工作,至少给了一个起点,也就是这样我有了正确的行号。很棒 :) 感谢你的帮助和代码。 - Boro

5

不确定这是否有帮助,但您需要设置文本区域的宽度,以便视图知道何时换行。一旦您设置了大小,就可以确定首选高度。当您知道首选高度时,可以使用字体度量行高来确定总行数,包括任何换行。

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

public class TextAreaPreferredHeight extends JFrame
{

    public TextAreaPreferredHeight()
    {
        JTextArea textArea = new JTextArea();
        textArea.setText("one two three four five six seven eight nine ten");
        textArea.setLineWrap( true );
        textArea.setWrapStyleWord( true );

        FontMetrics fm = textArea.getFontMetrics( textArea.getFont() );
        int height = fm.getHeight();

        System.out.println("000: " + textArea.getPreferredSize());
        textArea.setSize(100, 1);
        System.out.println("100: " + textArea.getPreferredSize());
        System.out.println("lines : " + textArea.getPreferredSize().height / height);

        textArea.setSize(200, 1);
        System.out.println("200: " + textArea.getPreferredSize());
        System.out.println("lines : " + textArea.getPreferredSize().height / height);

        textArea.setSize(300, 1);
        System.out.println("300: " + textArea.getPreferredSize());
        System.out.println("lines : " + textArea.getPreferredSize().height / height);
        add(textArea);
        pack();
        setVisible(true);
}

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

有趣的例子,点个赞。让我想想现在怎么使用它。 - Boro

3
我见过人们使用 TextLayout 来实现类似这样的功能。

经过简短的了解,这看起来很有前途。谢谢你提供这个。你有没有特别的例子想到? - Boro
@Boro:这里有一个链接 https://dev59.com/GUzSa4cB1Zd3GeqPiwSw。 - Catalina Island
我喜欢@camickr的答案,但我会提供第二个示例TextLayout - trashgod
感谢您的时间 (+1)。我将首先尝试使用FontMetric,然后尝试TextLayout,但我猜测在每种情况下都会使用类似的算法。如果我错了,请纠正我。 - Boro
@Boro:我不确定,但我认为TextLayout使用了FontMetrics。 - Catalina Island

2
昨天有一个类似的问题关于安卓。我认为需要采用迭代的方法来解决:
  • 获取JTextArea的宽度
  • 使用FontMetrics获取字符串的宽度,正如jjnguy所建议的那样
  • 将你的字符串拆分成单词。
  • 从添加一个单词开始逐个测量字符串的宽度,直到达到区域宽度。保存该数字。
  • 一旦达到目标,开始新的迭代,每次添加一个单词(从保存的数字开始)。
  • 行数将是迭代的次数。

不幸的是,行长将取决于字体和特定字符串(并非每个字符都具有相同的宽度)。您可以在每次迭代中计算单词中的字符数,并返回长度数组或长度平均值。

这是相关的安卓问题:如何找到安卓TextView每行的字符数?


很好的建议。我也在考虑这种方法。让我试试看。(+1) - Boro

2
我发现仅通过文本分析计算行数没有结果。相反,我的解决方案是从文本区域的首选大小计算行数...
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;

public class LineNumbering extends JFrame {

    private static final long serialVersionUID = 1L;
    private static final Font fixedFont = new Font("Monospaced", Font.PLAIN, 12);
    private static JTextArea jta;
    private static JTextArea lines;
    private static String lineSeparator = "\n";
    private static int numRows = 10;
    private static int numCols = 30;

    public LineNumbering() {
        super("Line Numbering Example");
    }

    public static void createAndShowGUI() {
        JFrame frame = new LineNumbering();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JScrollPane jsp = new JScrollPane();
        jta = new JTextArea(numRows, numCols);
        jta.setFont(fixedFont);
        jta.setLineWrap(true);
        jta.setWrapStyleWord(true);

        lines = new JTextArea(numRows, 3);
        lines.setEditable(false);
        lines.setFocusable(false);
        lines.setEnabled(false);
        lines.setFont(fixedFont);
        lines.setBackground(Color.LIGHT_GRAY);
        lines.setDisabledTextColor(Color.BLACK);
        // do initial line numbering
        for (int i = 1; i <= lines.getRows(); i++) {
            lines.append(i + System.getProperty("line.separator"));
        }

        final class DebugCaretListener implements CaretListener {

            int rowHeight = jta.getFontMetrics(jta.getFont()).getHeight();

            /**
             * @return total of lines showed in the text area
             */
            private int getTotalLinesInView() {
                int insetsTotalHeight = jta.getInsets().top + jta.getInsets().bottom;
                return (jta.getPreferredSize().height - insetsTotalHeight) / rowHeight;
            }

            /**
             * @return text with line numbers
             */
            public String getText() {
                StringBuffer text = new StringBuffer();
                int totalLines = getTotalLinesInView();
                System.out.println("totalLines : " + totalLines);
                for (int i = 1; i <= totalLines; i++) {
                    text.append(i);
                    if (i < totalLines) {
                        text.append(lineSeparator);
                    }
                }
                return text.toString();
            }

            /**
             * <p>
             * Reset line numbers on caret event. Since the total number of
             * lines is calculated from preferred size of text area, we do this
             * on an event that occurred after repainting of the text area.
             * </p>
             * (non-Javadoc)
             * 
             * @see javax.swing.event.CaretListener#caretUpdate(javax.swing.event.CaretEvent)
             */
            @Override
            public void caretUpdate(CaretEvent e) {
                int totalLines = getTotalLinesInView();
                System.out.println("totalLines : " + totalLines);
                if (totalLines >= numRows) {
                    lines.setText(getText());
                }
            }

        }

        jta.addCaretListener(new DebugCaretListener());
        jsp.getViewport().add(jta);
        jsp.setRowHeaderView(lines);
        jsp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER);
        jsp.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);

        JPanel textPanel = new JPanel();
        textPanel.add(jsp);
        JPanel contentPanel = new JPanel();
        contentPanel.add(textPanel);
        frame.setContentPane(contentPanel);
        contentPanel.setOpaque(true);
        frame.pack();
        frame.setPreferredSize(new Dimension(500, 500));
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        javax.swing.SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                createAndShowGUI();
            }
        });
    }
}

2

好的,我写了一个程序,可以加载图片并将其转换为ASCII艺术品。 我希望它能够根据输入的图像自动使文本区域具有正确的纵横比。

然而,我始终无法使其完全正常运行。 我放弃了,并注释掉了我的尝试,这是我尝试过的代码片段。 我最终做的是只使用一个textarea,有时候不能完全填满。

                //Graphics fontG = this.textBox4.CreateGraphics();
                //fontG.PageUnit = GraphicsUnit.Point;
                //SizeF fontSize = fontG.MeasureString(RowData, this.textBox4.Font,(SizeF) this.textBox4.ClientSize);
                sb.AppendLine();
                RowData = "";
                //fontH +=  fontSize.Height + 1.2F;
                //fontW = (int) fontSize.Width;

谢谢@Doug Chamberlain,你的代码看起来就像jjnguy建议的方法。 - Boro

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接