DiagonalLayout

Aus Byte-Welt Wiki
Version vom 2. April 2018, 16:04 Uhr von L-ectron-X (Diskussion | Beiträge)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Zur Navigation springenZur Suche springen

Theorie

Es besteht neben den vordefinierten LayoutManagern der JRE-Standartbibliothek und externen APIs natürlich die Möglichkeit, eigene Manager zu entwickeln. Jeder, der schon einmal mit "Null-Layout" gearbeitet hat, weiß, dass Components mit den Methoden setLocation() und setSize() bzw. gleichzeitig per setBounds() ihre Position und Größe vorgegeben werden. Der LayoutManager macht das Selbe mit dem Unterschied, dass er von den Komponenten auf dem er sitzt entkoppelt ist und so leicht wiederverwendet werden kann. Etwas ähnliches könnte man eigendlich nur mit einem ComponentListener erreichen, der dafür bei Weitem nicht optimal ist.

Um einen LayoutManager zu schreiben, gibt es prinzipiell 2 Wege:
Man implementiert das Interface LayoutManager oder LayoutManager2. Dabei wird LayoutManager benutzt, falls das Hinzufügen ohne Parameter stattfindet. Ein Beispiel wäre das FlowLayout, welches einfach der Reihe nach anordnet. Diese holen sich die Components direkt vom Parent, da sie keine weiteren Informationen abspeichern müssen. Hingegen wird LayoutManager2 benutzt, falls ein Parameter (genannt Constraints) wärend des Hinzufügens übergeben werden soll. Ein Beispiel wäre das BorderLayout, welches mit 5 Strings arbeitet, um die Position des Components anzugeben. Für den Parent des LayoutManagers spielt es keine Rolle, welches Interface benutzt wird. Es wird meistens von dem Component auf dem das Layout sitzt geprüft, um welches Interface es sich handelt und so die demensprechende Action durchgeführt. LayoutManager2 erweitert LayoutManager und hat so auch Methoden, die es eigendlich nicht benötigt.

Methoden von LayoutManager

LayoutManager definiert folgende Methoden:

addLayoutComponent(String, Component)

Es gibt bei Containern allgemein 5 add()-Methoden für Components.

add(Component)
add(Component, int)
add(Component, Object)
add(String, Component)
/*Diese Methode wird aufgerufen, sobald ein Component dem Parent mit einen der letzten 3 Methoden hinzugefügt wird und es sich nicht um einen '''LayoutManager2''' handelt.*/
add(Component, String, Object)
/*Wird aufgerufen, sobald ein Component von den Parent entfernt wird.*/
removeLayoutComponent(Component)
/*Gibt die optimale Größe des Parents wieder.*/
preferredLayoutSize(Container)

Diese kann extern für jeden Component gesetzt werden, besitzt er jedoch einen LayoutManager (und wird sie nicht extra gesetzt) wird die Methode des LayoutManagers verwendet. Z. B. benutzen JScrollPanes die Größe, um den Bescrollbaren Bereiches festzustellen. JLabels geben z. B. die benötigte Größe für den Text oder das Bild (oder gegebenfalls beides) zurück. Grundsätzlich ist die preferredLayoutSize die, nach der man sich bei der Anordnung der Komponenten richtet.

/*Gibt die minimale Größe des Parents zurück. Da es meistens keinen Grund gibt die Größe zu beschränken ist das meistens 0/0.*/
minimumLayoutSize(Container)
/*Die wohl wichtigste Methode. Hier werden die Components neu angeordnet.*/
layoutContainer(Container)


Methoden von LayoutManager2

LayoutManager2 erweitert wie oben bereits gesagt LayoutManager und besitzt damit die selben Methoden, zuzüglich dieser:

/*Diese add()-Methode wird statt '''addLayoutComponent(String, Component)''' aufgerufen, wenn es sich um einen '''LayoutManager2''' handelt und ein 
Component hinzugefügt wird (egal welche der oben genannten Methoden). In Object steht der Constraint des add-Aufrufes oder gegebenfalls <code>null</code>.*/
addLayoutComponent(Component, Object)
/*Gibt die maximale Layout-Größe an. Oft wird einfach <code>Integer.MAX_VALUE</code> verwendet.*/
maximumLayoutSize(Container)
/*Gibt die Ausrichtung der X-Achse des Component wieder. 0 bedeutet die "normale" Ausrichtung, 0.5 zentriert und 1 am weitesten von der "normalen" Ausrichtung entfernt.*/
getLayoutAlignmentX(Container)
/*Genauso wie '''getLayoutAlignmentX()''', nur natürlich über die Y-Achse.*/
getLayoutAlignmentY(Container target)
/*Wird immer aufgerufen, wenn der Parent als invalid gekenntzeichnet wird, also es einer neuen Anordnung bedarf.*/
invalidateLayout(Container target)

Praxis - LayoutManager

Ich habe mich für einen LayoutManager entschieden, der die Compontents diagional anordnet (DiagonalLayout). Das heißt, der nächste Component wird genau ein Pixel weiter unten/rechts als die untere rechte Ecke des vorherigen platziert. Da dieser Manager keine Constraints abspeichert, implementieren wir das Interface LayoutManager . Da die add()-Methode des LayoutManagers nur in seltensten Fällen sinnvoll ist, holen wir die Components direkt vom Parent.

Zuerst müssen wir einmal wissen, was genau geschrieben werden soll. Wir überlegen also, was in den verschiedenen Methoden wie gemacht werden soll:

addLayoutComponent(String, Component)

Da wir keinen String oder Constraints übergeben, wird dazu eigendlich nur add(Component) verwendet. Da die Methode da nicht aufgerufen wir implementieren wir sie leer.


removeLayoutComponent(Component)

Da nichts gespeichert wird, muss auch nichts entfernt werden, wenn ein Component vom Parent entfernt wird. Daher wird diese Methode ebenfalls leer implementiert.


layoutContainer(Container)

Hier werden wie gesagt die Components angeordnet. Dazu wird zuerst eine Variable definiert, um die aktuelle Position der Anordnung zu speichern. Danach werden die Components des Parents durchgelaufen, an die aktuelle Position gesetzt und die Position wird um die PreferredSize nach rechts unten verschoben.


preferredLayoutSize(Container)

Die Komponenten und ihre Abstände sind hier fix zueinander. Deshalb ist die optimale Größe des Parent einfach die addierte optimale Höhe und Breiter aller Childs + der Rahmen (Auf diesen darf man nicht vergessen).


minimumLayoutSize(Container)

Da ich keinen Grund sehe, eine minimale Größe festzulegen, wird hier 0/0 zurückgegeben.


Legen wir uns mal die Klasse an. In meinen Beispiel im Package org.javaforum.tutorials.layoutmanager, und implementieren gleich die sowieso klaren Methoden:

    package org.javaforum.tutorials.layoutmanager;
     
    import java.awt.Component;
    import java.awt.Container;
    import java.awt.Dimension;
    import java.awt.LayoutManager;
     
    public class DiagionalLayout implements LayoutManager {
        public void layoutContainer(Container parent) {
           
        }
        public Dimension preferredLayoutSize(Container parent) {
            return null;
        }
       
     
        public Dimension minimumLayoutSize(Container parent) {
            return(new Dimension(0, 0));
        }
        public void addLayoutComponent(String name, Component comp) {}
        public void removeLayoutComponent(Component comp) {}
    }

Ok. Nun müssen wir die Components erstmal anordnen. Als erstes (was ich allzu gerne vergesse) ist die Breite des Rahmens. Auch mit Rahmen ist 0/0 ganz links oben (bei normaler Ausrichtung), die Components würde als über den Rahmen gezeichnet werden. Die Größe des Rahmens wird in den Insets genannten Objekt von einem Component geliefert:

    public void layoutContainer(Container parent) {
        Insets insets = parent.getInsets();
    }

Als nächstes brauchen wir natürlich eine Variable, die die aktuelle Position speichert. Diese beginnt direkt nach dem Rahmen:

    public void layoutContainer(Container parent) {
        Insets insets = parent.getInsets();
        Point offs = new Point(insets.left, insets.top);
    }

Ein Point ist einfach ein Wrapper für 2 int's, in der Regel gibt er einen Punkt auf einem 2-Dimensionalen Raster an. (Die Variablen sind public und heißen x und y.)

Nun müssen wir in einer kleinen Schleife nur alle Components durchgehen und an die linke untere Ecke des letzten Components setzen. Dazu müssen wir nur die Größe auf die eigene bevorzugte Größe setzen und diese zu den Koordinaten (offs) addieren. Außerdem überspringen wir unsichtbare Components, da sonst Lücken entstehen würden.

    public void layoutContainer(Container parent) {
        Insets insets = parent.getInsets();
        Point offs = new Point(insets.left, insets.top);
       
        for(int i = 0, size = parent.getComponentCount(); i < size; i++) {
            Component c = parent.getComponent(i);
            if(!c.isVisible()) { //Unsichtbare Componenten überspringen.
                continue;
            }
           
            //Position auf das Offset setzen.
            c.setLocation(offs.x, offs.y);
           
            Dimension prefSize = c.getPreferredSize();
     
            //Auf die vom Component gelieferte optimale Größe setzen.
            c.setSize(prefSize);
           
            //Position verändern
            offs.x+= prefSize.width;
            offs.y+= prefSize.height;
        }
    }

Als letztes müssen wir nur noch preferredLayoutSize richtig implementieren. Wir verwenden dafür die selbe Schleife, mit dem Unterschied, dass wir nur die Größe hochzählen und nichts anordnen.

    public Dimension preferredLayoutSize(Container parent) {
        Insets insets = parent.getInsets();
        Dimension parentPrefSize = new Dimension(insets.left + insets.right, insets.top + insets.bottom);
       
        for(int i = 0, size = parent.getComponentCount(); i < size; i++) {
            Component c = parent.getComponent(i);
            if(!c.isVisible()) {
                continue;
            }
           
            Dimension prefSize = c.getPreferredSize();
           
            parentPrefSize.width+= prefSize.width;
            parentPrefSize.height+= prefSize.height;
        }
        return(parentPrefSize);
    }

Statt einen Point verwenden wir nun eine Dimension , da wir die Größe und keine Position speichern wollen. Außerdem addieren wir zu der optimalen Größe noch die untere Höhe und rechte Breite des Rahmens hinzu.

Nun schreiben wir uns noch eine kurze main()-Methode, um das Layout auch testen zu können:

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                JFrame frame = new JFrame("DiagionalLayout Test");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new DiagionalLayout());
               
                frame.add(new JLabel("Hello"));
                frame.add(new JLabel("World"));
                frame.add(new JLabel("We"));
                frame.add(new JLabel("are"));
               
                JLabel label = new JLabel("all"); //Ein unsichtbarer Component als Test
                label.setVisible(false);
                frame.add(label);
               
                frame.add(new JLabel("diagional"));
                frame.add(new JLabel("located"));
               
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

Durch die pref. Size des ContentPanes setzt das JFrame auch die richtige Größe, und zeigt wie erwartet alle Components in einer diagionalen Linie an:

DiagonalLayout.png

Dadurch, dass der Text breiter als hoch ist wirkt es eher wie eine Treppe, aber genau so habe ich es ja auch geschrieben. Ansonst könnte ich einfach den höheren der beiden Größenangaben werden und diese verwenden:

    int j = Math.max(prefSize.width, prefSize.height);
    offs.setLocation(offs.x + j, offs.y + j);

Aber da es sich um mathematische Grundlagen handelt und solange es nicht wirklich zu kompliziert wird (sollte das der Fall sein denkt darüber nach, ob sich 2 verschaltete Components mit unterschiedlichen Layouts nicht besser eignen würden) sollte es jeden einfach fallen, sein Layout seinen Wünschen anzupassen.

Praxis - LayoutManager2

Jetzt noch ein LayoutManager , der das Interface LayoutManager2 implementiert und damit auch Parameteter übergeben werden können. Dafür werden wir einen LayoutManager schreiben, der wie das FlowLayout anordnet. Der Unterschiede ist, dass Component en sowohl links als auch rechts angeordnet werden können. Der einfach heit halber verzichten wir aber auf Dinge wie Zeilenumbruch und lassen bei zu kurzen Parent Component s einfach überlappen, damit der Code als Anschauungsmaterial übersichtlich bleibt. Ich gebe ihm den Namen LeftRightFlowLayout. Nicht sehr einfallsreich, ich weiß.

Zuerst wieder die Klasse und LayoutManager2 implementieren.

Folgende Methoden enthalten die Hauptimplemention:

addLayoutComponent(Component, Object)
removeLayoutComponent(Component)
preferredLayoutSize(Container)
layoutContainer(Container)

Folgende Methoden werden leer oder mit Standartrückgabewerten implementiert:

addLayoutComponent(String, Component)
invalidateLayout(Container)
getLayoutAlignmentX(Container)
getLayoutAlignmentY(Container)
maximumLayoutSize(Container)
minimumLayoutSize(Container)


Als Speicherung der Component en sehe ich 2 LinkedList s vor. Jeweils eine für die linken und für die rechten Component s. Als Abstand zwischen ihnen definiere ich einen Standart von 2, kann allerdings auch per Constructor gesetzt werden. y ist immer die Höhe des Rahmens. Um die Position (Links oder Rechts) anzugeben verwende ich 2 simple Object e. Ist keines davon angegeben, wird standartgemäß links angeordnet.

Unsere Klasse sieht nun fürs Erste so aus:

    package org.javaforum.tutorials.layoutmanager;
     
    import java.awt.Component;
    import java.awt.Container;
    import java.awt.Dimension;
    import java.awt.LayoutManager2;
    import java.util.LinkedList;
    import java.util.List;
     
    public class LeftRightFlowLayout implements LayoutManager2 {
        public static final Object LEFT = new Object();
        public static final Object RIGHT = new Object();
       
        private static final Object DEFAULT_ALIGN = LEFT;
       
        private static final int DEFAULT_X_GAP = 2;
       
        private int xGap;
       
        private List<Component> leftComponents;
        private List<Component> rightComponents;
       
        public LeftRightFlowLayout() {
            this(DEFAULT_X_GAP);
        }
        public LeftRightFlowLayout(int xGap) {
            this.xGap = xGap;
           
            leftComponents = new LinkedList<Component>();
            rightComponents = new LinkedList<Component>();
        }
       
        public void addLayoutComponent(Component comp, Object constraints) {
            // TODO Auto-generated method stub
        }
        public void removeLayoutComponent(Component comp) {
            // TODO Auto-generated method stub
        }
        public Dimension preferredLayoutSize(Container parent) {
            // TODO Auto-generated method stub
            return null;
        }
        public void layoutContainer(Container parent) {
            // TODO Auto-generated method stub
        }
     
       
        public void addLayoutComponent(String name, Component comp) {}
        public void invalidateLayout(Container target) {}
       
        public float getLayoutAlignmentX(Container target) {
            return 0;
        }
        public float getLayoutAlignmentY(Container target) {
            return 0;
        }
        public Dimension maximumLayoutSize(Container target) {
            return(new Dimension(0, 0));
        }
        public Dimension minimumLayoutSize(Container parent) {
            return(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE));
        }
    }

Widmen wir uns nun den Hinzufügen der Component en. Eigendlich müssen wir nur prüfen, ob es sich um eins unserer 2 Objekte handelt.

    public void addLayoutComponent(Component comp, Object constraints) {
        if(constraints == LEFT) {
            leftComponents.add(comp);
        } else if(constraints == RIGHT) {
            rightComponents.add(comp);
        } else {
            addLayoutComponent(comp, DEFAULT_ALIGN);
        }
    }

Sehr simpel. Ist das Object für Links angegeben wird es der linken Liste hinzugefügt wenn das rechte Object angegeben wurde der rechten Liste und wenn nicht wird die Methode erneut mit dem Wert für die linke Liste (Standart-Wert) aufgerufen. Hier müssen wir nur etwas vorsichtig sein, wenn z. B. DEFAULT_ALIGN weder LEFT noch RIGHT ist gibt es eine Endlosschleife und einen StackOverflow .

removeLayoutComponent wird noch einfacher, wir entfernen den Component aus der linken Liste und wenn es darin nicht vorhanden ist aus der rechten.

    public void removeLayoutComponent(Component comp) {
        if(!leftComponents.remove(comp)) {
            rightComponents.remove(comp);
        }
    }

Als nächstes folgt die Anordnung der Componenten.

Ich schreibe einfach 2 Schleifen die die Component en anordnet. Zwar ließe sich das sicher durch Methoden abstrahieren, allerdings denke ich, dass der Code dann schwerer zu lesen ist.

    public void layoutContainer(Container parent) {
        Insets insets = parent.getInsets();
        Point offs = new Point(insets.left, insets.top);
       
        for(Component c:leftComponents) {
            if(!c.isVisible()) {
                continue;
            }
           
            Dimension prefSize = c.getPreferredSize();
            c.setSize(prefSize);
            c.setLocation(offs.x, offs.y);
           
            offs.x+= prefSize.width + xGap;
        }
       
        offs = new Point(parent.getWidth() - insets.right, insets.top);
        for(Component c:rightComponents) {
            if(!c.isVisible()) {
                continue;
            }
           
            Dimension prefSize = c.getPreferredSize();
            c.setSize(prefSize);
            c.setLocation(offs.x - prefSize.width, offs.y);
     
            offs.x-= (prefSize.width + xGap);
        }
    }

Die erste Schleife ist im Grunde genau das Selbe, wie wir bereits oben hatten. Component en bekommen nacheinander ihre optimale Größe, werden an die Position des Offsets positioniert, das offs bewegt sich um die Größe + den Abstand und der nächste Component kommt an die Reihe.

Genauso ist die untere Schleife, mit dem Unterschied, dass hier das Offset an der Breite des Parents beginnt und die Position immer abgezogen wird, um das Offset nach links zu verschieben.

Als letztes müssen wir noch die optimale Größe errechnen. Dazu benutzen wir wieder eine modifizierte Version der Layout -Schleifen. Wir zählen einfach die Breite sämtlicher Component s zusammen. Da wir die Component s nur in einer Reihe anordnen benötigen wir nur die Höhe des höchsten Component (+ Rahmenhöhe). Dafür verwenden wir einfach die statische Methode max in Math, die die größere Zahl der beiden Parameter zurückliefert.

    public Dimension preferredLayoutSize(Container parent) {
        Insets insets = parent.getInsets();
        int borderHeight = insets.top + insets.bottom;
        Dimension parentPrefSize = new Dimension(insets.left + insets.right, borderHeight);
       
        for(Component c:leftComponents) {
            Dimension prefSize = c.getPreferredSize();
            parentPrefSize.width+= (prefSize.width + xGap);
            parentPrefSize.height = Math.max(parentPrefSize.height, prefSize.height + borderHeight);
        }
       
        for(Component c:rightComponents) {
            Dimension prefSize = c.getPreferredSize();
            parentPrefSize.width+= (prefSize.width + xGap);
            parentPrefSize.height = Math.max(parentPrefSize.height, prefSize.height + borderHeight);
        }
        return(parentPrefSize);
    }

Nun noch eine kurze Main zum testen:

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                JFrame frame = new JFrame("LeftRightFlowLayout Test");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new LeftRightFlowLayout(15));
               
                frame.add(new JLabel("Links A"), LeftRightFlowLayout.LEFT);
                frame.add(new JLabel("Rechts A"), LeftRightFlowLayout.RIGHT);
                frame.add(new JLabel("Links B"), LeftRightFlowLayout.LEFT);
                frame.add(new JLabel("Links C"));
                frame.add(new JLabel("Rechts B"), LeftRightFlowLayout.RIGHT);
                frame.add(new JLabel("Rechts C"), LeftRightFlowLayout.RIGHT);
               
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

Lrfl.png

Tipp: Falls eure LayoutManager2 eine Zahl als Parameter benötigt (wie das LayeredPane) denkt daran, Objekte (z. B. Integer.valueOf(int)) zu benutzen, da ihr sonst die Höhe des Components und keinen Constraint übergebt.


Quellen und Weiterführendes Material

--Volvagia (06.11.2012, 04:01 Uhr)