Grafikobjekt verbessern

Ideal wäre es doch, wenn wir in unserem Grafikobjekt nicht nur den Typ der Grafik abspeichern, sondern die Grafik selbst – also gleich die Zeichnung die mit dem Zeichnungsbefehl fillOval oder drawRect erstellt wird.
Eine Ellipse ist ein Objekt vom Typ Ellipse ein Quadrat vom Typ Rectangle. Wir könnten nun in unserem Grafikobjekt statt dem Stifttyp einfach eine Variable vom Typ Object speichern was einfach ausgedrückt alles mögliche, also auch eine Ellipse oder ein Rectangle enthalten kann. Nur wie bringt man es dann in der Paint auf dem Bildschirm?
Unser  Graphics-Objekt das der paint-Methode übergeben wurde, hat auch dafür eine Lösung bzw. eine Methode. Man ruft diese auf mit: g.draw(shape).  Ein Shape ist ein sogenanntes Interface. Im Gegensatz zur Klasse besitzt ein Interface keine Implementierungen von Methoden, sondern nur leere Methoden sowie evtl. Konstanten, das sind nicht mehr veränderbare Variable. Wir werden uns auch gleich mehr damit befassen. Jedenfalls müssten wir, wenn wir Object verwenden, dieses dann erst auf ein Shape casten. Also g.draw((shape) objekt). Auch an einigen anderen Stellen müssten wir darauf achten.
Eleganter ist es, wenn wir uns mit der Schnittstelle Shape jetzt kurz befassen.

Ein Interface ist eine Sammlung von leeren Methoden die einem bestimmten Zweck dienen. Ein Objekt das dieses Interface implementiert passt dann einfach jene Methoden für den eigenen Zweck an.

Das Objekt Ellipse enthält zunächst nur die Koordinaten der Ellipse. Aber es braucht auch Methoden um diese Daten zu verarbeiten. Zum Beispiel ein Methode um die Werte auszulesen (getter) oder ob ein übergebener Punkt innerhalb der Ellipse liegt etc.
Diese Methoden werden von allen geometrischen Objekten in Java verwendet. Deshalb implementieren auch alle letztendlich das Interface Shape (Ellipse z.B. das Interface RectangleShape welches wiederum Shape implementiert). Shape enthält also weitere Methoden die benötigt werden um die Grafik darzustellen.

Von einem Interface allein kann natürlich kein noch Objekt gebildet werden. Man kann ihm aber ein Objekt zuweisen das ein Interface des gleichen Typs implementiert hat. Genau das machen wir uns zunutze indem wir in unserem Grafikobjekt statt stifttyp, eine Variable vom Datentyp Shape definieren.
Für den Fall das die Werkzeugspitze Kreis ausgewählt ist, erzeugen wir einfach eine neue Ellipse und fügen sie unserem virtuellem Bildschirm hinzu. Im Konstruktor des Grafikobjekts muss dann nur noch das Shape mit der Ellipse initialisiert werden, was problemlos möglich ist da ja Ellipse das Shape implementiert. Wie in Java üblich enthält danach die Variable Shape eine Referenz auf das Ellipse-Objekt.
Jetzt aber ran an den Speck:
Grafikobjekt.java

package miniMalen1;

import java.awt.Point;
import java.awt.Shape;

import javax.swing.JCheckBox;

class Grafikobjekt {

  Point koordinaten;
  Shape form;
  boolean gefuellt;

  public Grafikobjekt(Point p, Shape st, boolean cb) {
    this.koordinaten = p;
    this.form = st;
    this.gefuellt = cb;
  }

  public Point getKoordinaten() {
    return this.koordinaten;
  }

  public void setKoordinaten(Point koordinaten) {
    this.koordinaten = koordinaten;
  }
  
  public Shape getForm() {
    return this.form;
  }

  public boolean isGefuellt() {
    return this.gefuellt;
  }
}

Wir sehen das stifttyp dem Shape gewichen ist und stiftstaerke weggefallen, da das Shapeobjekt gleich in der richtigen Stärke, bzw. Größe eingestellt wird. Dementsprechend müssen wir natürlich auch den Konstruktor sowie die getter ändern oder löschen.

Tafel.java

package miniMalen1;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import javax.swing.JPanel;

public class Tafel extends JPanel implements MouseListener, MouseMotionListener {

  private ArrayList<Grafikobjekt> virtuellerBildschirm = new ArrayList<Grafikobjekt>();

  private int x = -10;
  private int y = -10;
  private StartFenster auswahl;

  public Tafel(StartFenster auswahl) {
    this.auswahl = auswahl;
    this.setBackground(Color.WHITE);
    this.addMouseListener(this);
    this.addMouseMotionListener(this);
  }

  public void paintComponent(Graphics g0) {
    Graphics2D g = (Graphics2D) g0;
    super.paintComponent(g);
    
      for (int z = 0; z < virtuellerBildschirm.size(); z++) {
        Grafikobjekt go = virtuellerBildschirm.get(z);
        if(go.isGefuellt()) {
          g.fill(go.getForm());
        } else {
          g.draw(go.getForm());
        }
      }
  }
  
  public void zeichneAufVirtuellenBildschirm(double dx, double dy) {
    Shape werkzeug;
    
    if ( auswahl.quadratBtn.isSelected() ) {
      werkzeug = new Rectangle2D.Double(dx, dy, auswahl.getLinienstaerke(), auswahl.getLinienstaerke());
    } else {
      werkzeug = new Ellipse2D.Double(dx, dy, auswahl.getLinienstaerke(),auswahl.getLinienstaerke());
    }
    virtuellerBildschirm.add(new Grafikobjekt(new Point(x, y), werkzeug, auswahl.fuellen.isSelected() ));

    this.repaint();
  }

  @Override
  public void mouseDragged(MouseEvent e) {
    System.out.println("Dragged");
    x = e.getX();
    y = e.getY();
    
    zeichneAufVirtuellenBildschirm(x,y);
  }

  @Override
  public void mousePressed(MouseEvent e) {
    System.out.println("Pressed");
    x = e.getX();
    y = e.getY();
    
    zeichneAufVirtuellenBildschirm(x,y);
  }
}

Nach der Übergabe der x und y-Koordinaten in die zeichneAufVirtuellenBildschirm-Methode findet ein sogenannter impliziter Cast statt da wir im Methodenkopf statt int nun double verwenden (Zeile 46). Von einem impliziten Cast spricht man wenn ein niederwertiger Datentyp in einen höherwertigen umgewandelt wird. Solch ein Cast muss in Java nicht angegeben werden, der Compiler übernimmt das selbst.

Nach der Deklaration von Shape werkzeug verwenden wir die if-Abfrage um das Shape entweder mit einer Ellipse oder einem Quadrat zu initialisieren (Zeile 49 – 53).

Da Sowohl Ellipse2D als auch Rectangle2D die nur in Verbindung mit Graphics2D funktionieren, müssen wir in Zeile 33 unser Graphics-Objekt in ein Graphics2D-Objekt casten. Graphics2D ist eine abstrakte Klasse, die wesentlich mehr Methoden zur Verfügung stellt als eine normale Graphics-Klasse.

Schließlich haben wir in den Zeilen 38 bis 42 wesentlich weniger zu tun und können bequem das Shape zeichnen lassen.

StartFenster.java

package miniMalen1;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;

import javax.swing.Action;
import javax.swing.ButtonGroup;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JSpinner;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.SpinnerNumberModel;

public class StartFenster extends JFrame {

  public static void main(String[] s) {
    new StartFenster();
  }

  private ArrayList<Grafikobjekt> virtuellerBildschirm = new ArrayList<Grafikobjekt>();
  private JPanel panel;
  private int x = -10;
  private int y = -10;
  JRadioButton kreisBtn;
  JRadioButton quadratBtn;
  JSpinner linienstaerke;
  JCheckBox fuellen;
  ButtonGroup werkzeugspitzen = new ButtonGroup();
  private JToolBar auswahl;

  public StartFenster() {
    initialisiereStartFenster();
  }

  private void initialisiereStartFenster() {
    this.setDefaultCloseOperation(EXIT_ON_CLOSE);
    this.setBounds(0, 0, 500, 500);
    this.setLocationRelativeTo(null);
    this.setAlwaysOnTop(true);
    this.setBackground(Color.white);
    this.setTitle("Tafel");
    this.setLayout(new BorderLayout());

    auswahl = new JToolBar();
    auswahl.setPreferredSize(new Dimension(300, 30)); // PreferredSize = bevorzugte Größe
    auswahl.setFloatable(true);
    
    kreisBtn = new JRadioButton("Kreis");
    quadratBtn = new JRadioButton("Quadrat");
    werkzeugspitzen.add(kreisBtn);
    werkzeugspitzen.add(quadratBtn);
    
    fuellen = new JCheckBox("Füllen");
    auswahl.add(fuellen);
    
    auswahl.add(kreisBtn);
    auswahl.add(quadratBtn);

    SpinnerNumberModel nummern = new SpinnerNumberModel(10.0, 1.0, 99.0, 1.0); // double Werte enthalten immer Kommaangaben ( default, Minimum, Maximum, Schrittweite )
    linienstaerke = new JSpinner(nummern);
    linienstaerke.setMinimumSize(new Dimension(50, 30)); // setzt die Mindestgröße ( Breite, Höhe )
    linienstaerke.setMaximumSize(new Dimension(50, 30)); // setzt die Maximalgröße

    auswahl.add(linienstaerke);
    this.add(auswahl, BorderLayout.PAGE_START);
    
    Tafel tafel = new Tafel(this);
    this.add(tafel);

    sichtbar();
  }

  private void sichtbar() {
    this.setVisible(true);

  }
  
  public double getLinienstaerke() {
    double staerke = (double) this.linienstaerke.getValue();
    return staerke;
  }
}

In StartFenster.java muss zunächst der JSpinner mit double-Werten angepasst werden (Zeile 66). double-Werte kennzeichnet man einfach indem man immer Kommawerte nimmt.

Dann passen wir den getter getLinienstaerke() an (Zeile 85 – 87), da sowohl Rectangle2D wie auch Ellipse2D double-Werte verwendet (Zeilen 50 und 52 in Tafel.java). Ansonsten würde es eine Fehlermeldung geben.
Da linienstaerke.getValue() einen Wert vom Typ Object zurückgibt, müssen wir ihn hier explizit in ein double casten.

Unser Quellcode ist nun nocheinmal effizienter geworden und eröffnet uns weitere schöne Möglichkeiten. Als nächstes wollen wir einen Farbwähler hinzufügen.

Ich glaube jetzt ist wieder höchste Zeit für einen Kaffee.