Kann man die Zeilen einer JTextArea nummerieren?
Eine Textkomponente, die Zeilennummern darstellen kann leitet
man nicht von JTextArea, sondern sinnvollerweise von JScrollPane
ab. Sie besitzt die Methode setRowHeaderView(), der als
Parameter eine Komponente zur Darstellung der Zeilennummern
ü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 der JTextArea
- numberLineBreak dient zur Steuerung der Darstellung der Zeilennummerierung abhängig vom Zeilenumbruch des geladenen Textes. Ist dieser Wert auf true gesetzt, so werden bei der Zeilennummerierung nur die Zeilenanfänge des geladenen Textes nummeriert. Durch die JTextArea umgebrochene Zeilen bleiben unnummeriert.
Die Beispielklasse erweitert JScrollPane. Im Konstruktor werden
zwei JTextArea initialisiert, von denen textArea zur
Darstellung des geladenen Textes und numberArea zur
Darstellung der Zeilennummern dient. Die erste bleibt im
Gegensatz zur zweiten natürlich 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 geladen. Die JTextArea 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.text.BadLocationException;
import javax.swing.text.Utilities;
public class NumberedTextArea extends JScrollPane implements ComponentListener,
KeyListener {
private JTextArea textArea, numberArea;
private boolean areaLineBreak = true;
private boolean numberLineBreak = true;
private String datNam = "test.txt";
private int rows = 20, cols = 60;
public NumberedTextArea() {
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));
this.setViewportView(textArea);
this
.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
this
.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
// eine JTextArea für die Zeilennummern erstellen und formatieren
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);
this.setRowHeaderView(numberArea);
}
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) {
JFrame frame = new JFrame();
JScrollPane sp = new NumberedTextArea();
frame.add(sp);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(500, 400);
frame.setVisible(true);
}
}
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 der JTextArea 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 der JTextArea getrennt werden muss, da entschieden werden muss, ob er vom Originaltext oder dem Umbruch der JTextArea 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 der JTextArea 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 der JTextArea gezeigten Zeilen
nummeriert werden.
In diesem Fall wird ebenfalls die JTextArea nach einem Zeilenumbruch durchsucht, nur dass nun in jeder dort gezeigten Zeile eine Nummer hizugefügt und gezeigt wird