# Zeichen verschiedenfarbig in einer JTextPane darstellen



## Youlian (6. Jan 2007)

Hallo,

ich bin langzeitiger Leser und nun zum ersten mal Poster hier, da ich nun bei meinem Problem schon längere Zeit anstehe.

Nun zu meinem Projekt: Ich habe einen Art Editor programmiert der auch syntax-Highlighting unterstützen soll.
Die Schlüsselwörter werden vorher aus einer XML Datei eingelesen: Es gibt Kommandos, Funktionen, spezielle Variablen, Kommentare und noch ein paar andere... Das ist aber für mein eigentliches Problem nicht so wichtig.

Also:

Ich habe eine JTextPane der ich ein javax.swing.text.DefaultStyledDocument (ab jetzt: doc)  zuweise.

Dieses doc erzeuge ich mir mit dem Konstruktor der einen StyleContext nimmt. In diesem StyleContext sind meine Styles die später zuweisen werde abgelegt. Die Stylehierarchie ist nichts Besonders. -> Alle Styles haben als "parent"-Style den DefaultStyle und unerscheiden sich nur im Attribut "Foreground" (also in meinem Fall, Textfarbe) von einander.

Nun gibt es in meinem Editor noch ein scanner-Objekt, dass bestimmt welche Farbe/Style welche Zeichen haben sollen.
Diese Scanner-Klasse ist erneut nichts großartiges, und macht auch keinen großen Rechenaufwand.
Diesem scanner-objekt gebe ich noch mein DefaultStyleDocument bekannt (es bekommt eine Referenz).

Auf dem Document habe ich einen DocumentListener regestriert der nichts anderes macht als bei insertUpdate (es gibt noch changeUpdate und removeUpdate) die Methode scan() auf dem scannerObjekt aufzurufen.

Die scan Methode schnappt sich mit doc.getText() den Inhalt (string) des docs (also gleichzeitig den Inhalt der JTextPane) und parst diesen. Jedes mal wenn ein vollständiger Token erkannt wurde setze ich mit

```
this.doc.setCharacterAttributes(drawFrom, pos, this.doc.getStyle(""+this.token), true);
```
für diesen Abschnitt/Token den Style mit der richtigen Farbe. drawFrom ist hierbei der Anfang des tokens, pos die Länge; mit this.doc.getStyle(String) hole ich mir den entsprechenden Style aus dem DefaultStyleDocument, dem ich im Konstruktor meinen vorbereiteten StyleContext übergeben habe, true -> heißt das ich die bisherigen Attribute für diesen Abschnitt überschreiben will) ""+this.token, desshalb weil mein token nur ein int ist, dieser int legt fest um welche Art von Schlüsselwort es sich handelt. Die Stylenamen sind einfach Nummern: "1", "2".. etc..

Ok - so weit so gut. Es funktioniert grundsätzlich - nur elend langsam.

Falls ich das scan() direkt im AWT-Thread der das insertUpdate des DocumentListeners kommt wie zu erwarten, (oder wenn man gleich mit copy & paste ganze textblöcke einfügt) eine "Exception in thread "AWT-EventQueue-0" java.lang.IllegalStateException: Attempt to mutate in notification". Die setCharacterAttributes() ist nämlich "thread-sicher" im Gegensatz zu den meisten anderen Swing-Methoden.

Also habe ich das scannen und setzen der Styles in einen eigenen Thread ausgelagert. (Bei jedem insertupdate wird ein neuer Thread angelegt).
Jetzt flackert der Text bei jedem Tastendruck wie wild herum, da die Styles immer neu gesetzt werden. Außerdem geht die Prozessorauslastung beim Einfügen größere Textblöcke sehr schnell auf 100%. (10 Zeilen kopieren und dann schnell 10 mal STRG+V und man kann mit eigenen Augen zusehen wie er die Styles setzt :/, diesen Test nenne ich ab jetzt "schnelles Einfügen")
Um das "Flackern" (in der JTextPane) wegzubekommen habe ich versucht DoubleBuffered auf true zu setzen -> kein erfolg.

Nun habe ich das Scannen und Setzen mit SwingUtilities.invokeLater(new Runnable() { ... }); probiert, also wieder vom AWT-Thread machen zu lassen.
Das Flackern war weg, aber dafür war es noch unperformanter. Und wenn man jetzt das "schnelle Einfügen" gemacht hat dann war die ganze GUI für 2-3 Sekunden nicht ansprechbar weil der AWT Thread beschäftigt war.

Ein weiter Ansatz der nicht wirklich was gebracht hat war einen Thread der beim Erzeugen der Scannerklasse ebenfalls erstellt wurde und dann mit Hilfe eines Objects und eines boolean so synchronisiert wurde, dass er immer schläft außer es tritt ein insertUpdate am Dokument auf: läuft in einer Endlosschleif als Daemon und wird immer dann vom AWT-Thread aufgeweckt wenn das Dokument verändert wurde, der awt-Thread setzt also das parse-boolean auf true weckt den Scanner-Thread auf und ist fertig mit insertUpdate. Der ScannerThread scannt und setzt die Styles setzt den parse boolean auf false und legt sich wieder schlafen. -> Kein Performancegewinn, Flacker vermindert aber nicht weg; außerdem wurde oft das letze Schlüsselwort beim copy & paste nicht richtig gehighlightet da manche insertUpdate( wo ich den parse boolean auf true setze und den thread aufwecke (notifyAll() einfach verpufft sind, da der Thread noch nicht wieder mit dem Scannen fertig war, aber schon ein neues insertUpdate-Event ausgelöst wurde

Ein ganz ein anderer Ansatz -> der vorher schon einma perfekt funktioniert hat:
Das Dokument wird immer dann neu geparst wenn der Text in der JTextPane neu gezeichnet werden muss.
Es wurde die API-Methode drawUnselectedText(Graphics g) überschrieben und vor jedem "Zeichnen" (am Bildschirm) die aktuelle Zeichenfarbe auf die gewünschte Farbe gesetzt. So wurde dann Token für Token auf den Bildschirm gezeichnet. Das war flacker und ruckelfrei möglich. Daher bin ich mir auch ziemlich sicher, dass die Scan-Funktion nicht die Bremse/der große Aufwand ist, da die drawUnselectedText nicht nur aufgerufen wurde wenn sich der Inhalt des Dokuments verändert hat sondern immer dann wenn der Text neu gezeichnet werden muss. (also zB. auch dann wenn das Fenster verschoben wird oder man mit dem Mauszeiger darüberfährt)
Ich hab den Ansatz dann verworfen, weil ich den Editor noch erweitern will (Autovervollständigung) und es sehr schwer ist, dann hinter die gezeichneten Tokens noch Logik zu legen. Generell ist es kein schönes Konzept. (nicht Java )

Sachen die ich bereits probiert habe, keinen Unterschied/Erfolg gebracht haben:
die JTextPane.setDoubleBuffered(true); setzen
eigener Thread für jeden Parsevorgang
SwingUtilites.invokeLater()
Einzelner Thread der parst und sich dann wieder schlafen legt.
(Zeichenfarbe vor jedem Token setzen *pfui* schmutzig)

Sachen die ich noch nicht ausprobiert habe, weil ich nicht glaube, dass sie was bringen:
einen eigenen Thread (entweder einzelner mit Schlafen legen oder immer neuen pro Parsen) fürs Parsen der statt dass er die Styles setzt die Position(start, ende) und Schlüsselwort eine Collection anlegt und dann mit SwingUtilities.invokeLater() dem AWT Thread sagt, dass dieser diese Styles setzten soll -> (ich glaube nicht, dass das Scannen die Bremse ist, siehe oben, ich glaube der Flaschenhals ist das setzen der Styles)

Mach ich vom Konzept her etwas falsch? - Ich weiß echt nicht mehr weiter. Hab extra schon wie inder API beschrieben ein Document mit StyleContext, damit für alle Textabschnitte mit gleicher Farbe auch das gleiche Style-Objekt verwendet wird, erstellt.
Falls sich wirklich jemand die Mühe machne würde und sich den Code anschauen will, ich kann ihn soweit es geht vom bestehenden Code trennen (so viel, wie es sich vielleicht anhört ist es auch wieder nicht) (Scanner-Klasse liefert dann halt nur Random-dummy Farben). -> Ich wäre demjenigen dann ewig dankbar. Anbieten kann ich euch für eure Hilfe leider nichts. (Ist bloß ein freiwillges Projekt eines Klassenkameradens und mir für die Schule),

Ein weiterer Versuch wäre ein Thread der das Dokument einfach in gewissen Zeitabstand scannt. Ich glaub, da tritt das Problem des flackerns aber wieder auf 

Falls irgendetwas noch unklar: Einfach hier posten.

Ich bin für jede Hilfe dankbar -> weiß inzwischen echt nicht mehr weiter.
Youlian


----------



## Wildcard (6. Jan 2007)

hmm.... jede Menge Informationen....
Bevor man da irgendetwas angeht solltest du das echte Performanceproblem herausarbeiten.
Lass dazu einen Profiler über die Anwendung laufen, dann sollte der Bösewicht schnell gefunden sein.
Sobald du mehr weißt: Melde dich nochmal.


----------



## Youlian (6. Jan 2007)

Wildcard hat gesagt.:
			
		

> Lass dazu einen Profiler über die Anwendung laufen, dann sollte der Bösewicht schnell gefunden sein.



Hallo,
ich kann zwar erahnen was so ein Profiler macht, habe aber noch nie einen benutzt, bzw. kenne kein geeignetes Programm.
Kannst du mir einen empfehlen. (Link?)
Und danke für die schnelle Antwort.


----------



## Illuvatar (6. Jan 2007)

Wenn du Eclipse verwendest, schau einfach mal bei den Plugins davon.

Oder du verwendest den Profiler, den Java eingebaut hat. Er lässt sich über den Kommandozeilenparameter -Xrunhprof starten. Mehr dazu steht zum Beispiel im Javabuch (ich glaub das ist Kapitel 49, das Kapitel zu Performance eben).


----------



## Wildcard (6. Jan 2007)

Für Eclipse kannst du zB tptp verwenden.


----------



## Youlian (7. Jan 2007)

Da ich NetBeans verwende habe ich mir dafür den Profiler 5.5 runtergeladen, das Tutorial gemacht und hier sind die Ergebnisse (nochmals Danke für den Tipp, ich habe vorher nicht gewusst, dass es so einfach zu bedienende aber dennoch brauchbare Tools gibt; war eher der Meinung, dass das was für Experten die in der Console und auf einer tieferenen Ebene rumhantieren, ist):





Wie ich befürchtet habe ist der Auslöser die thread-sichere swing-Funktion "setCharacterAttributes". Diese fordert eine Schreibsperre an-> (soweit ich das aus der API richtig verstanden habe). Also mache ich das natürlich mit einem synchronisierten Thread, statt für jeden Parsevorgang einen eigenen Anzulegen. Weiters ist es mir gelungen, nicht mehr das ganze Dokument zu parsen, sondern nur mehr den Teil (besser gesagt: die Zeile in der sich etwas verändert hat). Ich war ziemlich happy, musste aber mit Ernüchterung feststellen, dass das nur wenig bis sehr wenig gebracht hat.
Die oben erwähnte Methode braucht bei mehreren threads ca. 95% der Laufzeit (sagt man das so?) und bei einem Thread immerhin noch 90%.

Zur Zeit mache ich es so:
Ein Thread parst und setzt Attribute und wird vom awt-thread aufgeweckt.

Zu meiner Verwunderung ging es am Besten wenn ich alles vom awt-Thread machen hab lassen!
Dann ist mir auch eingefallen warum das so sein könnte: Ein Thread verändert die Zuweisung eines Styles im Dokument -> es wird ein AttributeChanged (oder so ähnlich, habs bis jetzt noch nicht gefunden)-Event geworfen und der awt-Thread wird von diesem angestoßen das die TextPane neu zu zeichnen -> Wenn alles der awt-Thread macht (SwingUtilities.invokeLater()) dann wird die JTextPane nur einmal neu gezeichnet, und zwar wenn der Thread mit parsen fertig ist. -- So oder so ähnlich stelle ich mir das zumindest vor. - Ich kann mich natürlich auch irren.

Nun wäre es toll wenn man am Dokument irgendwo einstellen kann, dass dieses AttributeChangeEvent (ich weiß nicht wie es wirklich heißt) nicht geworfen wird. -> Ich habe schon die API durchsucht und auch geglaubt, dass ich es gefunden habe. Hat jedoch nicht funktioniert oder war eine Methode die für etwas anderes gedacht war. Weiß einer von euch darüber bescheid oder kann mir anderwertig weiterhelfen?

Youlian


----------



## Wildcard (7. Jan 2007)

Mein Vorschlag: Setz ein eigenes StyledDocument  das erst dann ein Event feuert wenn du fertig mit dem Parsen bist und alle Attributes gesetzt sind.


----------



## Youlian (7. Jan 2007)

In der API habe ich im AbstractDocument (von dem das DefaultStyledDocument erbt) diese 3 Methoden gefunden:


```
protected  void fireChangedUpdate(DocumentEvent e)

protected  void fireInsertUpdate(DocumentEvent e)

protected  void fireRemoveUpdate(DocumentEvent e)
```
Notifies all listeners that have registered interest for notification on this event type.

Wie genau gehe ich jetzt beim Überschreiben der Methoden vor ? Sind das überhaupt dir richtigen?
Weiters habe ich noch folgendes gefunden


```
protected  void writeLock()
```
Acquires a lock to begin mutating the document this lock protects.

```
protected  void writeUnlock()
```
Releases a write lock previously obtained via writeLock.

Muss oder soll ich Locks setzen wenn ich die Attribute für den Inhalt neu zuweise?


----------



## Wildcard (7. Jan 2007)

Überschreib doch setCharacterAttributes. Mit Copy/Paste kannst du die alte Implementierung übernehmen, feuerst das Event aber nicht. Am ende triggerst du dann manuell.


----------



## Youlian (7. Jan 2007)

```
/**
     * Sets attributes for some part of the document.
     * A write lock is held by this operation while changes
     * are being made, and a DocumentEvent is sent to the listeners 
     * after the change has been successfully completed.
     * 


     * This method is thread safe, although most Swing methods
     * are not. Please see 
     * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
     * to Use Threads</A> for more information.     
     *
     * @param offset the offset in the document >= 0
     * @param length the length >= 0
     * @param s the attributes
     * @param replace true if the previous attributes should be replaced
     *  before setting the new attributes
     */
    public void setCharacterAttributes(int offset, int length, AttributeSet s, boolean replace) {
        if (length == 0) {
            return;
        }
	try {
	    writeLock();
	    DefaultDocumentEvent changes = 
		new DefaultDocumentEvent(offset, length, DocumentEvent.EventType.CHANGE);

	    // split elements that need it
	    buffer.change(offset, length, changes);

	    AttributeSet sCopy = s.copyAttributes();

	    // PENDING(prinz) - this isn't a very efficient way to iterate
	    int lastEnd = Integer.MAX_VALUE;
	    for (int pos = offset; pos < (offset + length); pos = lastEnd) {
		Element run = getCharacterElement(pos);
		lastEnd = run.getEndOffset();
                if (pos == lastEnd) {
                    // offset + length beyond length of document, bail.
                    break;
                }
		MutableAttributeSet attr = (MutableAttributeSet) run.getAttributes();
		changes.addEdit(new AttributeUndoableEdit(run, sCopy, replace));
		if (replace) {
		    attr.removeAttributes(attr);
		}
		attr.addAttributes(s);
	    }
	    changes.end();
	    fireChangedUpdate(changes);
	    fireUndoableEditUpdate(new UndoableEditEvent(this, changes));
	} finally {
	    writeUnlock();
	}

    }
```

Du meinst ich soll da sozusagen einfach die fire***-Methoden auskommentieren und dann wenn ich mit dem setzen der Attribute fertig bin auf der JTextPane ein validate() machen?


----------



## Wildcard (7. Jan 2007)

Ich hab mich mit StyledDocuments ehrlich gesagt nie beschäftigt, daher bin ich nicht sicher was im Hintergrund alles passiert. Wenn ich mir den Source so ansehe, würde ich diese 3 Zeilen:
	
	
	
	





```
changes.end();
       fireChangedUpdate(changes);
       fireUndoableEditUpdate(new UndoableEditEvent(this, changes));
```
entfernen, und gegebenenfalls den lock. Dann machst du in deiner abgeleiteten Klasse eine flush Methode in der dieser Code ausgeführt wird. Diese Methode rufst du auf sobald du mit dem Scanner durch bist.
Dann mal schauen was sich in der Performance verändert.
Als nächstes würde ich mir dann überlegen ob dein Document den Scanner nicht selbst verwenden sollte, anstatt von aussen Attribute gesetzt zu bekommen.


----------



## Youlian (7. Jan 2007)

Ok, schonmal Danke für die Hilfe.
Das werde gleich morgen ausprobieren (heute nicht mehr, hab erstmal genug von Java  :wink: ).

Ich wünsche noch einen schönen Sonntag Abend.


----------



## Youlian (9. Jan 2007)

Also ich habe jetzt meine eigenes DefaultStyledDocument und die ensprechenden Methoden sehen wie folgt aus:
(wenn ich versuche ohne Lock; writeLock() das Attribut zu sethen dann schmeißt er bei
buffer.change(offset, length, changes); eine
"Exception in thread "uc4-script-parser" javax.swing.text.StateInvariantError: Illegal cast to MutableAttributeSet" - Exception.

Also lass ich das Lock drinnen und kommentier einfach mal die beiden Methoden die die Events werfen auf die der awt anspringt aus:



```
public void setCharacterAttributes(int offset, int length, AttributeSet s, boolean replace) {
        if (length == 0) {
            return;
        }
        try {
            writeLock();
            DefaultDocumentEvent changes =
                    new DefaultDocumentEvent(offset, length, DocumentEvent.EventType.CHANGE);
            
            // split elements that need it
            buffer.change(offset, length, changes);
            
            AttributeSet sCopy = s.copyAttributes();
            
            // PENDING(prinz) - this isn't a very efficient way to iterate
            int lastEnd = Integer.MAX_VALUE;
            for (int pos = offset; pos < (offset + length); pos = lastEnd) {
                Element run = getCharacterElement(pos);
                lastEnd = run.getEndOffset();
                if (pos == lastEnd) {
                    // offset + length beyond length of document, bail.
                    break;
                }
                MutableAttributeSet attr = (MutableAttributeSet) run.getAttributes();
                changes.addEdit(new AttributeUndoableEdit(run, sCopy, replace));
                if (replace) {
                    attr.removeAttributes(attr);
                }
                attr.addAttributes(s);
            }
            changes.end();
            //fireChangedUpdate(changes);
            //fireUndoableEditUpdate(new UndoableEditEvent(this, changes));
        } finally {
            writeUnlock();
        }
    }
    
//der Offset, Length in dieser Methode "überspannt" alle Attribute die in der obigen Methode gesetzt wurden
//zb. wurde oben an der Position 3 mit länge 4 und an der position 7 mit länge 2 die Attribute gesetzt
// dann ist dieser offset 3 und die länge 6
    public void flush(int offset, int length) {
        if (length == 0) {
            return;
        }
            DefaultDocumentEvent changes = new DefaultDocumentEvent(offset, length, DocumentEvent.EventType.CHANGE);
            changes.end();
            fireChangedUpdate(changes);
            fireUndoableEditUpdate(new UndoableEditEvent(this, changes));
    }
```

Mit dem Ergebnis das die Methode jetzt nur mehr 5% der ganzen Programmlaufzeit in Anspruch nimmt !!! (getestet anhand eines sehr langen Codestücks)
Der geparste Text bleibt - wie zu erwarte, da der AWT-Thread nicht anspringt und neu zeichnet - weiß.
Jedoch tritt bei der 2 Veränderung (als noch nicht beim 1. Einfügen, oder nach einmaligen drücken einer Taste: zb: Schreiben eines beliebigen Buchstabens, oder Einfügen vieler Zeilen Scriptcodes) sondern genau bei der 2. Veränderung eine Exception. (egal ob ich meine flush Methode aufrufe oder nicht.)



> Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
> at javax.swing.text.CompositeView.replace(CompositeView.java:170)
> at javax.swing.text.View.updateChildren(View.java:1095)
> at javax.swing.text.View.insertUpdate(View.java:679)
> ...



Ich habe keine Ideen mehr, wie ich ein "überspannendes" gültiges Event simulieren soll. :/
Was mir noch einfallen würde wäre: Speichern der Events in eine Collection die ich dann nachher erst "feuere". So würde der parse-thread wenigstens nicht unterbrochen, dennoch würden dann "einen ganzen Haufen" Events fliegen. Also eigentlich auch keine Lösung.


----------



## Wildcard (9. Jan 2007)

Youlian hat gesagt.:
			
		

> Ich habe keine Ideen mehr, wie ich ein "überspannendes" gültiges Event simulieren soll. :/
> Was mir noch einfallen würde wäre: Speichern der Events in eine Collection die ich dann nachher erst "feuere". So würde der parse-thread wenigstens nicht unterbrochen, dennoch würden dann "einen ganzen Haufen" Events fliegen. Also eigentlich auch keine Lösung.


5% hört sich doch schon ganz gut an   
Das mit der Collection könnte durchaus funktionieren. Das eigentliche Problem ist vermutlich das repaint. Wenn diese Events aber sehr schnell aufeinander kommen, dann kann der EventDispatcher redundante paints aus der queue werfen und damit die benötigte Gesamtrechenzeit drücken.
Ich weiß nicht wie das ganze aussieht, aber falls es zu lange dauert bis man was sieht, könntest du auch hin und wieder ein Event 'freilassen' das schonmal etwas gezeichnet werden kann.
Ich würde dir gern mehr sagen, leider fehlen mir zu den Internas von JTextPane und StyledDocument jegliche Hintergrundinformation. 
De facto: Hab ich noch nie benutzt  :bae:


----------



## Guest (10. Jan 2007)

Es treten immer Exceptions auf, sogar wenn ich mir alle Events in einer Liste merke und sie nachträglich werfe. :/ 
Sobald ich die Events bzw. deren Feuerung abändere treten Exceptions (siehe oben) "tief" drinnen in der API auf, die ich nicht wirklich zurückverfolgen kann.
-> Ich werde wohl zu der alten Implementierung (Setzen der Farbe vor dem Zeichnen, immer alles Parsen) zurückgehen.


----------



## Wildcard (10. Jan 2007)

Je nachdem was du alles brauchst könnte man das Ding auch selbst schreiben.
Aber wenn die Geschwindigkeit für dich noch im erträglichen Maß ist....


----------



## Youlian (10. Jan 2007)

Selbst schreiben hört sich jetzt zwar verlockend an, wegen der Übersichtlichkeit und Performance, aber ich bin mir ziemlich sicher, dass dabei auch unlösbare/schwere (für mich, als nicht java profi) Probleme auf mich zukommen.

Brauchen würde ich einfach nur eine grafische Komponente wie etwa JTextPane, JTextArea bei der ich einzelne Abschnitte, Zeichen verschiedenfarbig darstellen kann. (Ohne die oben beschriebenen Wechselwirkungen; AWT-Thread, Event werfen)
Wenn sich irgendwer findet der sowas machen könnte, währe ich natürlich dankbar - ich kann mit meinem jetzigen java/swing nicht.


----------



## kawrom (14. Jan 2007)

Hi,
Ich hatte auch das gleiche Problem wie Du mit der Performance:
bei ca 10 Zeilen Text ging das noch rel. schnell, aber bei 100 oder mehr Zeilen konnte man zusehen wie sich die Farbe der einzelnen chars aufbaut.

Hab dies aber dann doch gelöst:
    beim parsen sammelst du nur die Farb- bzw. Font-Werte und die einzelnen Positionen für alle chars in einem Vector.
    Nachdem der Parse-Vorgang abgeschlossen ist und du alle Werte berechnet hast,
    läufst du den kompletten Vector in einer Schlefe durch und setzt die Werte der einzelnen chars 
    (setCharacterAttributes(beg, length, doc.getStyle(color), true); ... wobei length immer 1 beträgt)


... so einfach ist das!


----------

