Kann man die Zeilen einer JTextArea nummerieren?
Das Prinzip besteht darin, eine JTextArea
für den anzuzeigenden Text als Viewport eines JScrollPane
und eine weitere als RowHeader zu setzen. Allerdings besteht
eine Einschränkung darin, dass die Nummerierung nur
dann ohne weiteres und vollständig dargestellt werden
kann, wenn in der JTextArea
kein Zeilenumbruch
aktiviert ist und die Nummerierung zudem ohne Rücksicht
auf die Position der Zeilenumbrüche des Originaltextes
erfolgen soll. Möchte man, dass nur die
Zeilenumbrüche des geladenen Textes angezeigt werden
oder auch, dass bei aktiviertem Zeilenumbruch der JTextArea
alle dargestellten Zeilen kontinuierlich nummeriert werden,
so muss man etwas mehr Aufwand treiben und, wie hier
demonstriert, die Zeilenumbrüche auslesen. Für die
praktische Anwendung muss hierbei jedoch beachtet werden,
dass dies bei großen Texten die Anzeigegeschwindigkeit
der Zeilennummern erheblich ausbremst.
Ein JScrollPane
besitzt die Methode setRowHeaderView()
,
der als Parameter eine Komponente zur Darstellung eines
Zeilenheaders übergeben wird und die am linken
Bildrand einen Anzeigebereich generiert, der
hinsichtlich seiner Erscheinung konventionell formatiert
werden kann.
Zu Beginn definiert das Beispiel zwei
Instanzvariablen:
areaLineBreak
dient der Steuerung des Zeilenumbruchs derJTextArea
numberLineBreak
dient zur Steuerung der Darstellung der Zeilennummerierung abhängig vom Zeilenumbruch des geladenen Textes. Ist dieser Wert auftrue
gesetzt, so werden bei der Zeilennummerierung nur die Zeilenanfänge des geladenen Textes nummeriert. Durch die den Text präsentierende Area umgebrochene Zeilen bleiben dann unnummeriert.
Im Konstruktor wird die Methode init()
aufgerufen. Sie erzeugt einen JScrollPane
und zwei JTextArea
-Komponenten, von denen textArea
zur Darstellung des geladenen Textes und numberArea
zur Darstellung der Zeilennummern dient. Die erste
bleibt im Gegensatz zur zweiten editierbar. Ihr
Zeilenumbruch wird durch die Methoden setLineWrap()
und setWrapStyleWord()
über die o.a.
Instanzvariable gesteuert. Um später die
Zeilennummern aktualisieren zu können, wird der
Textbereich bei einem ComponentListener
und
einem KeyListener
angemeldet.
Über die Methode ladeDatei()
, die im
Beispiel eine Testdatei mittels eines FileReader
liest, wird der anzuzeigende Text in den Textbereich der
zentralen Area geladen. Sie wird dem JScrollPane
hinzugefügt und die Scrolleigenschaften so
eingestellt, dass bei Bedarf horizontal und vertikal
gescrollt werden kann.
Die JTextArea
zur Anzeige der Zeilennummern wird ein wenig formatiert
und schließlich mit Hilfe der Methode setRowHeaderView()
dem JScrollPane
hinzugefügt.
Die
Zeilenanzeige wird durch die Methode gibNummern()
gesteuert. In ihr wird abhängig von den
Einstellungen der Zeilenumbrüche mittels eines StringBuilder
ein Text erzeugt, der die einzelnen Zeilennummern
untereinander darstellt. Hierzu werden die Zeichenzahl
und die Anzahl der Zeilen des geladenen Textes in zwei
lokalen Variablen gespeichert. Es ist wichtig zu wissen,
dass hier nicht die Anzahl der in der JTextArea
gezeigten Zeilen, sondern diejenige des Originaltextes
gespeichert wird. Ein z.B. 10-zeiliger Text mit langen
Zeilen kann bei aktiviertem Zeilenumbruch innerhalb der
JTextArea
auf 20 oder mehr Zeilen
umgebrochen werden.
Zum Auslesen der verschiedenen
Offsets in Bezug auf den Originaltext stellt JTextArea
die folgenden Methoden bereit:
getLineEndOffset(int line)
Offset (Zeichenzahl) des Zeilenendes der gegebenen ZeilegetLineStartOffset(int line)
Offset des Zeilenbeginns (Zeichenzahl bis zum Zeilenbeginn) der gegebenen ZeilegetLineOfOffset(int offset)
Zeilennummer der Cursorposition
Zum Bestimmen des Zeilenend-, bzw. -start-Offsets eines
in einer Komponente geladenen Textes können
Methoden der Klasse Utilities
herangezogen
werden.
import java.awt.Color;
import java.awt.Insets;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.text.BadLocationException;
import javax.swing.text.Utilities;
public class NumberedTextArea implements ComponentListener, KeyListener {
private static final long serialVersionUID = 1L;
private JTextArea textArea, numberArea;
private boolean areaLineBreak = true;
private boolean numberLineBreak = true;
private String datNam = "test.txt";
private static final int ROWS = 20, COLS = 60;
public NumberedTextArea() {
init();
}
private void init() {
textArea = new JTextArea(ROWS, COLS);
textArea.setMargin(new Insets(0, 10, 0, 10));
textArea.setLineWrap(areaLineBreak);
textArea.setWrapStyleWord(areaLineBreak);
textArea.addComponentListener(this);
textArea.addKeyListener(this);
textArea.setText(ladeDatei(datNam));
JScrollPane sp = new JScrollPane();
sp.setViewportView(textArea);
sp.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
sp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
numberArea = new JTextArea();
gibNummern();
numberArea.setMargin(new Insets(0, 4, 0, 4));
numberArea.setBackground(new Color(240, 240, 255));
numberArea.setForeground(new Color(180, 180, 180));
numberArea.setEditable(false);
sp.setRowHeaderView(numberArea);
JFrame frame = new JFrame();
frame.add(sp);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private String ladeDatei(String datName) {
File file = new File(datName);
if (!file.canRead() || !file.isFile()) {
System.err.println("Datei nicht vorhanden oder nicht lesbar!");
System.exit(0);
}
FileReader fr = null;
int c;
StringBuffer buff = new StringBuffer();
try {
fr = new FileReader(file);
while ((c = fr.read()) != -1) {
buff.append((char) c);
}
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
return buff.toString();
}
public void gibNummern() {
StringBuilder sb = new StringBuilder();
int zeilenZahl = textArea.getLineCount();
int textLength = textArea.getText().length();
if (!areaLineBreak & zeilenZahl != 0) {
for (int i = 0; i < zeilenZahl; i++) {
sb.append(String.valueOf(i + 1) + "\n");
}
}
try {
if (numberLineBreak && areaLineBreak) {
Pattern p = Pattern.compile(System.getProperty("line.separator"));
sb.append("1\n");
int n = 1;
for (int i = 0; i < textLength; i++) {
int lineEnd = Utilities.getRowEnd(textArea, i);
int lineStart = Utilities.getRowEnd(textArea, i + 1);
if (lineEnd < lineStart) {
String s = textArea.getText().substring(lineEnd, lineStart);
Matcher m = p.matcher(s);
boolean result = m.find();
if (!result) {
sb.append("\n");
} else {
n++;
sb.append(String.valueOf(n) + "\n");
}
}
}
} else if (!numberLineBreak && areaLineBreak) {
int n = 0;
for (int i = 0; i < textLength; i++) {
int lineEnd = Utilities.getRowEnd(textArea, i);
int lineStart = Utilities.getRowEnd(textArea, i + 1);
if (lineEnd < lineStart) {
n++;
sb.append(String.valueOf(n) + "\n");
}
}
sb.append(String.valueOf(++n) + "\n");
}
} catch (BadLocationException e1) {
e1.printStackTrace();
}
numberArea.setText(sb.toString());
}
public void componentHidden(ComponentEvent e) {
}
public void componentMoved(ComponentEvent e) {
}
public void componentResized(ComponentEvent e) {
gibNummern();
}
public void componentShown(ComponentEvent e) {
}
public void keyPressed(KeyEvent e) {
}
public void keyReleased(KeyEvent e) {
gibNummern();
}
public void keyTyped(KeyEvent e) {
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new NumberedTextArea());
}
}
Im Folgenden findet eine dreifache Fallunterscheidung über einfache if-else-Verzweigungen statt:
- Der Zeilenumbruch der
JTextArea
ist deaktiviert.
In diesem Fall sind die Zeilenzahl im Anzeigebereich und diejenige des Originaltextes identisch und es reicht aus, die Anzahl der Zeilen derJTextArea
auszulesen und die Nummern von 1 an untereinander auszugeben. - Der Zeilenumbruch der
JTextArea
ist aktiviert, es sollen nur die Zeilenanfänge des Originaltextes nummeriert werden.
Hier besteht, wie bereits oben kurz angesprochen, das Problem, dass ein Auslesen der Zeilenumbrüche des geladenen Originaltextes von demjenigen derJTextArea
getrennt werden muss, da entschieden werden muss, ob er vom Originaltext oder dem Umbruch derJTextArea
stammt. Dies geschieht über das zeichenweise Durchsuchen des Textes nach dem systemweiten Zeilenumbruchszeichen mittels eines regulären Ausdrucks. Hierzu wird so vorgegangen, dass Zeichen für Zeichen die Position des Zeichens am Ende der Zeile innerhalb derJTextArea
ermittelt wird, in der sich das untersuchte Zeichen befindet. In dem Moment, in dem sich diese End-Position ändert, bedeutet dies, dass eine neue Zeile begonnen hat. Entspricht das an dieser Stelle gefundene Zeichen demjenigen für einen Zeilenumbruch, so beginnt daraufhin eine neue Zeile des Originaltextes und dem Text der Zeilenanzeige wird eine neue Zahl hinzugefügt. Im anderen Fall wird im Textbereich der Zeilennummern ein einfacher Zeilenumbruch ohne neue Ziffer erzeugt und die nächste Zeile untersucht. - Der Zeilenumbruch der
JTextArea
ist aktiviert, es sollen alle in derJTextArea
gezeigten Zeilen nummeriert werden.
In diesem Fall wird ebenfalls dieJTextArea
nach einem Zeilenumbruch durchsucht, nur dass nun in jeder dort gezeigten Zeile eine Nummer hinzugefügt und gezeigt wird.
Wenn Ihnen javabeginners.de gefällt, freue ich mich über eine Spende an diese gemeinnützigen Organisationen.