Jul 18 2010

Layoutmanager in Java

Ohne die gefürchteten Layout-Manager geht bei der Entwicklung von graphischen Benutzeroberflächen in Java herzlich wenig. Wer nicht gern auf die Arbeit mit entsprechenden GUI-Editoren zurückgreift, der steht schnell vor dem Problem, das zwar die Layoutmanager von Java zweckmäßig ihren Dienst verrichten, die Anordnung der einzelnen Komponenten innerhalb zum Beispiel eines Fenster eine echte Geduldsprobe ist. Daneben bläht sich der Quelltext schnell auf, da man gezwungen ist, die angezeigten Komponenten in weitere Komponenten einzubetten, um das gewünschte Gestaltungsziel zu erreichen. In jede Fall bekommt man schnell das Gefühl: Die Layoutmanager von Java machen einfach was sie wollen.
Gelegentlich sieht man Entwickler daher in die sogenannten “Null-Layouts” flüchten. Mit “Null-Layouts”  kann man zwar Position und Größe der einzelnen Elemente genau bestimmen, jedoch bleiben sie fix. Finden Größenänderungen an der aufnehmenden Komponente statt, wirken sich diese nicht auf die Komponenten aus, die innerhalb der Komponente platziert wurden. Es steht eben kein Layoutmanager mehr zur Verfügung, der die Herrschaft über das Desing übernimmt. Diese Überlegungen führten mich im Rahmen meines Programmierpraktikums dazu, mich ein wenig mit der Entwicklung von Layoutmanagern zu befassen.

Ich für meinen Teil finde die Lösung mit den Null-Layouts eigentlich ziemlich reizvoll, da sie eine einfache und übersichtliche Möglichkeit bieten Komponenten auf dem Bildschirm zu platzieren. Insofern trug mich der Gedanke zu einem Layoutmanager, der alle Objekt anfangs über die setBounds()-Methode platziert und ausrichtet, im Falle einer Größenänderung der umgebenen Komponente jedoch in der Lage ist, die Verhältnisse neu anzupassen. Nach Möglichkeit der Gestalt, das die ürsprünglichen Größen- und Abstandsverhältnisse beibehalten werden. Eine passende Lösung habe ich mittlerweile gefunden und will hier kurz beschreiben, wie das Implementieren eines entsprechenden Layoutmanagers in Java von statten geht.

In Java werden die Layoutmanager als Strategie zur Gestaltung von Anzeigeelement aufgefaßt und als Interface realisiert. Es gibt zwei Typen von Layoutmanagern (LayoutManager und LayoutManager2), die wir für die Implementierung heranziehen können. LayoutManager2 ist dabei von LayoutManager abgeleitet und bietet mehr Methoden an, bzw. erfordert vom Entwickler mehr Methoden zu implementieren. Will man einen Layoutmanager implementieren, so realisiert man eine Klasse und läßt diese per Implements-Statement von den LayoutManagern “erben”. Durch das Ausarbeiten der einzelnen Methode definiert man dann das Verhalten des Managers.

Der Kern beider Layoutmanager bildet die Methode “layoutComponent()“. Diese Methode bekommt eine Referenz auf jene Komponente übergeben, für das der Layoutmanager ein Layout berechnen soll. Vorher muß der Manager natürlich wissen, welche Komponenten er da eigentlich verwalten soll. Zu diesem Zweck muss der Manager buch über alle Komponenten führen, die einem Design hinzufügt wurden. Dafür ist die addLayoutComponent()-Methode zuständig, die offenkundig innerhalb der add()-Methode aufgerufen wird, über die jede von Container abgeleitete Klasse verfügt. Für die Entwicklung eines Layout-Managers, der initial mit Bounding-Boxes arbeiten soll, müssen wir jeder Komponente weitere Informationen mitgeben. Namentlich sind das die Angabe, wo sie im Anzeigebereich mit welchen Abmessungen platziert werden soll. Das Problem hier ist, das die add()-Methode neben der Übergabe der eigentlich Komponente lediglich ein weiteres Objekts als Parameter zuläßt (in der Dokumentation “Constrains” genannt). Ist die Komponente dem Layout hinzugefügt, können wir jedoch nicht mehr so einfach darauf zugreifen, um Einstellungen darauf vorzunehmen. Wir brauchen also ein Objekt (diesmal auch vom Typ Object) über die wir diese Informationen an den Layoutmanager herantragen können. Da es keine Standardklasse gibt (bzw. ich keine gefunden habe), die unsere Anforderungen erfüllt, müssen wir sie selbst implementieren. Wir erzeugen also eine Klasse und haben wir damit unseren Informationsträger, den wir der add()-Methode übergeben können. Per Default erben schließlich alle Klassen in Java von Object.  Die so erzeugten Objekte enthalten die absolute Position innerhalb des darstellenen Komponente, sowie deren Höhe und Breite als Attribute und stellen Methode bereit, um diese zu verändern. Für die Implementierung sind es reine “Wegwerf”-Objekte, die direkt im Aufruf der add()-Methode annonym erzeugt werden und vom Garbage-Collector wieder eingesammelt werden können, sobald die entsprechenden Informationen beim Layoutmanager angekommen und ausgelesen wurden. Hier der komplette Quelltext der Klasse:


public class Bounds
{
	private int x, y, width, height;

	public Bounds(int x, int y, int width, int height)
	{
		this.x = x;		// x Position
		this.y = y;		// y Position
		this.width = width;	// Breite
		this.height = height;	// Höhe
	}

	// Getter
	public int getX()
	{
		return x;
	}
	public int getY()
	{
		return y;
	}
	public int getWidth()
	{
		return width;
	}
	public int getHeight()
	{
		return height;
	}
}

Objekte dieses Typs werden die benötigten Informationen direkt im Konstruktor übergeben. Die Werte werden gespeichert und können dann vom Layoutmanager über die Getter genutze werden, um die Komponente zu platzieren. Man sieht: Eine recht einfache Klasse, die uns nur als Informationsträger dient.

Für die Verwaltung unserer Komponenten reichen diese beiden Klassen allerdings noch nicht aus. Unsere Layoutstrategie orientiert sich an der ursprünglichen Position aller Komponenten innerhalb der darstellenden Komponente und soll die Verhältnisse, die jede Komponente in Relation zu Höhe und Breite der umgebenen Komponente hatte, beim Neuberechnen des Layouts beibehalten (Den Satz kann man übrigens gern dreimal lesen…). Es reicht also nicht, wenn der Layoutmanager nur eine Referenz auf die verwalteten Komponente und deren ursprüngliche Position speichert, auch die Verhältnisse im ursprünglichen Anzeigebereich muß festgehalten werden. Zu diesem Zweck wird eine dritte Klasse benötigt. Die dritte Klasse speichert eine Referenz auf die verwaltete Komponente selbst und gleichzeitig für X und Y Koordinate, sowie für Höhe und Breite einen double Wert, der das Verhältnis von Höhe, Breite und Position in Relation zur aufnehmenden Komponente speichert. Hier einmal der Quelltext:


public class LayoutManagerComponent
{
	Component myComponent;
	double relativeX, relativeY;
	double relativeHeight, relativeWidth;
	double relativeFontSize;		

	public LayoutManagerComponent(Component comp)
	{
		myComponent = comp;
		relativeX = 0.0;
		relativeY = 0.0;
		relativeHeight = 0.0;
		relativeWidth = 0.0;
		relativeFontSize = 0.0;
	}
	// Setters
	public void setRelativeX(double relativeX)
	{
		this.relativeX = relativeX;
	}
	public void setRelativeY(double relativeY)
	{
		this.relativeY = relativeY;
	}
	public void setRelativeHeight(double relativeHeight)
	{
		this.relativeHeight = relativeHeight;
	}
	public void setRelativeWidth(double relativeWidth)
	{
		this.relativeWidth = relativeWidth;
	}
	public void setRelativeFontSize(double relativeFontSize)
	{
		this.relativeFontSize = relativeFontSize;
	}

	// Getters
	public Component getComponent()
	{
		return myComponent;
	}
	public double getRelativeX()
	{
		return relativeX;
	}
	public double getRelativeY()
	{
		return relativeY;
	}
	public double getRelativeHeight()
	{
		return relativeHeight;
	}
	public double getRelativeWidth()
	{
		return relativeWidth;
	}
	public double getRelativeFontSize()
	{
		return relativeFontSize;
	}
}

Wie man sieht, habe ich etwas gelogen. Diese Klasse speichert neben der relativen Position und relativen Größe auch einen Wert, der die relative Schriftgröße angibt. In der Tat ist auch die Größe der Schrift eine Aufgabe, die der Layoutmanager im Zweifel übernehmen muß, sonst passen sich zwar die Komponenten, die Schrift aufnehmen, der umgebenen Komponente an, der angezeigte Text bleibt jedoch unverändert. In jedem Fall sieht man auch hier, das die Klasse ein reiner Informationsträger ist und keinen Code enthält, der mehr mach als die Attribute zu verändern und diese von Außen zugänglich zu machen. Die Implementierung dieser Klasse erfolgt nun in Objekten des Layoutmanagers selbst. Werfen wir einen Blick auf den Quellcode des Layoutmanagers:



public class RelativeScaleAndPlacementLayout implements LayoutManager2
{
	ArrayList compsToCalc;
        double firstWidth, firstHeight;	

	public RelativeScaleAndPlacementLayout()
	{
		firstWidth = -1;
		firstHeight = -1;
		compsToCalc = new ArrayList();
	}
	public void addLayoutComponent(Component toAdd, Object bounds)
	{
		// Wir müssen erst casten.
		Bounds theBounds = (Bounds) bounds;
		// Anschließend Fonts ermitteln und dessen Größe speichern.
		// Die Relative Größe wird anhand der Höhe des Containers ermittelt.
		if (toAdd.getFont() != null)
		{
			Font tmpFont = toAdd.getFont().deriveFont(0, theBounds.getHeight() / 2);
			toAdd.setFont(tmpFont);
		}
		// Anschließend Platzieren.
		toAdd.setSize(theBounds.getWidth(), theBounds.getHeight());
		toAdd.setLocation(theBounds.getX(), theBounds.getY());
		compsToCalc.add(new LayoutManagerComponent(toAdd));
	}
	public Dimension maximumLayoutSize(Container parent)
	{
		int x, y;
		x = 0;
		y = 0;
		for (LayoutManagerComponent comp: compsToCalc)
		{
			y = y + comp.getComponent().getHeight();
			x = x + comp.getComponent().getWidth();
		}
		return new Dimension(x,y);
	}
	public void layoutContainer(Container parent)
	{
		Bounds tmpBs = new Bounds(0,0,0,0);
		float tmpFontSize = 0;
		Font tmpFont;
		// Wurde dieses Objekt schon initialisiert?
		if (firstWidth == -1 || firstHeight == -1)
		{
			// Nein, also speichern wir die augenblickliche Größe.
			firstHeight = parent.getHeight();
			firstWidth = parent.getWidth();
		}
		for (LayoutManagerComponent comp: compsToCalc)
		{
			if (comp.getRelativeX() == 0.0 && comp.getRelativeY() == 0.0)
			{
				comp.setRelativeX(comp.getComponent().getLocation().x / firstWidth);
				comp.setRelativeY(comp.getComponent().getLocation().y / firstHeight);
			}
			if (comp.getRelativeHeight() == 0.0 && comp.getRelativeWidth() == 0.0)
			{
				comp.setRelativeHeight(comp.getComponent().getHeight() / firstHeight);
				comp.setRelativeWidth(comp.getComponent().getWidth() / firstWidth);
			}
			if (comp.getRelativeFontSize() == 0.0)
			{
				comp.setRelativeFontSize(comp.getComponent().getFont().getSize2D() / firstHeight);
			}
			// Anschließend berechnet die neuen Bounds...
			tmpBs.setX((int)(parent.getWidth() * comp.getRelativeX()));
			tmpBs.setY((int)(parent.getHeight() * comp.getRelativeY()));
			tmpBs.setHeight((int)(parent.getHeight() * comp.getRelativeHeight()));
			tmpBs.setWidth((int)(parent.getWidth() * comp.getRelativeWidth()));
			// ...und die neue Schriftgröße...
			tmpFontSize = (float) (parent.getHeight() * comp.getRelativeFontSize());
			tmpFont = comp.getComponent().getFont().deriveFont(tmpFontSize);
			// ...dann wende sie auf das betrachtete Objekt an.
			comp.getComponent().setBounds(tmpBs.getX(), tmpBs.getY(), tmpBs.getWidth(), tmpBs.getHeight());
			comp.getComponent().setFont(tmpFont);
		}
	}
	public Dimension minimumLayoutSize(Container parent)
	{
		return maximumLayoutSize(parent);
	}
	public Dimension preferredLayoutSize(Container parent)
	{
		return maximumLayoutSize(parent);
	}
	public void removeLayoutComponent(Component comp)
	{
		for (LayoutManagerComponent comps : compsToCalc)
		{
			if (comps.getComponent() == comp)
			{
				compsToCalc.remove(comps);
				return;
			}
		}
	}
	public void addLayoutComponent(String name, Component toAdd)
	{
	}
	public float getLayoutAlignmentX(Container parent)
	{
		return 0;
	}
	public float getLayoutAlignmentY(Container parent)
	{
		return 0;
	}
	public void invalidateLayout(Container parent)
	{
	}
}

Gehen wir mal durch was diese Klasse tut. Es ist nicht viel. Eines der wichtigsten Attribute des Layoutmanages ist die ArrayList, die mit “LayoutManagerComponent” parametrisiert ist. Innerhalb des Layoutmanagers werden also alle Komponenten in einem Objekt vom Typ der zuvor besprochenen Klasse verwaltet. Wird dem Manager eine Komponente über die addLayoutComponent()-Methode hinzugefügt, wird die übergebenen Komponente zunächst auf die entsprechenden Abmessungen und Koordinaten eingestellt und ein neues LayoutManagerComponent-Objekt erzeugt, das die hinzugefügte Komponenten im Konstruktor als Parameter erhält und der ArrayList “compsToCalc” hinzugefügt wird. Im Konstruktor des LayoutManagerComponent-Objekts geschieht nichts weiter, als das die Referenz auf die zu verwaltende Komponente gespeichert wird und alle sonstigen Attribute auf 0.0 eingestellt werden. Damit sind zwar die Objekte initialisiert, aber noch nicht die richtigen Werte ermittelt und eingestellt. Diese Aufgabe wird in der layoutContainer()-Methode nachgeholt. Die layoutContainer()-Methode ist das Herzstück eines jeden Layoutmanagers. Diese Methode durch das Eventsystem jedesmal dann aufgerufen, wenn das Container-Objekt neugestaltet werden muß, da sich zum Beispiel die Größe geändert hat oder das Objekt zuvor verdeckt war und nun neu gezeichnet werden muß.

Wird die layoutContainer()-Methode dieses Layoutmanagers das erste mal aufgerufen (in der Regel also, wenn das Objekt zum ersten Mal auf dem Bildschirm angezeigt wird), so stehen noch die beiden Attribute “firstWidth” und “firstHeight” auf -1. Daran erkennt die Methode, das für diese Layoutmanager-Objekt noch keine initialen Werte gespeichert wurden und holt dies nach. Anschließend wird für jedes Objekt innerhalb der ArrayList geprüft, ob bereits die Werte für die relative Postion und Größe, sowie die relative Schriftgröße gespeichert wurden. Ist dies nicht der Fall, so werden diese Werte jetzt ermittelt und stehen ab dann für die Platzierung der Elemente zur Verfügung. Als Verhältnis wird einfach die aktuelle Position und Größe geteilt durch die initiale Höhe und Breite der Komponente angenommen. Die jeweiligen absoluten Positionen werden anhand der neuen Abmessungen des aufnehmenden Containers ermittelt, indem die entsprechenden Werte mit den relativen Werten multipliziert werden. Dieses Vorgehen wird auf alle Komponenten innerhalb der ArrayList angewandt und dies bei jedem Aufruf von layoutContainer.

Alle weiteren Methoden sind lediglich zier. Durch das LayoutManager-Konzept von Java sind sie zwingendermaßen zu implementieren und machen bei näherer Betrachtung auch Sinn. Für meine Fall waren sie aber einigermaßen bedeutungslos, auch wenn hier die ein oder andere Methode dennoch implementiert wurde.  Näheres dazu entnimmt man jedoch am Besten der Java-Dokumentation.

Softwareentwicklung ist eine komplizierte Sache, ich hoffe dennoch, ich konnte die Arbeit von Layoutmanagern in Java etwas veranschlaulichen. Wer sich den Quelltext der drei Klassen mal mit Kommentaren anschauen möchte, kann die Dateien unten herunterladen.

Bounds.java
Manager.java
ManagerComponent.java