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)!!
}
}