Gastbeitrag: AWT-Schwergewichte nach vorne, bitte!

Der Code hinter Container.add(Component comp) hat sich immer wieder gewandelt, und lange Zeit war es problematisch, AWT- und Swing-Komponenten zu mischen. Seit Java 7 stellt das AWT jedoch jede mögliche Kombination von beliebigen Komponenten zuverlässig dar – den zusätzlichen Rechenaufwand bekommt jeder zu spüren, der etwa versucht, eine JScrollPane mit 50.000 JLabels zu bestücken.

Wenn ich im Folgenden von „Komponenten“ rede, meine ich Instanzen von java.awt.Component, der Klasse, die direkt von Object abstammt und die Mutter aller GUI-Elemente ist, egal ob AWT wie schwergewichtig, heavy weight, HW – oder leichtgewichtiges Swing.

Die z-Reihenfolge der Peers von Javas GUI-Komponenten ist folgendermaßen definiert:

  1. Schwergewichtige Komponenten vor leichtgewichtigen
  2. Kinder vor Eltern
  3. Geschwister mit kleinerem Index weiter vorne

Beispiele:

  1. Panel.add(JButton): Panel-Peer im Vordergrund
  2. Panel.add(Button): Button-Peer im Vordergrund
  3. Panel.add(Button1); Panel.add(Button2): Button1-Peer im Vordergrund. Nur sichtbar, wenn sich button1 und button2 überlappen.

Die z-Reihenfolge der Komponenten innerhalb eines Containers (Regel 3) entspricht nichts anderem als der Reihenfolge derselben in der internen ArrayList, von der man eine Kopie über getComponents() bekommt. Oft ist das zwar auch gleichzeitig die x-y-Reihenfolge, in der der Layoutmanager die Komponenten nebeneinander anordnet, aber grundsätzlich können sämtliche Komponenten unabhängig von ihrer z-Reihenfolge auf dem Bereich des Containers positioniert werden und sich dabei auch überlappen.

Die Regeln 2 und 3 sind leicht zu realisieren, weil sich die daraus ergebende Sortierung direkt aus der Datenstruktur, welche die AWT-Komponenten bilden, ablesen lässt. Kopfzerbrechen bereitet Punkt 1, denn leicht- und schwergewichtige Komponenten können in dieser Datenstruktur bunt gemischt vorkommen, und es gilt die Peers der schwergewichtigen Komponenten immer im Vordergrund darzustellen. Davon bekommt der Anwender nichts mit, denn über Component.applyCompoundShape() bekommt jede schwergewichtige Komponente vorgeschrieben, welche Bereiche sie von sich aussparen muss, sodass leichtgewichtige Komponenten durchscheinen können und der Eindruck entsteht, die HW-Komponente befände sich mitten unter den leichtgewichtigen. Für den Anwender genügen also die Regeln 2 und 3; ihm wird vorgetäuscht, Regel 1 existiere nicht.

Wir werden uns im Folgenden nicht damit befassen, den richtigen CompoundShape für eine HW-Komponente zu finden, sondern wir wollen lediglich nachvollziehen, wie der Peer einer HW-Komponente seinen Platz in der z-Reihenfolge findet.

Zu Zeiten von Java 6 wurde das Problem noch so gehandhabt, dass beim Hinzufügen einer Komponente ein mehrmaliges Aufrufen von getParent() den jüngsten schwergewichtigen direkten Vorfahren ermittelte. Von ihm aus wurden dann per Rekursion alle schwergewichtigen Nachkommen zusammengetragen und anhand dieser Liste stellte eine native Methode die Ordnung wieder her.

Seit Java 7 hat sich das geändert. Um in jedem Fall den jeweils allernächsten schwergewichtigen Vorfahren ausfindig zu machen, werden nunmehr nicht nur die direkten Vorfahren, sondern auch deren sowie die eigenen Geschwister überprüft. Doch der Reihe nach.

Ein Component c werde einem Container p (wie parent) hinzugefügt, welcher schon viele weitere Components enthält und sich seinerseits in der ContentPane eines bereits sichtbar gemachten JFrames befindet:

  1: p.add(c);

Dabei wird nebst vielem anderen auch die Methode c.addNotify() aufgerufen – soweit ist noch alles beim Alten. Innerhalb von addNotify() hat sich der Code nun etwas verändert:

Java 6: c.parent.peer.restack();

Java 7: c.updateZOrder();

  1: public abstract class Component
  2: {
  3:     void updateZOrder() {
  4:         peer.setZOrder(getHWPeerAboveMe());
  5:     }
  6: }

Component.getHWPeerAboveMe() liefert den Peer der nächsthöheren schwergewichtigen Komponente. Das anschließende Ordnen ist gegen die Schnittstellenmethode ComponentPeer.setZOrder() programmiert, die man im sun.awt.NullComponentPeer oder dem sun.awt.windows.WComponentPeer implementiert findet.

  1: public abstract class WComponentPeer
  2: {
  3:     /**
  4:      * Handle to native window
  5:      */
  6:     protected volatile long hwnd;
  7:     /**
  8:      * Lowers this component at the bottom of the above component. If the above parameter
  9:      * is null then the method places this component at the top of the Z-order.
 10:      */
 11:     public void setZOrder(ComponentPeer above) {
 12:         long aboveHWND = (above != null) ? ((WComponentPeer)above).getHWnd() : 0;
 13:         setZOrder(aboveHWND);
 14:     }
 15:     private native void setZOrder(long above);
 16: }
 17: public class NullComponentPeer
 18: {
 19:     public void setZOrder(ComponentPeer above)
 20:     {
 21:     }
 22: }

Die Methode setZOrder() ist bei den NullComponentPeers, die von den Swing-Komponenten benutzt werden, als leerer Block implementiert – das Ergebnis der im Folgenden beschriebenen Sucherei wird also beim Hinzufügen einer leichtgewichtigen Komponente gar nicht verwendet. Trotzdem führt getHWPeerAboveMe() immer die vollständige Suche durch.

Wir gehen jetzt von der Annahme aus, dass im Moment alles in Ordnung ist, dass also sämtliche schwergewichtigen Peers vorne sind und alle leichtgewichtigen hinten. Wir müssen nur den Peer der neu hinzukommenden Komponente c an der richtigen Stelle einordnen. Angenommen, c ist schwergewichtig. Sein Peer muss also "vorgezogen" werden in die Gruppe der HW-Peers – aber zwischen welche beiden? Um das herauszufinden, gibt es zwei Möglichkeiten:

a) Man hangelt sich anhand von Regel 2 und 3 von der neuen Komponente c nach hinten durch, bis man eine schwergewichtige Komponente findet. Vor dem Peer dieser Komponente wird dann c.peer eingeordnet. Findet man keine schwergewichtige Komponente, so kommt c.peer hinter den hintersten HW-Peer und ist damit immer noch vor allen leichtgewichtigen.

b) Man hangelt sich anhand von Regel 2 und 3 von c nach vorne durch, bis man eine schwergewichtige Komponente findet und ordnet c.peer dann entsprechend hinter dem Peer dieser Komponente ein oder ganz vorne, wenn c die einzige HW-Komponente ist.

In Java ist letzteres implementiert. Zunächst wird ausfindig gemacht, welchen Index die Komponente c in p bekommen hat: p.getComponentZOrder(c), was in p.component.indexOf(c) resultiert. In unserem Beispiel wurde c einfach per p.add(c) eingefügt und hat somit den Index p.getComponentCount()-1.

Mittels diesem Index i erreicht man nun die Komponente, die in der z-Reihenfolge die nächste Komponente in Richtung Vordergrund ist: p.getComponent(i-1). Ist sie leichtgewichtig, wird p.getComponent(i-2) ausprobiert usw. bis zu p.getComponent(0), der vordersten Komponente des Containers p. Genauso wird dann noch mit jedem Vorfahren von p verfahren, bis man endlich auf eine schwergewichtige Komponente stößt oder aber auf ein Window, was bedeutet c.peer ist der einzige HW-Peer und kommt ganz nach vorne.

  1: public abstract class Component
  2: {
  3:     final int getSiblingIndexAbove()
  4:     {
  5:         Container parent = getParent();
  6:         if (parent == null) {
  7:             return -1;
  8:         }
  9:         return parent.getComponentZOrder(this) - 1;
 10:     }
 11:     final ComponentPeer getHWPeerAboveMe()
 12:     {
 13:         Container cont = getParent();
 14:         int indexAbove = getSiblingIndexAbove();
 15:         while (cont != null) {
 16:             for (int i = indexAbove; i > -1; i--) {
 17:                 Component comp = cont.getComponent(i);
 18:                 if (comp != null && !comp.isLightweight()) {
 19:                     return comp.getPeer();
 20:                 }
 21:             }
 22:             indexAbove = cont.getSiblingIndexAbove();
 23:             cont = cont.getParent();
 24:         }
 25:         return null;
 26:     }
 27: }

Wer nun viele Komponenten hintereinander mit p.add(c) hinzufügt, verursacht einen quadratischen Anstieg dieser Suchvorgänge, weil man so die neuen Komponenten immer hinten einfügt und dann jedes Mal alle vorherigen abklappern muss.

Wenn man keine maßgeschneiderten Überlappungen von Komponenten innerhalb eines Containers braucht, kann man die Suche abkürzen, indem man auf p.add(c,0) umsteigt. Die hinzukommenden Kopiervorgänge von p.component, der internen ArrayList vom Container p, fallen dabei nicht so sehr ins Gewicht.

Wer außerdem keine schwergewichtigen Komponenten in den Container p packen will, kann stattdessen folgendes Konstrukt einsetzen und umgeht damit sämtliche der beschriebenen Vorgänge für diesen Container.

  1: p = new Container()
  2: {
  3:     @Override
  4:     public int getComponentZOrder(Component comp) {
  5:         return 0;
  6:     }
  7: };

 

Ein ausgezeichneter Beitrag von Daniel Frisch. Danke! Und an der Stelle noch einmal der Hinweis, das ich in dem Blog gerne Gastbeiträge poste.

Über Christian Ullenboom

Ich bin Christian Ullenboom und Autor der Bücher ›Java ist auch eine Insel. Einführung, Ausbildung, Praxis‹ und ›Java SE 8 Standard-Bibliothek. Das Handbuch für Java-Entwickler‹. Seit 1997 berate ich Unternehmen im Einsatz von Java. Sun ernannte mich 2005 zum ›Java-Champion‹.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.