Inhalt |
|
|
|
|
|
|
|
|
|
|
|
|
Wir haben bisher rein text-basierte Programme geschrieben. Nun wollen wir Grafiken erzeugen. Dies geht in Java auf zweierlei Arten
Applets werden wir später einführen. Zunächst bleiben wir, wie bisher, bei selbständigen Programmen (Applikationen). Es ist nicht schwer, ein Grafikfenster zu öffnen.
import java.awt.*; public class Test { public static void main (String args[]) { Frame F=new Frame(); F.setSize(300,300); F.setVisible(true); } }
Das Programm zeigt lediglich ein leeres Fenster. Außerdem lässt sich das Fenster noch nicht wie gewohnt schließen, und das Programm lässt sich daher nicht wie gewohnt beenden. Man muss es durch CTRL-C abbrechen. (Dazu muss die Komanndozeile zuerst durch einen Mausklick nach vorne geholt werden).
Die Klasse Frame arbeitet also mit dem Betriebssystem zusammen, um ein Fenster zu erzeugen. Dazu erzeugt sie einen sogenannten Peer. Dieses Objekt funktioniert für jedes System ein wenig anders. Es erzeugt die Grafikausgabe und nimmt die Ereignisse entgegen.
Für den Java-Programmierer ist lediglich eine Instanz von Frame zu erzeugen. Dann kann er noch die gewünschte Größe einstellen. Das Fenster erscheint erst, wenn setVisible(true) aufgerufen wird.
Die Erzeugung des Fensters geschah im obigen Beispiel mit dem sogenannten AWT (Abstract Windowing Toolkit). Wie erwähnt, verwendet das AWT für alle Elemente Peers, also Objekte, die die Verbindung zum Betriebssystem herstellen. Es stellte sich allerdings heraus, dass es einfacher und kompatibler wäre, wenn Java selbst das Zeichnen von Elementen übernähme. Solche Elemente nennt man "Light Weight Elemente".
Sun hat daher eine eigene, reichhaltige und mächtige Benutzeroberfläche geschaffen, die man zunächst Swing nannte. Inzwischen heißen diese Klassen JFC (Java Foundation Classes). Sie basiert nur noch an wenigen Stellen auf AWT, und erweitert dessen Möglichkeiten erheblich. Swing bietet Grafikobjekte an, die man nur mit viel Aufwand selbst nachahmen könnte. Zudem lässt sich der "Look and Feel" von Swing konfigurieren, so dass die Anwendung eher nach Windows, oder eher nach einem anderen Betriebssystem aussieht.
Im Prinzip können wir das obige Programm fast wörtlich übernehmen.
import javax.swing.*; public class Test { public static void main (String args[]) { JFrame F=new JFrame("Test Frame"); F.setSize(300,300); F.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); F.setVisible(true); } }
Eine weitere Funktion haben wir hinzugenommen. Die Methode
setDefaultCloseOperation
erlaubt es, das Verhalten beim Schließen
des Fensters auf einfache Weise zu beeinflussen. Wie entschließen uns, dass das
Programm dann beendet werden soll und übergeben die entsprechende Konstante.
Man beachte auch den anderen import!
Wir wollen nun einen String in das Fenster zeichnen. Dazu könnte man den String einfach in main zeichnen lassen.
import java.awt.*; public class Test { public static void main (String args[]) { Frame F=new Frame(); F.setSize(300,300); F.setVisible(true); Graphics g=F.getGraphics(); g.drawString("Hello World!",50,50); } }
Die Klasse Graphics stellt Instanzen bereit, die Methoden zum Zeichnen auf eine bestimmte Ausgabeflächen haben. In diesem Fall versuchen wir, als Ausgabefläche das Fenster zu nutzen. drawString() ist eine Methode von Graphics, die einen String an einer bestimmten Stelle ausgibt.
Das eben gezeigte Programm funktioniert aber nicht! Das Fenster wird nämlich durch einen anderen Prozess geöffnet. Zu der Zeit, wenn main das Graphics-Objekt bestellt, ist das Fenster noch nicht geöffnet. Daher geht das Zeichnen ins Leere. Man kann dies dadurch beheben, dass man eine gewaltige Verzögerungsschleife einbaut, etwa
for (int i=0; i<100000000; i++) {}
Natürlich ist das unschön. Außerdem wird das Fenster nicht neu gezeichnet, wenn es von einem anderen Fenster kurzzeitig verdeckt wurde.
Man muss daher einen völlig anderen Programmierstil entwickeln. Ein Programm mit einer graphischen Benutzeroberfläche wird von Ereignissen gesteuert. Eines dieser Ereignisse ist die Aufforderung, das Fenster neu zu zeichnen. Am einfachsten entspricht man dieser Aufforderung, indem man die paint-Methode von Frame, bzw. JFrame überlagert. Dazu benötigt man eine Kindklasse.
import javax.swing.*; import java.awt.*; class TestFrame extends JFrame { public void paint (Graphics g) { g.drawString("Hello World!",100,100); } } public class Test { public static void main (String args[]) { JFrame F=new TestFrame(); F.setTitle("Test"); F.setSize(300,300); F.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); F.setVisible(true); } }
Die Methode paint bekommt schon ein gültiges Graphics-Objekt übergeben, das man sofort benutzen kann.
Nun funktioniert auf einmal das Programm. Man kann das Fenster sogar in der Größe verändern. Unschön ist weiter, dass der String nicht in der Mitte des Fensters ausgegeben wird. Dies liegt daran, dass die Koordinatenangaben vom linken oberen Eck des Fensters aus gezählt werden. Wir werden dieses Problem durch Einfügen einer Komponente in das Fenster beheben.
Übrigens wird die Methode paint beim Öffnen des Fensters aufgerufen, weil ja ein Neuzeichnen notwendig ist. Man kann das aber mit
repaint();
jederzeit selbst forcieren. Dies ruft paint nicht direkt auf, sondern erzeugt ein Ereignis, das angibt, dass ein Neuzeichnen notwendig ist. Das Betriebssystem selbst initiiert das Neuzeichnen. Dies kann in einem anderen Thread geschehen. Ein Thread ist ein selbständig laufendes Programm, das scheinbar parallel zum aktuellen Programm arbeitet. Wir besprechen Threads in einem anderen Abschnitt ausführlich. Die Ereignisbehandlung wird in Java einem eigenen Thread durchgeführt.
Wir versuchen nun, die Grafik gezielter in das Fenster zu schreiben. Dazu ermitteln wir seine Größe und zeichnen ein diagonales Kreuz und ein Rechteck über die gesamte Fenstergröße.
public void paint (Graphics g) { Dimension d=getSize(); int w=d.width,h=d.height; g.drawLine(0,0,w,h-1); g.drawLine(0,h-1,w-1,0); g.drawRect(0,0,w,h); }
Die Methode drawLine() erwartet einen Anfangs- und einen Endpunkt. Die Koordinaten (x,y) werden dabei so gezählt, dass (0,0)links oben ist. Dagegen erwartet drawRect() die Koordinate der linken oberen Ecke und die Ausmessungen des Rechtecks (Weite und Höhe).
Offenbar wird als Zeichenfläche die Gesamtfläche des Fensters genommen und nicht seine sichtbare innere Fläche. Dies behebt man dadurch, dass man in das Fenster eine Komponente vom Typ JCanvas einfügt und auf diese zeichnet. Das Programm sieht dann folgendermaßen aus.
import javax.swing.*; import java.awt.*; class TestCanvas extends Canvas { public void paint (Graphics g) { Dimension d=getSize(); int w=d.width,h=d.height; g.drawLine(0,0,w-1,h-1); g.drawLine(0,h-1,w-1,0); g.drawRect(0,0,w-1,h-1); } } public class Test { public static void main (String args[]) { JFrame F=new JFrame(); F.setTitle("Test"); F.setSize(300,300); F.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); F.add(new TestCanvas()); F.setVisible(true); } }
Wir haben hier mittels add() eine Komponente ins Fenster eingefügt. Deren paint-Methode übernimmt das Neuzeichnen.
Nun können wir durch Ändern der paint-Methode komplexere Zeichnungen anfertigen. Als Beispiel dient ein Farbspiel, das jedoch nur auf System mit genügend vielen Farben zur Geltung kommt.
class TestCanvas extends Canvas { public void paint (Graphics g) { Dimension d=getSize(); int w=d.width,h=d.height; int i=0; Color C; while (i<=w-1-i && i<=h-1-i) { // Beschaffe neue Farbe: if (2*i<256) C=new Color(2*i,255-2*i,255); else C=new Color(255,0,255); g.setColor(C); // setze Farbe g.drawRect(i,i,w-1-2*i,h-1-2*i); i++; } } }
Das Farbmodell, das hier verwendet wird, setzt Farben aus ihren Rot-, Grün- und Blauanteilen zusammen. Von außen nach innen nimmt also Rot ab und Grün zu. Ganz im innern wird die Farbe zu Pink (Mischung aus Rot und Blau). Die Farbanteile können als int-Werte im Bereich 0 bis 255 oder als float-Werte im Bereich 0.0 bis 1.0 angegeben werden.
Das Programm erzeugt folgende Grafik:
Dieses Programm zeichnet noch schnell genug, um als interaktives Programm durchzugehen. Falls das Neuzeichnen des Fensters zu lange dauert, sollte man auf andere Techniken zurückgreifen. In diesem Fall blockiert nämlich die Ereignisbearbeitung in paint die gesamte Ereignisbehandlung des Programms.
Es ist auch möglich, mehrere Elemente zu einem Fenster hinzuzufügen. In diesem Fall muss man aber die Anordnung (das Layout) der Elemente einstellen. Java benutzt Instanzen spezieller Klassen für diese Arbeit, die Layout-Manager.
Im folgenden Beispiel verwenden wir ein BorderLayout. Dieses Layout hat Nord-, Süd-, Ost-, West-Elemente und ein zentrales Element, das den meisten Platz einnimmt. Man braucht nicht alle diese Elemente verwenden. Als Beispiel erzeugen wir das folgende Fenster.
Im Norden befindet sich eine Komponente vom Typ
JLabel,
die den Text "Farbenspiel" enthält. Im Süden haben wir eine Komponente vom Typ JPanel, die zwei Komponenten vom Typ
JButton
enthält. Manche Komponenten, wie JPanel dienen
nur dazu, andere Komponenten aufzunehmen. Sie sind Kinder vom Typ
JContainer
, wie auch JFrame
. Verwendet man noch das AWT, so
kann man einfach das J in diesen Klassennamen weglassen.
Wir ersetzen lediglich das Hauptprogramm.
public class Test { public static void main (String args[]) { JFrame F=new JFrame(); F.setTitle("Test"); F.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); F.setSize(300,300); // Setze Layout: F.setLayout(new BorderLayout()); // Oben das Label: F.add("North",new JLabel("Farbenspiel")); // Im Zentrum die Farben: F.add("Center",new TestCanvas()); // Unten die Knöpfe: JPanel p=new JPanel(); p.add(new JButton("Change")); p.add(new JButton("Close")); F.add("South",p); // Anzeigen: F.setVisible(true); } }
Wie man sieht, muss man sich nur eine Instanz von BorderLayout beschaffen und als Layout-Manager einrichten (mit setLayout()). Außerdem übergibt man der Methode add(), die übrigens jede Komponente hat, einen String, der die Ausrichtung angibt. Die TestCanvas-Klasse bleibt von all dem natürlich unberührt.
Die Knöpfe tun derzeit noch nichts.
Der Layout-Manager berechnet die Höhe nach dem Rückgabewert der Methoden
Dimension getPreferredSize (); Dimension getMinimumSize ();
die alle Komponenten haben. Man kann diese Methoden überschreiben, um zu verhindern, dass eine Canvas auf die Höhe 0 zusammenschrumpft. Ein Panel berücksichtigt z.B. bei der Rückgabe seiner bevorzugten Dimension automatisch auch die Komponenten, die in ihm enthalten sind.
Es gibt auch andere Layout-Manager. Einfach zu handhaben ist GridLayout. Der Konstruktor für GridLayout benötigt eine Zeilen- und eine Spaltenzahl und die Komponenten werden einfach in eine rechteckige Matrix geformt. Ist dabei die Zeilenzahl 0, so bestimmt die Anzahl der übergebenen Komponenten die Zeilenzahl. Hier ist ein Beispiel.
public class Test { public static void main (String args[]) { JFrame F=new JFrame(); F.setTitle("Test"); F.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); F.setSize(300,300); // Setze Layout: F.setLayout(new GridLayout(0,2)); F.add(new JLabel("Text 1: ")); F.add(new JLabel("High")); F.add(new JLabel("Text 2: ")); F.add(new JLabel("Zweite Zeile")); // Packen: F.pack(); // Anzeigen: F.setVisible(true); } }
Die Methode pack(), die hier noch aufgerufen wurde, sorgt dafür, dass das Fenster gerade so groß wird, dass man alle enthaltenen Komponenten sehen kann.
Ein etwas schwierigerer Layout-Manager ist GridBagLayout. Er ordnet die Komponenten im Prinzip in Zeilen und Spalten an, jedoch mit unterschiedlichen Maschenweiten. Außerdem kann sich eine Komponente über mehrere Zeilen und Spalten erstrecken. Erfahrungsgemäß braucht man diesen Manager sehr selten.
Es gibt noch den CardPanel, der alle Komponenten übereinander anordnet, wobei jeweils nur eine sichtbar ist.
Schließlich kann man noch die Anordnung der Komponenten in eigener Regie vornehmen. Dazu kann man einfach die Methode doLayout der Komponenten überschreiben und in dieser Methode das Layout der enthaltenen Komponenten selbst vornehmen (mittels setLocation und setSize).
Den Font aller Komponenten ändert setFont. Dazu besorgt man sich einen neuen Font und stellt ihn ein. Der Font wird mit seinem Font-Namen, der Fontart (PLAIN, BOLD, ITALIC) und der Font-Größe angegeben. Mit
String[] fonts=Toolkit.getDefaultToolkit().getFontList();
kann man sich eine Liste aller verfügbaren Fonts ausdrucken. Für ein JLabel stellt man den Font folgendermaßen ein, wobei wir hier die doppelte Größe des eingestellten Fonts wählen.
JLabel label=new JLabel("Text 1: "); label.setFont(new Font("Dialog",Font.BOLD,l.getFont().getSize()*2));
Das gleiche funktioniert bei einem Graphics-Objekt, so dass man auch verschiedene Fonts in paint verwenden kann. Voreingestellt ist der Font der Komponente (z.B. des Canvas).
Wir stellen uns nun die Aufgabe einen String genau zentriert auf einem Canvas darzustellen, egal wie groß dieses Canvas ist. Dazu muss man sich eine Klasse FontMetrics besorgen, mit deren Hilfe man die Größe des Strings berechnen kann. Außerdem muss man beachten, dass die Methode drawString von Graphics Strings immer an der Basislinie ausgibt. Deswegen muss man zum linken oberen Eck des Strings noch den Ascent hinzuzählen.
import java.awt.*; import javax.swing.*; class HalloCanvas extends Canvas { public HalloCanvas () { setPreferredSize(new Dimension(400,200)); } public void paint (Graphics g) { g.setFont(new Font("Dialog",Font.BOLD,40)); FontMetrics metrics=g.getFontMetrics(); String s="Ich grüße die Welt!"; // Berechne Stringbreite und -höhe int w=metrics.stringWidth(s); int h=metrics.getHeight(); // Berechne Aufpunkt für String Dimension d=getSize(); int x=(d.width-w)/2; int y=(d.height-h)/2+metrics.getAscent(); // Zeichne Rechteck g.setColor(Color.gray); g.drawRect((d.width-w)/2,(d.height-h)/2,w,h); // Zeichne String g.setColor(Color.black); g.drawString(s,x,y); } } public class Test extends JFrame { public Test () { super("Test"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); add("Center",new HalloCanvas()); pack(); } public static void main (String args[]) { JFrame F=new Test(); F.setVisible(true); } }
In diesem Beispiel haben wir ein Canvas in den JFrame eingebettet, das eine eigene bevorzugte Größe hat. Dadurch kann JFrame gepackt werden und wird diese Größe berücksichtigen.