在 LineBreakMeasurer 中处理 \n

发布于 2024-07-27 00:53:51 字数 146 浏览 9 评论 0原文

有数千篇文章如何使用 LineBreakMeasurer 绘制多行文本,但没有一篇关于绘制多行文本并考虑到 \n(当您想在文本中的特定位置强制换行时,而不仅仅是当右边距或左边距结束)。

秘密似乎在于 BreakIterator,但我找不到处理 \n 的实现。

There are thousand articles how to use LineBreakMeasurer to draw multi-line text but there is none about drawing multi-line text taking into account also \n(when you want to force a new line at a specific position in text and not only when the right - or left - margin ends).

The secret seems to lie in BreakIterator, but I couldn't find an implementation which handles \n.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

若有似无的小暗淡 2024-08-03 00:53:51

使用重载的 LBM.nextLayout(float, int) 代替 LineBreakMeasurer 的 (LBM) nextLayout(float) 方法, 布尔值) 方法。 这允许您限制 LBM 将包含在返回的 TextLayout 中的文本。 就您而言,您将指示它不要超出下一个换行符。

这段代码应该能让你明白。 首先使用 LBM.nextOffset 来“查看”哪个字符索引将是下一个布局的末尾。 然后迭代字符串内容直到该偏移量,看看是否找到任何换行符。 这将告诉 LBM 不要超过换行符

int next = lineMeasurer.nextOffset(formatWidth);
int limit = next;
if (limit < totalLength) {
   for (int i = lineMeasurer.getPosition(); i < next; ++i) {
      char c = string.charAt(i);
      if (c == '\n') {
         limit = i;
         break;
      }
   }
}

TextLayout layout = lineMeasurer.nextLayout(formatWidth, limit, false);

如果这样做,则使用找到的限制作为 nextLayout(float, int, boolean) 的第二个参数,

: ="http://java.sun.com/developer/onlineTraining/Media/2DText/style.html#layout" rel="nofollow">http://java.sun.com/developer/onlineTraining/Media/2DText/ style.html#layout
http://java.sun.com/developer/onlineTraining/Media/2DText/Code/LineBreakSample.java

Instead of LineBreakMeasurer's (LBM's) nextLayout(float) method, use the overloaded LBM.nextLayout(float, int, boolean) method. This allows you to limit the text that LBM will include in the returned TextLayout. In your case, you'll instruct it not to go beyond the next newline.

This code snippet should give you the idea. First use LBM.nextOffset to "peek" which character index would be the end of the next layout. Then iterate over your string content up to that offset to see if you find any newline characters. If you do, then use that found limit as the second argument to nextLayout(float, int, boolean) which will tell LBM not to exceed the newline:

int next = lineMeasurer.nextOffset(formatWidth);
int limit = next;
if (limit < totalLength) {
   for (int i = lineMeasurer.getPosition(); i < next; ++i) {
      char c = string.charAt(i);
      if (c == '\n') {
         limit = i;
         break;
      }
   }
}

TextLayout layout = lineMeasurer.nextLayout(formatWidth, limit, false);

References

http://java.sun.com/developer/onlineTraining/Media/2DText/style.html#layout
http://java.sun.com/developer/onlineTraining/Media/2DText/Code/LineBreakSample.java

ヅ她的身影、若隐若现 2024-08-03 00:53:51

我发现这段代码对于换行问题很有效。 我使用 atdixon 作为模板来得到这个。

while (measurer.getPosition() < paragraph.getEndIndex()) {
   next = measurer.nextOffset(wrappingWidth);
   limit = next;
   charat = tested.indexOf('\n',measurer.getPosition()+1);
   if(next > (charat - measurer.getPosition()) && charat != -1){
      limit = charat - measurer.getPosition();
   }
   layout = measurer.nextLayout(wrappingWidth, measurer.getPosition()+limit, false);
   // Do the rest of your layout and pen work.
}

I find that this code works well for the newline issue. I used atdixon as a template to get this.

while (measurer.getPosition() < paragraph.getEndIndex()) {
   next = measurer.nextOffset(wrappingWidth);
   limit = next;
   charat = tested.indexOf('\n',measurer.getPosition()+1);
   if(next > (charat - measurer.getPosition()) && charat != -1){
      limit = charat - measurer.getPosition();
   }
   layout = measurer.nextLayout(wrappingWidth, measurer.getPosition()+limit, false);
   // Do the rest of your layout and pen work.
}
娇柔作态 2024-08-03 00:53:51

首先对文本进行标记,然后将 LineBreakMeasureCode 应用于每个标记。

Tokenize the text first, then just apply the LineBreakMeasureCode to each token.

小帐篷 2024-08-03 00:53:51

Aaron 的代码并不总是正确工作,因此这里有一些对我有用的调整后的代码:

int next = measurer.nextOffset(width);
int limit = next;
if (limit <= text.length()) {
  for (int i = measurer.getPosition(); i < next; ++i) {
    char c = text.charAt(i);
    if (c == '\n') {
      limit = i + 1;
      break;
    }
  }
}
TextLayout textLayout = measurer.nextLayout(width, limit, false);

如果您需要来自 AttributedString 的文本,您可以预先执行此操作

AttributedCharacterIterator iterator = attributedString.getIterator();
StringBuilder stringBuilder = new StringBuilder(iterator.getEndIndex());
while (iterator.getIndex() < iterator.getEndIndex()) {
  stringBuilder.append(iterator.current());
  iterator.next();
}
String text = stringBuilder.toString();

Aaron's code doesn't always work right so here's some tweaked code that is working for me:

int next = measurer.nextOffset(width);
int limit = next;
if (limit <= text.length()) {
  for (int i = measurer.getPosition(); i < next; ++i) {
    char c = text.charAt(i);
    if (c == '\n') {
      limit = i + 1;
      break;
    }
  }
}
TextLayout textLayout = measurer.nextLayout(width, limit, false);

If you need text from an AttributedString you can just do this beforehand

AttributedCharacterIterator iterator = attributedString.getIterator();
StringBuilder stringBuilder = new StringBuilder(iterator.getEndIndex());
while (iterator.getIndex() < iterator.getEndIndex()) {
  stringBuilder.append(iterator.current());
  iterator.next();
}
String text = stringBuilder.toString();
雪花飘飘的天空 2024-08-03 00:53:51

尽管这个话题很老了,但我自己也遇到过这个问题并且必须解决它。 经过大量的调查后,我提出了可以在包装“JTextArea”的单个类中工作的解决方案。

代码是 Kotlin 语言,因为我正在使用它。 希望它仍然有用。

package [your package name]

import java.awt.Font
import java.awt.FontMetrics
import java.awt.Insets
import java.awt.font.LineBreakMeasurer
import java.awt.font.TextAttribute
import java.text.AttributedString
import java.text.BreakIterator
import javax.swing.JTextArea

class TextAreaLineCounter(
    private val textArea: JTextArea
) {

    private val font: Font
        get() = textArea.font
    private val fontMetrics: FontMetrics
        get() = textArea.getFontMetrics(font)
    private val insets: Insets
        get() = textArea.insets
    private val formatWidth: Float
        get() = (textArea.width - insets.left - insets.right).toFloat()

    fun countLines(): Int {
        return countLinesInParagraphs(
            textRaw = textArea.text,
            font = font,
            fontMetrics = fontMetrics,
            formatWidth = formatWidth
        )
    }

    private fun countLinesInParagraphs(
        textRaw: String,
        font: Font,
        fontMetrics: FontMetrics,
        formatWidth: Float
    ): Int {
        val paragraphs: List<String> = textRaw.split("\n")
        val lineCount = paragraphs.fold(0) { acc: Int, sentence: String ->
            val newCount = acc + countLinesInSentence(sentence, font, fontMetrics, formatWidth)
            newCount
        }
        return lineCount
    }

    private fun countLinesInSentence(
        textRaw: String,
        font: Font,
        fontMetrics: FontMetrics,
        formatWidth: Float
    ): Int {
        val text = AttributedString(textRaw)
        text.addAttribute(TextAttribute.FONT, font)
        val frc = fontMetrics.fontRenderContext
        val charIt = text.iterator
        val lineMeasurer = LineBreakMeasurer(
            charIt,
            BreakIterator.getWordInstance(),
            frc
        )
        lineMeasurer.position = charIt.beginIndex
        var noLines = 0
        while (lineMeasurer.position < charIt.endIndex) {
            lineMeasurer.nextLayout(formatWidth)
            noLines++
        }
        return noLines
    }
}

另外,一个可以让您测试行计数器的 GUI 应用程序也可能很有用。

package [your package name]

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.awt.*
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.JFrame
import javax.swing.JPanel
import javax.swing.JTextArea
import javax.swing.SwingUtilities

class MainJTextArea(
    private val l: Logger
): JPanel(GridBagLayout()) {

    init {
        val inputStr = "Lorem ipsum dolor sit amet, consectetur adipisicing\n elit, sed do eiusmo," +
                " Lorem ipsum \ndolor sit amet, consectetur adipisicing elit, sed do eiusmo," +
                " Lorem ipsum dolor sit amet, \nconsectetur adipisicing elit, sed do eiusmo," +
                " Lorem ipsum dolor sit amet, \nconsectetur adipisicing elit, sed do eiusmo"

        val textArea = drawTextArea(
            text = inputStr,
            fontSize = 12.0
        )
        val textAreaLineCounter = TextAreaLineCounter(textArea)

        // Add Components to this panel.
        val c = GridBagConstraints().apply {
            gridwidth = GridBagConstraints.REMAINDER
            fill = GridBagConstraints.BOTH
            weightx = 1.0
            weighty = 1.0
        }
        add(textArea, c)
        addComponentListener(object : ComponentAdapter() {
            override fun componentResized(e: ComponentEvent?) {
                super.componentResized(e)
                l.debug("Line count: ${textAreaLineCounter.countLines()}")
            }
        })
    }

    private fun drawTextArea(
        text: String,
        fontSize: Double = 12.0
    ): JTextArea {
        val textArea = JTextArea(text)
        textArea.size = Dimension(width, height)
        textArea.foreground = Color.BLACK
        textArea.background = Color(0, 0, 0, 0)
        textArea.font = Font(null, Font.LAYOUT_LEFT_TO_RIGHT, fontSize.toInt())
        textArea.lineWrap = true
        textArea.wrapStyleWord = true
        return textArea
    }

    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            val logger = LoggerFactory.getLogger(MainJTextArea::class.java)!!
            SwingUtilities.invokeLater {
                val frame = JFrame("JTextAreaLineCountDemo").apply {
                    preferredSize = Dimension(400, 360)
                    defaultCloseOperation = JFrame.EXIT_ON_CLOSE
                    add(MainJTextArea(logger))
                    pack()
                }
                frame.isVisible = true
            }
        }
    }
}

更新

经过进一步调查,我注意到计算器仍然有问题,需要一些定制。 因此,我改进了计算机制,以提供内部组成的文本中断的详细信息。

这种机制大部分时间都有效。 我注意到有几种情况,其中 JTextArea 会用空行换行,但未检测到。 因此,使用该代码的风险由您自行承担。

/**
 * Parses text to fit in [TextProvider.formatWidth] and wraps whenever needed
 */
class TextAreaLineCounter(
    private val textProvider: TextProvider
) {

    private val formatWidth: Float
        get() = textProvider.formatWidth

    fun parseParagraph(
        font: Font,
        fontMetrics: FontMetrics
    ): WrappedParagraph {
        return countLinesInParagraphs(
            textRaw = textProvider.text,
            font = font,
            fontMetrics = fontMetrics,
            formatWidth = formatWidth
        )
    }

    /**
     * Counts lines in [JTextArea]
     * Includes line breaks ('\n')
     */
    private fun countLinesInParagraphs(
        textRaw: String,
        font: Font,
        fontMetrics: FontMetrics,
        formatWidth: Float
    ): WrappedParagraph {
        val paragraphsAsString: List<String> = textRaw.split("\n")
        val sentences = paragraphsAsString.map { paragraph ->
            countLinesInSentence(paragraph, font, fontMetrics, formatWidth)
        }
        return WrappedParagraph(sentences = sentences)
    }

    /**
     * Counts lines in wrapped [JTextArea]
     * Does not include line breaks.
     */
    private fun countLinesInSentence(
        textRaw: String,
        font: Font,
        fontMetrics: FontMetrics,
        formatWidth: Float
    ): Sentence {
        if (textRaw.isEmpty()) {
            return Sentence(
                wraps = listOf(
                    SentenceWrap(
                        wrapIndex = -1,
                        wrapText = textRaw
                    )
                )
            )
        }
        val text = AttributedString(textRaw)
        text.addAttribute(TextAttribute.FONT, font)
        val frc = fontMetrics.fontRenderContext
        val charIt = text.iterator
        val words = mutableListOf<SentenceWrap>()
        val lineMeasurer = LineBreakMeasurer(
            charIt,
            BreakIterator.getLineInstance(),
            frc
        )
        lineMeasurer.position = charIt.beginIndex
        var posBegin = 0
        var posEnd = lineMeasurer.position
        var noLines = 0
        do {
            lineMeasurer.nextLayout(formatWidth)
            posBegin = posEnd
            posEnd = lineMeasurer.position
            words.add(
                SentenceWrap(
                    wrapIndex = noLines,
                    wrapText = textRaw.substring(posBegin, posEnd)
                )
            )
            noLines++
        } while (posEnd < charIt.endIndex)
        return Sentence(words)
    }

    /**
     * Holds wrapped [Sentence]s that break during 'wrap text' or text break symbols
     */
    data class WrappedParagraph(
        val sentences: List<Sentence>
    ) {
        fun lineCount(): Int {
            val sentenceCount = sentences.fold(0) { currentCount: Int, sentence: Sentence ->
                val newCount = currentCount + sentence.lineCount()
                newCount
            }
            return sentenceCount
        }
    }

    /**
     * Sentence contains text pieces which are broken by 'wrapText'
     */
    data class Sentence(
        val wraps: List<SentenceWrap>
    ) {
        fun lineCount(): Int = wraps.size
    }

    /**
     * Entity for holding a wrapped text snippet
     */
    data class SentenceWrap(
        val wrapIndex: Int,
        val wrapText: String
    )

    interface TextProvider {
        val text: String
        val formatWidth: Float
    }

    companion object {
        val l = LoggerFactory.getLogger(TextAreaLineCounter::class.java)!!
    }
}

Even though the topic is very old, I’ve had this issue myself and had to solve it. After considerable amount of investigation, I’ve come up with solution that would work in single class that wraps ’JTextArea’.

The code is in Kotlin, as that is what I’m using. Hopefully it’ll still be useful.

package [your package name]

import java.awt.Font
import java.awt.FontMetrics
import java.awt.Insets
import java.awt.font.LineBreakMeasurer
import java.awt.font.TextAttribute
import java.text.AttributedString
import java.text.BreakIterator
import javax.swing.JTextArea

class TextAreaLineCounter(
    private val textArea: JTextArea
) {

    private val font: Font
        get() = textArea.font
    private val fontMetrics: FontMetrics
        get() = textArea.getFontMetrics(font)
    private val insets: Insets
        get() = textArea.insets
    private val formatWidth: Float
        get() = (textArea.width - insets.left - insets.right).toFloat()

    fun countLines(): Int {
        return countLinesInParagraphs(
            textRaw = textArea.text,
            font = font,
            fontMetrics = fontMetrics,
            formatWidth = formatWidth
        )
    }

    private fun countLinesInParagraphs(
        textRaw: String,
        font: Font,
        fontMetrics: FontMetrics,
        formatWidth: Float
    ): Int {
        val paragraphs: List<String> = textRaw.split("\n")
        val lineCount = paragraphs.fold(0) { acc: Int, sentence: String ->
            val newCount = acc + countLinesInSentence(sentence, font, fontMetrics, formatWidth)
            newCount
        }
        return lineCount
    }

    private fun countLinesInSentence(
        textRaw: String,
        font: Font,
        fontMetrics: FontMetrics,
        formatWidth: Float
    ): Int {
        val text = AttributedString(textRaw)
        text.addAttribute(TextAttribute.FONT, font)
        val frc = fontMetrics.fontRenderContext
        val charIt = text.iterator
        val lineMeasurer = LineBreakMeasurer(
            charIt,
            BreakIterator.getWordInstance(),
            frc
        )
        lineMeasurer.position = charIt.beginIndex
        var noLines = 0
        while (lineMeasurer.position < charIt.endIndex) {
            lineMeasurer.nextLayout(formatWidth)
            noLines++
        }
        return noLines
    }
}

Also, may be useful as well, a GUI application that lets you test out the line counter.

package [your package name]

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.awt.*
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.JFrame
import javax.swing.JPanel
import javax.swing.JTextArea
import javax.swing.SwingUtilities

class MainJTextArea(
    private val l: Logger
): JPanel(GridBagLayout()) {

    init {
        val inputStr = "Lorem ipsum dolor sit amet, consectetur adipisicing\n elit, sed do eiusmo," +
                " Lorem ipsum \ndolor sit amet, consectetur adipisicing elit, sed do eiusmo," +
                " Lorem ipsum dolor sit amet, \nconsectetur adipisicing elit, sed do eiusmo," +
                " Lorem ipsum dolor sit amet, \nconsectetur adipisicing elit, sed do eiusmo"

        val textArea = drawTextArea(
            text = inputStr,
            fontSize = 12.0
        )
        val textAreaLineCounter = TextAreaLineCounter(textArea)

        // Add Components to this panel.
        val c = GridBagConstraints().apply {
            gridwidth = GridBagConstraints.REMAINDER
            fill = GridBagConstraints.BOTH
            weightx = 1.0
            weighty = 1.0
        }
        add(textArea, c)
        addComponentListener(object : ComponentAdapter() {
            override fun componentResized(e: ComponentEvent?) {
                super.componentResized(e)
                l.debug("Line count: ${textAreaLineCounter.countLines()}")
            }
        })
    }

    private fun drawTextArea(
        text: String,
        fontSize: Double = 12.0
    ): JTextArea {
        val textArea = JTextArea(text)
        textArea.size = Dimension(width, height)
        textArea.foreground = Color.BLACK
        textArea.background = Color(0, 0, 0, 0)
        textArea.font = Font(null, Font.LAYOUT_LEFT_TO_RIGHT, fontSize.toInt())
        textArea.lineWrap = true
        textArea.wrapStyleWord = true
        return textArea
    }

    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            val logger = LoggerFactory.getLogger(MainJTextArea::class.java)!!
            SwingUtilities.invokeLater {
                val frame = JFrame("JTextAreaLineCountDemo").apply {
                    preferredSize = Dimension(400, 360)
                    defaultCloseOperation = JFrame.EXIT_ON_CLOSE
                    add(MainJTextArea(logger))
                    pack()
                }
                frame.isVisible = true
            }
        }
    }
}

Update

After further invetigation, I’ve noticed calculator was still having problems and needed a bit of customization. So I’ve improved calculation mechanism to provide details with text breaks composed inside.

This mechanism works most of the time. I’ve noticed couple of cases, where JTextArea would wrap with empty line, which was not detected. So use the code at your own risk.

/**
 * Parses text to fit in [TextProvider.formatWidth] and wraps whenever needed
 */
class TextAreaLineCounter(
    private val textProvider: TextProvider
) {

    private val formatWidth: Float
        get() = textProvider.formatWidth

    fun parseParagraph(
        font: Font,
        fontMetrics: FontMetrics
    ): WrappedParagraph {
        return countLinesInParagraphs(
            textRaw = textProvider.text,
            font = font,
            fontMetrics = fontMetrics,
            formatWidth = formatWidth
        )
    }

    /**
     * Counts lines in [JTextArea]
     * Includes line breaks ('\n')
     */
    private fun countLinesInParagraphs(
        textRaw: String,
        font: Font,
        fontMetrics: FontMetrics,
        formatWidth: Float
    ): WrappedParagraph {
        val paragraphsAsString: List<String> = textRaw.split("\n")
        val sentences = paragraphsAsString.map { paragraph ->
            countLinesInSentence(paragraph, font, fontMetrics, formatWidth)
        }
        return WrappedParagraph(sentences = sentences)
    }

    /**
     * Counts lines in wrapped [JTextArea]
     * Does not include line breaks.
     */
    private fun countLinesInSentence(
        textRaw: String,
        font: Font,
        fontMetrics: FontMetrics,
        formatWidth: Float
    ): Sentence {
        if (textRaw.isEmpty()) {
            return Sentence(
                wraps = listOf(
                    SentenceWrap(
                        wrapIndex = -1,
                        wrapText = textRaw
                    )
                )
            )
        }
        val text = AttributedString(textRaw)
        text.addAttribute(TextAttribute.FONT, font)
        val frc = fontMetrics.fontRenderContext
        val charIt = text.iterator
        val words = mutableListOf<SentenceWrap>()
        val lineMeasurer = LineBreakMeasurer(
            charIt,
            BreakIterator.getLineInstance(),
            frc
        )
        lineMeasurer.position = charIt.beginIndex
        var posBegin = 0
        var posEnd = lineMeasurer.position
        var noLines = 0
        do {
            lineMeasurer.nextLayout(formatWidth)
            posBegin = posEnd
            posEnd = lineMeasurer.position
            words.add(
                SentenceWrap(
                    wrapIndex = noLines,
                    wrapText = textRaw.substring(posBegin, posEnd)
                )
            )
            noLines++
        } while (posEnd < charIt.endIndex)
        return Sentence(words)
    }

    /**
     * Holds wrapped [Sentence]s that break during 'wrap text' or text break symbols
     */
    data class WrappedParagraph(
        val sentences: List<Sentence>
    ) {
        fun lineCount(): Int {
            val sentenceCount = sentences.fold(0) { currentCount: Int, sentence: Sentence ->
                val newCount = currentCount + sentence.lineCount()
                newCount
            }
            return sentenceCount
        }
    }

    /**
     * Sentence contains text pieces which are broken by 'wrapText'
     */
    data class Sentence(
        val wraps: List<SentenceWrap>
    ) {
        fun lineCount(): Int = wraps.size
    }

    /**
     * Entity for holding a wrapped text snippet
     */
    data class SentenceWrap(
        val wrapIndex: Int,
        val wrapText: String
    )

    interface TextProvider {
        val text: String
        val formatWidth: Float
    }

    companion object {
        val l = LoggerFactory.getLogger(TextAreaLineCounter::class.java)!!
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文