Grafikfenster

Inhalt

 

 

Fenster

 

Swing

 

Ereignisorientiertes Programmieren

 

Layout mit JLabel, JCanvas, JPanel

  Fonts, Anti-Aliasing und Graphics2D

 

Übungsaufgaben

Fenster

Wir haben bisher rein text-basierte Programme geschrieben. Nun wollen wir Grafiken erzeugen. Es ist nicht schwer, ein Grafikfenster zu öffnen.

import javax.swing.*;

public class Test
{
    public static void main (String args[])
    {
        JFrame F = new JFrame("Test Frame");
        F.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        F.setSize(600, 600);
        F.setLocationRelativeTo(null);
        F.setVisible(true);
    }
}

Das Programm zeigt lediglich ein leeres Fenster. Das Fenster kann allerdings schon verschoben, vergrößert, maximiert oder geschlossen werden. Man kann das Schließen des Fensters und alle anderen Operationen selbst überwachen. Im Moment ist es einfacher, die CloseOperation zu setzen.

Die Position des Fensters wird mit null in die Mitte des Bildschirms gesetzt. Das sollte erst geschehen, nachdem das Fenster eine gute Größe hat. Das Fenster erscheint erst, wenn setVisible(true) aufgerufen wird.

Man beachte die Importe. Wir importieren einfach alle Klassen aus java.swing.

Ereignisorientiertes Programmieren

Wir wollen nun einen String in das Fenster zeichnen. Dazu könnte man den String einfach in main zeichnen lassen.

import java.awt.*;
import javax.swing.*;

public class Test
{
    public static void main (String args[])
    {
        JFrame F = new JFrame("Test Frame");
        F.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        F.setSize(600, 600);
        F.setLocationRelativeTo(null);
        F.setVisible(true);
        
        Graphics G = F.getGraphics();
        G.drawString("Hallo!",300,300);
    }
}

Für das Zeichnen benötigen wir die Klassen in java.awt, die wir zusätzlich importieren.

Das eben gezeigte Programm funktioniert aber nicht zuverlässig! Möglicherweise erscheint der Schriftzug "Hallo!" sogar bei einigen Systtem. Das Fenster wird aber 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 MyCanvas extends Canvas
{
    @Override
    public void paint (Graphics g)
    {
        int w = getWidth(), h = getHeight();

        g.setColor(Color.gray.darker());
        g.fillRect(0, 0, w, h);

        g.setColor(Color.gray.brighter());
        g.fillRect(20, 20, w - 40, h - 40);

        g.setColor(Color.red.darker());
        g.setFont(new Font("SansSerif", Font.PLAIN, 20));
        g.drawString("Hello!", w / 2 - 40, h / 2);
    }
}

class TestFrame extends JFrame
{
    public TestFrame()
    {
        super("Test Frame");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        add(new MyCanvas());
    }

}

public class Test
{
    public static void main (String args[])
    {
        JFrame F = new TestFrame();
        F.setSize(600, 600);
        F.setLocationRelativeTo(null);
        F.setVisible(true);
    }
}

JCanvas Beispiel

Hier einige Erklärungen zum ereignisgesteuerten Programmstil und den verwendeten Swing-Elementen.

Die Methode paint wird 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 System initiiert 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 MyCanvas 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.

Layout mit JLabel, LButton, JPanel

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.

Wir ersetzen lediglich die Klasse TestFrame.

class TestFrame extends JFrame
{
    public TestFrame()
    {
        super("Test Frame");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // Setze Layout:
        setLayout(new BorderLayout());

        // Oben das Label:
        JPanel north = new JPanel();
        north.add(new JLabel("Farbenspiel"));
        add("North", north);

        // Im Zentrum die Farben:
        add("Center", new MyCanvas());

        // Unten die Knöpfe:
        JPanel south = new JPanel();
        south.add(new JButton("Change"));
        south.add(new JButton("Close"));
        add("South", south);
    }

}

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 MyCanvas-Klasse bleibt von all dem natürlich unberührt.

Die Knöpfe tun derzeit noch nichts.

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.

import javax.swing.*;
import java.awt.*;
public class Test
{
    public static void main (String args[])
    {
        JFrame F = new JFrame();
        F.setTitle("Test");
        F.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        F.setPreferredSize(new Dimension(400,100)); 
         // 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();
        F.setLocationRelativeTo(null);
        
         // 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. Allerdings wird setPreferredSize() berücksichtigt, wenn dies größer ist.

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).

Fonts und Graphics2D

Den Font von 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. Wir ändern einfach die Klasse MyCanvas, um einen String genau in der Mitte auszugeben.

Außerdem verwenden wir die Klasse Graphics2D. Dies ist eine Erweiterung von Graphics. Sie erlaubt es die Ausgabe zu glätten (Anti-Aliasing), was für Fonts und Grafik separat eingestellt werden muss. Außerdem funktioniert die Einstellung der Linienbreite nun über Strokes.

import java.awt.*;
import javax.swing.*;

class MyCanvas extends Canvas
{
    @Override
    public void paint (Graphics g1)
    {
        Dimension d = getSize();
        int w = d.width, h = d.height;

        Graphics2D g = (Graphics2D) g1;
        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        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 ws = metrics.stringWidth(s);
        int hs = metrics.getHeight();

        // Berechne Aufpunkt für String
        int x = (w - ws) / 2;
        int y = (h - hs) / 2 + metrics.getAscent();

        // Zeichne Rechteck
        g.setColor(Color.gray);
        g.setStroke(new BasicStroke(4));
        g.drawRect((w - ws) / 2 - 10, (h - hs) / 2 - 10, ws + 20, hs + 20);

        // Zeichne String
        g.setColor(Color.black);
        g.drawString(s, x, y);
    }

}

class TestFrame extends JFrame
{
    public TestFrame ()
    {
        super("Test Frame");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        add(new MyCanvas());
    }

}

public class Test
{
    public static void main (String args[])
    {
        JFrame F = new TestFrame();
        F.setSize(600, 600);
        F.setLocationRelativeTo(null);
        F.setVisible(true);
    }
}

Übungsaufgaben

  1. Schreiben Sie ein Programm, das die Funktion sin(x)/x im Bereich -10 bis 10 plottet. Dazu sollten Sie nur TestCanvas in SinCanvas umändern. Der Plot besteht aus einzelnen Linien. Als Auflösung in der x-Koordinaten kann man einfach die Weite des Canvas benutzen. Zur Umrechnung der (x,y)-Koordinaten in Bildschirmkoordinaten verwenden Sie Funktionen row(x) und col(y), die die Weite und Höhe des Canvas über die Methode getSize von SinCanvas ablesen.
  2. Erzeugen Sie Grautöne von Schwarz bis Weiß, die von links nach rechts im Canvas fließen. Ganz links ist Schwarz, ganz rechts Weiß. Benutzen Sie hier einfach die Farbangabe mit float-Werten (Spalte/Weite).
  3. Benutzen Sie unser erstes TestCanvas, um sämtliche Komponenten, die ein BorderLayout verwalten kann, sichtbar zu machen, indem sie in alle vier Himmelsrichtungen und in das Zentrum ein TestCanvas legen. Dies funktioniert nicht! Es ist immer nur eine Komponente (Center) sichtbar. Überschreiben Sie deswegen die Methode getPreferredSize, um zu erreichen, dass die Höhe und Breite mindestens 20 ist.

Lösungen.

Aufgabe ohne Lösung

  1. Schreiben Sie ein Programm, das eine quadratische Canvas mit 1000 zufälligen Punktepaaren füllt. Markieren Sie diejenigen Punkte, die in den einbeschriebenen Kreis fallen, rot (setColor(Color.red)), die anderen schwarz. Merken Sie sich die Punkte in einem Array, damit nicht bei jedem paint() neue Punkte gezeichnet werden.
  2. Testen Sie GridLayout mit einem 3x3-Gitter aus unseren TestCanvas.
  3. Erweitern Sie SinCanvas zum Setzen der Grenzen der Darstellung. Stellen Sie die Funktion gleichzeitig mit mehreren verschiedenen Ausschnitten dar (etwa in einem 1x5 Gittter von SinCanvas-Komponenten).
  4. Zeichnen Sie eine Punktefolge, die durch eine Iterationsvorschrift gegeben wird. Als Beispiel, wählen Sie eine Funktion die zufällig (mit gleicher Wahrscheinlichkeit) eine der drei Abbildungen
    (x,y) -> (x/2,y/2)
    (x,y) -> (1/2+x/2,1/2+y/2)
    (x,y) -> (1/2+x/2,y/2)
    auswählt. Zeichnen Sie etwa 1000 Punkte dieser Iteration.

Zurück zum Java-Kurs