Home > Informatik > Stufe EF > 9. zweidimensionale Arrays

9.2 Eine Graphikanwendung

Notenliste - Graphikanwendung - zelluläre Automaten

Dieser Workshop dient allein der Vertiefung der Graphikprogrammierung sowie des Umgangs mit zweidimensionalen Arrays. Übungen sind für diesen fakultativen Workshop nicht vorgesehen. Falls Sie diesen Workshop im Informatikunterricht an Ihrer Schule bearbeiten, wird sich Ihre Lehrerin oder Ihr Lehrer sicherlich eigene Übungen ausgedacht haben. Oder auch nicht.

Betrachten Sie folgenden Screenshot:

Screenshot der fertigen Anwendung

Die Graphikanwendung, die Sie in diesem Workshop programmieren soll, ist nicht besonders kompliziert, könnte aber das Grundgerüst für ein eigenes kleines Spiel werden.

Sie sehen eine Spielfläche, die aus 10 x 10 Spielfeldern besteht. Ein roter Spielstein kann nun mithilfe der vier Buttons gesteuert werden. Das war's eigentlich schon.

Die Spielfläche besteht aus 10 x 10 Spielfeldern. Wir werden in dem Workshop so vorgehen, dass wir zuerst mal eine Klasse Feld programmieren und dann einen zweidimensionalen Array dieser Spielfelder erzeugen.

Aber eines nach dem anderen…

Schritt 1 - Klasse Feld

Wir verfahren nach dem bewährten Prinzip der Bottom-Up-Programmierung und entwickeln zunächst eine Klasse für ein einzelnes Feld (eines der kleinen Quadrate im Applet).

Ein Feld soll quadratisch dargestellt werden, und ein Attribut status speichert, ob es sich um ein leeres Feld oder ein von der Figur besetztes Feld handelt. Außerdem ist es notwendig, die relativen Koordinaten des Feldes mit zu speichern, wie wir später noch sehen werden. Hier ist der Quelltext der Klasse Feld, Version 1:

import java.awt.*;

public class Feld
{
   int status;
   int x,y;
   
   public Feld(int x, int y)
   {
      status = 0;
      this.x = x;
      this.y = y;
   }

   public void anzeigen(Graphics g)
   {
      if (status == 0)
      {
         g.setColor(new Color(255,255,191)); // hellgelb
         g.fillRect(50+x*40,50+y*40,40,40);
         g.setColor(new Color(0,0,0)); // schwarz
         g.drawRect(50+x*40,50+y*40,40,40);
      }
   }
}

Da können wir noch nichts Besonderes entdecken, nicht wahr? Die wichtigen Informationen werden in primitiven Attributen gespeichert, der Konstruktor initialisiert diese Attribute mit Werten, und die Methode anzeigen stellt das Feld in einem Applet dar.

Schritt 2 - Test-Anwendung

Jetzt schreiben wir uns eine kleine Test-Anwendung, welche die Methoden von Feld testet. Hier der Quelltext:

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

public class Graphikanwendung extends JFrame
{
    Feld[][] testfeld;

    public Graphikanwendung()
    {
        initFelder();
        
        setSize(500,500);
        setTitle("Eine kleine Graphik-Anwendung");
        setResizable(false);
        setVisible(true);
    }
 
    public void initFelder()
    {
        testfeld = new Feld[10][10];
        for (int x=0; x < 10; x++)
            for (int y=0; y < 10; y++)
                testfeld[x][y] = new Feld(x,y);
    }

    public void paint(Graphics g)
    {
        for (int x=0; x < 10; x++)
            for (int y=0; y < 10; y++)
                testfeld[x][y].anzeigen(g);
    }

    public static void main(String[] args) 
    {
        new Graphikanwendung();
    }
}

Diese Anwendung enthält nicht nur ein einzelnes Objekt der Klasse Feld, sondern einen zweidimensionalen Array von solchen Feldern.

⇒ Testen Sie den neuen Quelltext. Kompilieren Sie das Projekt. Klicken Sie mit der rechten Maustaste auf den orangefarbenen Kasten der Klasse Graphikanwendung und wählen Sie den zweiten Befehl aus dem Kontextmenü aus, nämlich main().

Wenn Sie alles richtig gemacht haben, müssten Sie folgendes Bild sehen:

Die erste Version der kleinen Graphikanwendung
Ersteller: Ulrich Helmich 2022, Lizenz: ---

Schritt 3 - Feld mit Zuständen

Wir ergänzen die anzeigen()-Methode der Klasse Feld so, dass zwei verschiedene Zustände angezeigt werden können. Den jeweiligen Zustand speichern wir in der globalen Variablen (bzw. einem Attribut) namens status:

public void anzeigen(Graphics g)
{
   g.setColor(new Color(255,255,191)); // hellgelb
   g.fillRect(50+x*40,50+y*40,40,40);
   g.setColor(new Color(0,0,0)); // schwarz
   g.drawRect(50+x*40,50+y*40,40,40);

   if (status == 1)
   {
      g.setColor(new Color(255,0,0)); // rot
      g.fillOval(50+x*40,50+y*40,40,40);
   }
}

Zunächst wird nur ein leeres gelbes Feld gezeichnet. Hat das Attribut status aber den Wert 1, wird auf das gelbe Feld ein roter Kreis gezeichnet.

Übung für Interessierte

Der einfache rote Kreis sieht nicht allzu plastisch aus. Vielleicht können Sie ihn ja schöner gestalten, mit Schatten, Reflexen, Lichtpunkten oder etwas Ähnlichem.

Das Attribut status muss jetzt natürlich auch verändert werden können, sonst macht die Sache ja keinen Sinn. Dazu ergänzen wir die Klasse Feld um eine neue Methode:

    public void setzeStatus(int neuerStatus)
    {
       status = neuerStatus;
    }

In der Klasse Graphikanwendung ergänzen wie die initFelder()-Methode um eine Zeile:

    public void initFelder()
    {
        testfeld = new Feld[10][10];
        for (int x=0; x < 10; x++)
            for (int y=0; y < 10; y++)
                testfeld[x][y] = new Feld(x,y);
                
        testfeld[5][5].setzeStatus(1);
    }

Auf diese Weise soll dann das Feld mit den Koordinaten (5,5) einen roten Kreis enthalten. Wenn wir das Ganze kompilieren und zum Laufen bringen, können wir uns davon überzeugen, dass der bisherige Quelltext funktioniert:

Die zweite Version der kleinen Graphikanwendung
Ersteller: Ulrich Helmich 2022, Lizenz: ---

Schritt 4 - Flexible Felder

Wir machen zunächst die Klasse Feld flexibler. Die Breite der Felder war bisher auf 40 Pixel festgelegt. Jetzt soll die Benutzerin der Klasse selbst entscheiden, wie breit ihre Felder sein sollen.

Um dies zu erreichen, müssen wir zwei Dinge erledigen. Erstens muss die Breite des Feldes innerhalb der Klasse Feld gespeichert werden. Dies sollte kein Problem sein: Ein neues Attribut breite, und das Problem ist gelöst. Beim Erzeugen eines Feld-Objektes muss dann die gewünschte Breite als Parameter übergeben werden. Betrachten wir den neuen Konstruktor:

    public Feld(int x, int y, int b)
    {
        status = 0;
        this.x = x;
        this.y = y;
        breite = b;
    }

Wie sich diese flexiblere Lösung auf das Zeichnen der Felder auswirkt, zeigt der Quelltext der anzeigen-Methode:

    public void anzeigen(Graphics g)
    {
        g.setColor(new Color(255,255,191)); // hellgelb
        g.fillRect(50+x*breite,50+y*breite,breite,breite);
        g.setColor(new Color(0,0,0)); // schwarz
        g.drawRect(50+x*breite,50+y*breite,breite,breite);

        if (status == 1)
        {
            g.setColor(new Color(255,0,0)); // rot
            g.fillOval(50+x*breite,50+y*breite,breite,breite);
        }
    }

Was hier noch nicht flexibel gestaltet wurde, ist die Position des Feldes bzw. der Gesamtheit der Felder. Die links liegenden Felder beginnen immer noch bei x=50 und y=50.

Übung für Interessierte

Machen Sie auch die Position der Felder etwas flexibler. Wenn die Felder beispielsweise nur 20 Pixel breit sind, wäre es schön, wenn das Gesamtfeld in der Mitte des Anwendungsfensters liegen würde und nicht links oben wie in der nächsten Abbildung.

Das Gesamtfeld liegt nicht optimal
Ersteller: Ulrich Helmich 2022

Schritt 5 - Applet mit Buttons

Wir wollen jetzt die Anwendung mit vier Buttons ausstatten, die wir up, down, left und right nennen. Hier der lauffähige Quelltext der erweiterten Graphikanwendung:

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

public class Graphikanwendung extends JFrame
{
    Feld[][] testfeld;
    Button   up,down,left,right;

    public Graphikanwendung()
    {
        initFelder();
        initButtons();

        setSize(500,580);
        setTitle("Eine kleine Graphik-Anwendung");
        setResizable(false);
        setVisible(true);
    }

    public void initFelder()
    {
        testfeld = new Feld[10][10];
        for (int x=0; x < 10; x++)
            for (int y=0; y < 10; y++)
                testfeld[x][y] = new Feld(x,y,40);

        testfeld[5][5].setzeStatus(1);
    }

    public void initButtons()
    {
        up    = new Button("Hoch");
        down  = new Button("Runter");
        left  = new Button("Links");
        right = new Button("Rechts");

        add(up);
        add(down);
        add(left);
        add(right);

        setLayout(null);

        up.setBounds   (200,440,100,30);
        down.setBounds (200,500,100,30);
        left.setBounds (160,470,100,30);
        right.setBounds(240,470,100,30);
    }

    public void paint(Graphics g)
    {
        for (int x=0; x < 10; x++)
            for (int y=0; y < 10; y++)
                testfeld[x][y].anzeigen(g);
    }

    public static void main(String[] args) 
    {
        new Graphikanwendung();
    }
}

Hier muss nicht viel erläutert werden, Buttons sind Ihnen bereits aus der Folge 6 (Roboter) sowie aus der Folge 8 (Sortieralgorithmen) bekannt. Eine Funktionalität haben die Buttons noch nicht, das kommt im nächsten Schritt.

Die Anwendung mit vier Buttons
Ersteller: Ulrich Helmich 2021, Lizenz: ---

Damit die vier Buttons Platz haben, wurde die Höhe des Anwendungsfensters übrigens von 500 auf 580 Pixel erhöht.

Schritt 6 - Buttons mit Funktionen

Wir wollen die Buttons nun mit Funktionalität ausstatten, zunächst aber nur den up-Button, um mal ganz einfach anzufangen. Wenn auf den up-Button geklickt wird, soll das Feld über dem markierten Feld ebenfalls markiert werden. Eine ganz einfache Aufgabe also.

An der Klasse Feld müssen wir diesmal nichts ändern. Was für ein Glück!

Die Klasse Graphikanwendung wird um einen ActionListener ergänzt, der in der Bibliothek event steckt, die wir mit dem import-Befehl ebenfalls einbinden müssen:

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

public class Graphikanwendung extends JFrame implements ActionListener

Wenn wir nun versuchen, die Anwendung zu kompilieren, gibt es eine Fehlermeldung:

Fehlermeldung beim Kompilieren
Ersteller: Ulrich Helmich 2022, Lizenz: ---

"Graphikanwendung is not abstract and does not override abstract method actionPerformed..."

Dadurch, dass wir das Interface ActionListener in unsere Anwendung eingebunden haben, wurde unsere Anwendung quasi zu einer Art "Tochterklasse" von ActionListener. Die Methoden, die von ActionListener "verordnet" werden, müssen von der "Tochterklasse" auf jeden Fall implementiert werden. Ein Interface in Java ist also im Grunde eine "Implementierungs-Vorschrift". Alle Klassen, die das gleiche Interface eingebunden haben, müssen auch die gleichen vorgeschriebenen Methoden implementieren. Wie sie das genau machen, das ist den untergeordneten Klassen überlassen.

Im Falle des Interfaces ActionListener muss nur eine Methode implementiert werden, nämlich ActionPerformed(). Und genau das machen wir jetzt:

    public void actionPerformed(ActionEvent event)
    {
       if (event.getSource() == up)
       {
       }
    }

Dieser Methode wird ein Parameter der Klasse ActionEvent übergeben, der hier als event bezeichnet wird. ActionEvent ist eine Klasse, und Klassen haben bekanntlich Methoden. Also kann auch das Objekt event auf eine Methode zugreifen. Was wir hier brauchen, ist die Methode getSource().

Diese Methode fragt nämlich ab, ob einer der vier Buttons geklickt wurde, und wir können auch überprüfen, welcher Button geklickt wurde. Mit der Abfrage

if (event.getSource() == up)

überprüfen wir, ob der up-Button angewählt wurde. Entsprechend könnte man mit drei weiteren Abfragen die drei anderen Buttons überprüfen.

Damit diese Abfrage aber auch funktionieren, müssen wir noch etwas tun. Wir müssen den vier Button-Objekten mitteilen, dass der ActionListener auf sie Zugriff haben soll. Wenn wir das nicht machen, kann man wie wild auf einen dieser Buttons klicken, aber getSource() ist nicht in der Lage, dies zu erkennen.

Darum ergänzen wir nun die initButtons()-Methode um die vier Zeilen

up.addActionListener(this);
down.addActionListener(this);
left.addActionListener(this);
right.addActionListener(this);

Jetzt erst können wir die Methode actionPerformed mit einer korrekten Funktionalität ausstatten:

    public void actionPerformed(ActionEvent event)
    {
        if (event.getSource() == up)
            testfeld[5][4].setzeStatus(1);
        if (event.getSource() == down)
            testfeld[5][6].setzeStatus(1);
        if (event.getSource() == left)
            testfeld[4][5].setzeStatus(1);
        if (event.getSource() == right)
            testfeld[6][5].setzeStatus(1);
        repaint();
    }

Wenn wir nun nacheinander auf die vier Buttons klicken, werden die Felder oberhalb, unterhalb, links und rechts des ursprünglichen Spielsteins ebenfalls mit einem Spielstein besetzt:

Die Buttons funktionieren, aber der Algorithmus noch nicht...
Autor: Ulrich Helmich 2022, Lizenz: ---

Das ist eigentlich noch nicht das, was wir wollen. Der Spielstein soll sich bewegen. Nach einem Buttonklick soll er sich also auf einem Nachbarfeld aufhalten, das ursprüngliche Feld soll dann aber wieder leer sein, also den Status 0 haben.

Schritt 7 - Die Klasse Spielbrett

Jetzt kommt eine ziemlich gravierende Umstellung des Graphik-Programms. Wir erzeugen nämlich eine neue Klasse Spielbrett. Schauen wir uns den Quelltext dieser Klasse an:

import java.awt.*;

public class Spielbrett
{   
   Feld[][] feld;
   int xpos, ypos;

   public Spielbrett(int xStart, int yStart, int breite)
   {   
      xpos = xStart;
      ypos = yStart;
      feld = new Feld[10][10];
   
      for (int x=0; x < 10; x++)
         for (int y=0; y < 10; y++)
            feld[x][y] = new Feld(x,y,breite);

      feld[xpos][ypos].setzeStatus(1);
   }

   public void anzeigen(Graphics g)
   {
      for (int x=0; x < 10; x++)
         for (int y=0; y < 10; y++)
            feld[x][y].anzeigen(g);
   }
}

Das ganze Projekt wurde umstrukturiert. Alle Anweisungen der Anwendung, die für die Verwaltung der Felder zuständig waren, sind nun in eine neue Klasse ausgelagert worden.

Der zweidimensionale Array Feld[][] feld befindet sich nun in dieser neuen Klasse, und auch das "Füllen" des Feldes mit Daten findet im Konstruktor dieser Klasse statt. Dem Konstruktor können wir auch mitteilen, welches der 100 Felder schon besetzt sein soll, und wir teilen ihm mit, wie breit die Felder sein sollen.

Das Anzeigen der 100 Felder übernimmt jetzt die klasseneigene Methode anzeigen(). Da diese Methode einen Parameter der Klasse Graphics benötigt, müssen wir im Kopf des Quelltextes auch den

import java.awt.*;

Befehl einbinden.

Refactoring

Eine solche gravierende Umstrukturierung eines Software-Projektes wird auch als Refactoring bezeichnet (siehe Wikipedia-Artikel "Refactoring").

Durch diese Umstrukturierung hat sich der Quelltext der Anwendung stark vereinfacht:

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

public class Graphikanwendung extends JFrame implements ActionListener
{
    Spielbrett brett;
    Button   up,down,left,right;

    public Graphikanwendung()
    {
        initFelder();
        initButtons();

        setSize(500,580);
        setTitle("Eine kleine Graphik-Anwendung");
        setResizable(false);
        setVisible(true);
    }

    public void initFelder()
    {
        brett = new Spielbrett(5,5,32);
    }

    public void initButtons()
    {
        up    = new Button("Hoch");
        down  = new Button("Runter");
        left  = new Button("Links");
        right = new Button("Rechts");

        add(up);
        add(down);
        add(left);
        add(right);

        setLayout(null);

        up.setBounds   (200,440,100,30);
        down.setBounds (200,500,100,30);
        left.setBounds (160,470,100,30);
        right.setBounds(240,470,100,30);

        up.addActionListener(this);
        down.addActionListener(this);
        left.addActionListener(this);
        right.addActionListener(this);
    }

    public void actionPerformed(ActionEvent event)
    {
        if (event.getSource() == up)    brett.hoch();
        if (event.getSource() == down)  brett.runter();
        if (event.getSource() == left)  brett.links();
        if (event.getSource() == right) brett.rechts();
        repaint();
    }    

    public void paint(Graphics g)
    {
        brett.anzeigen(g);
    }
	 
    public static void main(String[] args) 
    {
        new Graphikanwendung();
    }
}

Keine doppelten for-Schleifen mehr in der init()- oder paint()-Methode, sondern nur noch ein knapper Aufruf des Spielbrett-Konstruktors bzw. der Spielbrett.anzeigen()-Methode.

Dummerweise können wir diesen schönen und jetzt stark gekürzten Quelltext nicht mehr kompilieren. Etwas haben wir noch vergessen!

    public void actionPerformed(ActionEvent event)
    {
        if (event.getSource() == up)    brett.hoch();
        if (event.getSource() == down)  brett.runter();
        if (event.getSource() == left)  brett.links();
        if (event.getSource() == right) brett.rechts();
        repaint();
    }

Die Methoden der Klasse Spielbrett, die wir hier aufrufen, existieren noch gar nicht. Also müssen wir die Klasse Spielbrett noch etwas erweitern.

Schritt 8 - Bewegen der Figur

Wir müssen jetzt die Methoden Spielbrett.hoch() bis Spielbrett.rechts() implementieren. Schauen wir uns dazu den Quelltext der hoch()-Methode in der Klasse Spielbrett an:

    public void hoch()
    {
        if (ypos > 0)
        {
            feld[xpos][ypos].setzeStatus(0);
            ypos--;
            feld[xpos][ypos].setzeStatus(1);
        }
    }

Die Position der roten Spielfigur wird durch die neuen Attribute xpos und ypos verwaltet. Vergessen Sie nicht, die neuen Attribute in der Klasse Spielbrett zu deklarieren und im Konstruktor zu initialisieren.

Bei der hoch()-Methode muss als erstes überprüft werden, ob die Spielfigur überhaupt noch ein Feld nach oben gehen kann. Wenn die y-Koordinate der Figur nämlich bereits den Wert 0 hat, steht die Figur schon ganz oben und kann nicht mehr hoch bewegt werden.

Angenommen, die y-Koordinate der Figur ist > 0, dann passieren drei Dinge:

Erstens: Das alte Feld, auf dem die Figur gestanden hat, bekommt den Status 0:

feld[xpos][ypos].setzeStatus(0);

Zweitens: Die Figur bewegt sich nach oben, indem einfach die y-Koordinate um Eins vermindert wird:

ypos--;

Drittens: Das Feld, auf dem die Figur jetzt steht, bekommt den Status 1:

feld[xpos][ypos].setzeStatus(1);

Auf ähnliche Weise werden die drei anderen Methoden runter, links und rechts programmiert.

Übung für Interessierte

Natürlich ist das noch kein Spiel, was wir eben zusammen programmiert haben, sondern nur das Grundgerüst für ein mögliches Spiel. Ein paar Anregungen für Sie: Die Felder könnten einen dritten Status haben, der z.B. so zu interpretieren ist, dass etwas Essbares auf dem Feld liegt. Die Figur könnte am Anfang recht klein sein, und jedes mal, wenn sie auf ein Feld mit etwas Essbarem trifft, könnte ihr Durchmesser um 2 Pixel wachsen, bis sie ihre maximale Größe erreicht hat. Man könnte Wände in das Spielbrett einziehen, die von der Figur nicht passiert werden können, die Wände wiederum könnten Türen enthalten und so weiter und so fort.

Punkte für die Übung sind hier nicht angegeben; Ihre Lehrperson entscheidet selbst, wie viele Punkte sie Ihnen gibt, wenn Sie eine oder mehrere dieser Anregungen aufgreifen und in das Spiel einbauen.

Damit sind wir mit diesem Workshop am Ende angekommen. Wenn Sie noch mehr über zweidimensionale Arrays wissen möchten, gehen Sie bitte zum nächsten Abschnitt über Zelluläre Automaten.