Sinus-Kurve mit MVC


Eine Sinuskurve Die folgende Swing-Application demonstriert die Aufteilung eines Programms nach dem MVC-Prinzip: die Berechnung der Daten für die grafische Darstellung einer Sinuskurve erfolgt in einem separaten Objekt, dem sogenannten Model.

Das Model ist ein Objekt aus der Klasse SinusModel.class. Es berechnet einen Array von Werten für den Verlauf der Sinuskurve; es erzeugt dann ein Event-Objekt aus der Klasse SinusEvent.class. Dieses Event-Objekt bekommt als Field den Array mit den Daten des Kurvenverlaufs. Das Event-Objekt wird vom Model-Objekt an das Panel-Objekt aus der Klasse SinusModelPanel.class geschickt. Das Panel-Objekt entnimmt dem Event-Objekt den Array mit den Daten und zeigt die Sinus-Kurve grafisch an, siehe Screen-shot.

Das Design des Programms

Die Klasse SinusFrame ist die Hauptklasse des Programms. Der SinusFrame deklariert Model und Panel als Interfaces. Der Konstruktor von SinusFrame erzeugt ein SinusModel-Objekt mit einer beliebigen Anzahl zu berechnender Bildpunkte. Zweitens erzeugt die Hauptklasse ein SinusPanel-Objekt zur grafischen Anzeige der Bildpunkte. Das Panel wird beim Model als SinusModelListener registriert: Das Model weiss daher, dass das Panel verständigt werden muss, wenn frische Daten im Model vorliegen. Das ist in der vorliegenden Fassung des Programms nur beim Programmstart der Fall. Das Neuzeichnen der Sinuskurve nach dem Verändern der Grösse des Frame mit der Maus übernimmt das Panel allein in der Methode repaint(), da hierbei keine neuen Daten für die Sinuskurve als solche anfallen.

Das SinusModel berechnet die Werte für die Sinuskurve mittels der Methode rechnen(). In dieser Methode wird auch das Benachrichtigen des Panel geregelt: Das SinusModel unterhält eine java.util.Collection von SinusModelListener-Objekten, in diesem Fall nur 1 Objekt, nämlich das Panel. (Man könnte auch einen Array voller Listener-Objekten bauen.)

Das SinusPanel richtet sich in seiner Grösse nach dem zur Verfügung stehenden Platz, in einem JFrame kann der Benutzer die Grösse nach Bedarf mit der Maus regeln. Nach der aktuellen Grösse richtet sich die Schrittweite auf der x-Achse, wenn die Daten aus dem Model mittels der Methode paintComponent( Graphics g ) angezeigt werden.

Das Programm besteht also aus 3 Modulen:

Bei Vorliegen neuer Daten wird im Model ein Event-Objekt erzeugt, das die frischen Daten enthält:

Model und Panel implementieren entsprechende Interfaces:

Der SinusModelListener, also hier das Panel, wird mit der Methode geaendert( SinusEvent e ) darauf hingewiesen, dass neue Daten im Model vorliegen, die von einem Objekt aus der Klasse SinusEvent überbracht werden.

Das SinusModelInterface registriert erstens Klassen, die an den Daten interessiert sind: addSinusModelListener( SinusModelListener l ) Zweitens kommte das Model manchmal in die Lage, die Werte berechnen zu müssen, dafür die Methode rechnen() Die Methode getyWerte() hilft beim Testen, aber nicht nur dort: sie bringt die Daten.

Der Test des Programms

Das Programm ist getestet mittels des Test-Frameworks mit Unit-Test.

Die einzelnen Klassen, die am Programm beteiligt sind, wurden mittels Unit-Testing getestet wie folgt:

Die Hauptklasse des Testlaufs heisst TestLauf

Der Test verlief wie folgt:
Der Test des Sinus-Programms
Man sieht ein negatives Testergebnis im 5ten Test beim SinusPanel, vermutlich ein Rundungsfehler beim Wert von sin( 2*pi ).

Dokumentation des Programms

Die hier unten nachfolgenden Quellcode-Dateien werden in passenden Ordnern gespeichert und anschliessend mit javadoc dokumentiert:
C:>javadoc -d sinus/doku sinus sinus.sinustest
Die gesamte Doku landet im Ordner /sinus/doku, der vorher angelegt werden muss.


zum Testframework
zu Swing und MVC

Quellcode des Programms

nach oben | nach unten

class SinusFrame

package sinus;

import javax.swing.JFrame;
import javax.swing.JPanel;

/** Der Frame als Hauptklasse*/
public class SinusFrame extends JFrame
{
    private SinusModelInterface smi;
    private SinusModelListener sml;

    /** Konstruktor mit Titel und Anzahl der Bildpunkte */
    public SinusFrame( String b, int i )
    {
        super( b );
        smi = new SinusModel( i );
        sml = new SinusPanel();

        smi.addSinusModelListener( sml );
        getContentPane().add( (JPanel)sml );
        setSize( 456, 345 );
        smi.rechnen();
    }

    public SinusModel getSinusModel()
    {
        return (SinusModel)smi;
    }
    public SinusPanel getSinusPanel()
    {
        return (SinusPanel)sml;
    }

    public static void main( String [] args )
    {
        SinusFrame sf = new SinusFrame( "Sinus", 123 );
        sf.setVisible( true );
    }
}
nach oben | nach unten

class SinusModel

package sinus;

import java.util.Collection;
import java.util.ArrayList;
import java.util.Iterator;

/** SinusWerte auf der y-Achse berechnen. Im Konstruktor wird
 *  für alle Klassen verbindlich festgelegt, wieviele Werte das sind.
 *  Nach der bekannten Formel mit 2 PI usw.
 *  Unterhält eine Collection aller SinusModelListener;
 *  diese werden verständigt, wenn neue Daten im Model vorliegen.
 */
public class SinusModel implements SinusModelInterface
{
    Collection sinusModelListeners;
    private int anzahl;
    private double yWerte[];

    /**  Anzahl der Bildpunkte und Collection der Listener.*/
    public SinusModel( int a )
    {
        anzahl = a;
        yWerte = new double[a];
        sinusModelListeners = new ArrayList();
    }

    public Collection getSinusModelListeners()
    {
        return sinusModelListeners;
    }
    /**  Die Werte nach der bekannten Sinus-Formel.
     *   Anschliessend alle Listener verständigen mittels neuem SinusEvent-Objekt,
     *   das die errechneten Werte mitbringt.
     */
    public void rechnen()
    {
        for( int i = 0; i < anzahl; i++ )
        {
            yWerte[i] =  Math.sin( 2*Math.PI*i/anzahl );
        }
        Iterator it = sinusModelListeners.iterator();
        while(it.hasNext())
        {
            Object o = it.next();
            SinusModelListener sml = (SinusModelListener)o;
            sml.geaendert( new SinusEvent( yWerte ));
        }
    }

    /** */
    public int getAnzahl()
    {
        return anzahl;
    }
    /** */
    public double[] getyWerte()
    {
        return yWerte;
    }

    /**  */
    public void addSinusModelListener( SinusModelListener l )
    {
        sinusModelListeners.add( l );
    }
}
nach oben | nach unten

class SinusPanel

package sinus;

import javax.swing.JPanel;
import java.awt.Graphics;
import java.awt.Color;

/** SinusPanel zur grafischen Anzeige der SinusWerte.
 *  Wenn im SinusModel neue Werte anfallen, wird
 *  SinusPanel als SinusModelListener verständigt,
 *  bekommt ein SinusEvent, entnimmt diesem die Daten,
 *  rechnet sie auf die aktuelle AnzeigeGrösse um,
 *  und zeigt sie an.
 */
public class SinusPanel extends JPanel implements SinusModelListener
{
    private double xs[];
    private double ys[];
    private int breite, hoehe;  // des Panels
    private int anzahl;         //der Bildpunkte
    private double schrittweite;//zwischen den Bildpunkten

    /** Anzahl der Bildpunkte. */
    public int getAnzahl()
    {
        return anzahl;
    }
    /** Array der SinusWerte aus dem SinusEvent.
     * Das sind die Werte für die Y-Achse, die hier noch auf die Höhe des
     * Panel umgerechnet werden.
     */
    public double[] getYs()
    {
        return ys;
    }
    /** Array der Werte auf der X-Achse.
     * Das sind die Werte für die X-Achse, die hier noch auf die Breite des
     * Panel umgerechnet werden. Der erste Wert ist 0.
     */
    public double[] getXs()
    {
        return xs;
    }

    /** Schrittweite zwischen Bildpunkten auf der X-Achse.
     *  Richtet sich nach der aktuellen Breite des Panel.
     */
    public double getSchrittweite()
    {
        return schrittweite;
    }

    /**  Man bekommt ein frisches Event mit neuen Werten.
     *   Bringt Array der SinusWerte mit und damit Anzahl der Bildpunkte.
     */
    public void geaendert( SinusEvent e )
    {
        anzahl = e.getSinusWerte().length;
        ys = e.getSinusWerte();
        xs = new double[anzahl];
        repaint();
    }
    /** Bildpunkte zeichnen.
     *  Höhe und Breite bestimmen, daraus Schrittweite.
     */
    public void paintComponent( Graphics g )
    {
        int x1 = 0;
        int x2 = 0;
        int y1 = 0;
        int y2 = 0;

        super.paintComponent(g);
        setBackground( Color.white );
        g.setColor( Color.black );

        breite = getWidth();
        hoehe = getHeight();
        schrittweite = breite/(double)anzahl;

        xs[0] = 0;
        for( int i = 1; i < anzahl; i++ )
        {
            xs[i] = xs[i-1] + schrittweite;
        }
        ys[0] = 0;
        for( int i = 0; i < anzahl-1; i++ )
        {
            x1 = (int)(xs[i]);
            x2 = (int)(xs[i+1]);
            y1 = (int)(hoehe/2 - hoehe/2*ys[i]);
            y2 = (int)(hoehe/2 - hoehe/2*ys[i+1]);
            g.drawLine(x1, y1, x2, y2);
      }
      g.drawLine( x2, y2, (int)(x2+schrittweite), (int)(hoehe/2) );
  }
}
nach oben | nach unten

class SinusEvent

package sinus;

/** Das SinusEvent hat die aktuellen SinusWerte.
 *  Die SinusWerte sind in einem Array
 */
public class SinusEvent
{
    double[] sinusWerte;

    /** Konstruktor wird aufgerufen von SinusModel.
     *  Beim Vorliegen neuer Werte in double Array.
     */
    public SinusEvent( double[] ys )
    {
       int lang = ys.length;
       sinusWerte = new double[lang];
       sinusWerte = ys;
    }
    /** */
    public double[] getSinusWerte()
    {
        return sinusWerte;
    }
}
nach oben | nach unten

class SinusModelListener

package sinus;

/** Reagiert auf ein SinusEvent.
 *  Ein neues SinusEvent wird erzeugt, wenn neue Werte im Model vorliegen.
 *
 */
public interface SinusModelListener
{
    /** Das Model hat frische Daten.
     *  Die frischen Daten werden vom SinusEvent überbracht.
     */
    public void geaendert( SinusEvent e );
}
nach oben | nach unten

class SinusModelInterface

package sinus;

/** Interface SinusModel.
 *  Das SinusModel berechnet SinusWerte auf der y-Achse.
 *
 */
public interface SinusModelInterface
{
    /** */
    public void addSinusModelListener( SinusModelListener l );

    /** den Array der errechneten Werte herausgeben*/
    public double[] getyWerte();

    /** Einen Array von SinusWerten berechnen*/
    public void rechnen();
}
nach oben | nach unten

class SinusModelTest

package sinus.sinustest;

import test.*;
import sinus.SinusModel;
import sinus.SinusPanel;

/** Das SinusModel testen.*/
public class SinusModelTest extends AbstractTest
{
    /** Testet getAnzahl().  Testet getyWerte().length,
     *  den ersten und den letzten Wert im yWerte ( beide sind 0),
    *   Testet addSinusModelListener(SinusPanel sp),
    *   getSinusModelListeners(),  rechnen()
    *   und dann, ob der Listener die korrekte Anzahl Werte erhalten hat.
    */
    public void lauf( TestErg t ){

        ergebnis( " SinusModel - " );

        try
        {
             int anz = 234;

             SinusModel sm = new SinusModel( anz );
             ergebnis( sm.getAnzahl() == anz );
             ergebnis( sm.getyWerte().length == anz );

             double arr[] = sm.getyWerte();
             ergebnis( arr[0] == 0 );
             ergebnis( arr[anz-1] == 0 );

             SinusPanel sp = new SinusPanel();
             sm.addSinusModelListener( sp );

             ergebnis( sm.getyWerte().length == anz );
             ergebnis( sm.getSinusModelListeners().contains( sp ));

             sm.rechnen();
             ergebnis( sp.getYs().length == anz );
        }
        catch( Exception e )
        {
            ergebnis( e.toString());
        }
        t.aufsammeln( getErg() );
    }
}
nach oben | nach unten

class SinusPaneltest

package sinus.sinustest;

import test.*;
import sinus.SinusPanel;
import sinus.SinusEvent;
import sinus.SinusModel;
import java.awt.Dimension;

/** Das SinusPanel testen.*/
public class SinusPanelTest extends AbstractTest
{
    /** Erzeugt ein SinusModel und dann ein SinusPanel.
     * Testet Anzahl der Werte im Array des SinusModels,
     * Übergabe dieses Arrays der SinusWerte von Model an Panel.
     * getAnzahl(), den Wert des ersten Elements ( nämlich 0),
     * getXs().length, getYs().length,
     * setSize(), getWidth()
     */
    public void lauf( TestErg t ){

        ergebnis( " SinusPanel - " );

        try
        {
            int anz = 124;

             SinusPanel sp = new SinusPanel();  // wird getestet

             SinusModel sm = new SinusModel( anz );
             ergebnis( sm.getAnzahl() == anz );  //r

             sm.addSinusModelListener( sp );     //das Panel beim Modell registrieren

             sm.rechnen();
             ergebnis( sp.getAnzahl() == anz );  //r
             ergebnis( sp.getXs().length == anz ); //r

             double arr[] = sp.getYs();
             ergebnis( arr[0] == 0 );              //r
             ergebnis( arr[anz-1] == 0 );          //FALSE, Rundungsfehler


             double[] werte  = {1,2,3,4,5};
             SinusEvent se = new SinusEvent( werte );
             sp.geaendert( se );
             ergebnis( sp.getYs().length == werte.length ); //r
             ergebnis( sp.getXs().length == 5 );            //r

             int breite = anz*4;   //um die schrittweite zu testen
             sp.setSize( new Dimension( breite, breite ));
             ergebnis( sp.getWidth() == breite );         //r
             sp.geaendert( se );
             //ergebnis( sp.getSchrittweite() >= 0 );        //FALSE, aber warum??
             ergebnis( sp.getHeight() == breite );           //r

        }catch( Exception e ){

            ergebnis( e.toString());
        }

        t.aufsammeln( getErg() );

    }
}
nach oben | nach unten

class SinusEventTest

package sinus.sinustest;

import test.*;
import sinus.SinusEvent;

/** Das SinusEvent testen.*/
public class SinusEventTest extends AbstractTest
{
    /** Testet Länge des Arrays der SinusWerte
    */
    public void lauf( TestErg t ){

        ergebnis( " SinusEvent - " );

        try{
            double[] werte  = {1,2,3,4};
            SinusEvent se = new SinusEvent( werte );

            ergebnis( se.getSinusWerte().length == 4 );

        }catch( Exception e ){

            ergebnis( e.toString());
        }

        t.aufsammeln( getErg() );

    }
}
nach oben | nach unten

class SinusFrametest

package sinus.sinustest;

import test.*;
import sinus.SinusFrame;
import javax.swing.*;

/** Den SinusFrame testen.*/
public class SinusFrameTest extends AbstractTest
{
    public void lauf( TestErg t )
    {
        ergebnis( " SinusFrame - " );

        try{
            SinusFrame sf = new SinusFrame( "Test", 200 );

            ergebnis( sf.getSinusModel().getyWerte().length == 200 );

        }catch( Exception e ){

            ergebnis( e.toString());
        }
        t.aufsammeln( getErg() );
    }
}
nach oben | nach unten

class TestLauf

package sinus.sinustest;

import test.*;

/** Hauptklasse aller EinzelTests.
 * Und zwar: SinusModelTest, SinusPanelTest, SinusEventTest, SinusFrameTest
 */
public class TestLauf extends AbstractTestLauf
{
    /** Alle Tests werden hier aufgerufen.
     *  Aufruf erfolgt durch Aufruf des Konstruktors des Tests.
     */
    static public void main( String [] args )
    {
       new SinusModelTest();
       new SinusPanelTest();
       new SinusEventTest();
       new SinusFrameTest();

        alleTesten( erg );
        erg.auswerten();
        System.exit(0);
    }
}

nach oben
zum Test-Framework
zu Swing und MVC