Klassisches MVC

AndiE

Top Contributor
Ich stelle mal ein Thema, das mir etwas peinlich ist. Aber ich werde aus den Tutorials im Netz nicht schlau.

Es geht darum, dass es ein Datenmodell gibt, das ein Array x*y ist. Beim Start wird das Array ohne Daten angezeigt. Das Array wird mit Swing auf ein Panel gezeichnet.

Um das Daten reinzubekommen, gibt es für mich grundsätzlich 2 Möglichkeiten.

1. Der Nutzer klickt auf die Ansicht. Es wird ein Dialogfeld aufgerufen. Dort kann man den Eintrag in das entsprechende Kästchen ändern, dessen Koordinaten man aus der Position der Maus beim Klicken ermitteln kann. Bei Bestätigung mit dem "OK"-Button wird das Datenmodell geändert und die Ansicht neu gezeichnet.

2. Das Datenmodell wird in ein Serialisierungsobjekt überführt. Dann kann man es auch leeren, abspeichern oder eben mit dem Befehl Laden mit Daten füllen. Nun muss auch die Ansicht neu gezeichnet werden.

Problem: Wie macht man das mit Swing? So richtig habe ich keine Hilfe gefunden, wie man eine MVC-Architektur in so einem Fall umsetzt. Es geht auch darum, wie man den modalen Dialog verwendet und erstellt.

Ich weiß, dass das üblicherweise Anfängerwissen sein sollte, aber ich habe im Moment keinen Plan.
 

mihe7

Top Contributor
Ganz grundsätzlich trennst Du die Ansicht von den Daten, die angezeigt werden sollen. Die Komponente hat als View ein Model, das die anzuzeigenden Daten so liefert, wie die Komponente sie benötigt. Das Model informiert per Events über Änderungen. Die View registriert sich als Listener beim Model und zeichnet sich selbst, wenn das Model ein solches Event auslöst.
 

AndiE

Top Contributor
Die Funktion soll so sein:
Bei Mausklick: Zeige Dialog an, in den Daten eingegeben werden
Bei Klick auf den OK-Button des Dialogs: Verändere Daten. Erzwinge Neuzeichnen der Ansicht.

Problem1: Das Zeichnen und die Handhabung des Dialogs

Der MouseInputListener sitzt in der JPanel. JDialg erwartet aber ein Frame. Wie komme ich an das Frame?

Wie erstelle ich den Dialog? Erweitere ich die JDialog-Klasse( extends JDialog)? Oder gehe ich eher so vor, als ob ich ein JPanel befülle.
Aber wie auch immer- Wie rufe ich das dann auf( im modalen Modus), und erhalte die Daten?

Das blöde ist nämlich, dass ich so etwas lange nicht gedacht habe, und noch nie unter Java und Swing. Und bei meinen aktiven Versuchen( auch schon lange her) war ich MFC-verwöhnt, wo das so geht:

MeinDialog erbt von CDialog
md= new MeinDialog()

if(md.doModal()==OK)
//Übertrage Daten

Das wird in Swing so nicht gehen. aber wie das geht, habe ich noch nicht rausgefunden.

Problem2: Update der View bei Änderung des Models

Ich rufe "repaint" auf, das sich klassischer Weise in der Panel-Klasse befindet.

Von der Logik her, müsste ich eine Methode "JPanel:dataChanged()" erstellen, die dieses "repaint" ausführt. Wenn ich im Model Daten geladen habe, kann ich dann diese Methode aufrufen, um die Ansicht neu zu zeichnen. Oder ich male beim Schließen des Dialogs die Ansicht neu.

Habe ich das so einigermaßen richtig verstanden, oder gibt es bei Swing noch einen anderen Mechanismus?
 

White_Fox

Top Contributor
Naja...ich habe da auch drei Anläufe gebraucht, bis ich eine brauchbare Lösung hatte. Ich hatte damals sowas gebaut:

Für das Model:
Java:
public interface ModelsViewLinkable{
    //Alle Methoden, die es der View ermöglichen, sich die Daten des Models anzusehen.
    //Nur Lesezugriff, keine schreibenden oder datenverändernden Methoden.
}


public class Model implements ModelsViewLinkable{
    //Alles, was dein Model eben so tut zur Datenhaltung.
}

Der Controller:
Java:
public interface ControllersViewLinkable {
    /*
    Alle Methoden, die der Controller benötigt um auf dem Model etwas zu tun.
    In meiner Implementation gibt es nur eine Methode:
    
    public void requestCommand(Command c);
    
    Diese Methode nimmt ein Command-Objekt entgegen, daß von der View erstellt
    und mit dieser Methodem dem Controller übergeben und ausgeführt wird.
    */
}

Die View:
Java:
public interface ViewsControllerLinkable {
    /*
    Interface für den Fall, daß der Controller über die View
    dem Benutzer etwas mitzuteilen hat. Z.B. falls ein Kommando
    nicht ausgeführt werden kann.
    */
}


public class View implements ModelObservable, ViewdataControllable, ViewsControllerLinkable{
    
}

Ich hoffe, ich habe nichts vergessen.
Am besten ist, du überlegst erst für jedes Interface einzeln, welche Methoden für dessen Einsatz genau gebraucht werden. Wenn du hier Mist baust, z.B. versehentlich Schreibmethoden wo du eigentlich nur lesen sollen willst, hast du sehr schnell sehr viel Arbeit. Die exakte Implementierung ist weitaus weniger kritisch.
 

mihe7

Top Contributor
Habe ich das so einigermaßen richtig verstanden, oder gibt es bei Swing noch einen anderen Mechanismus?
MVC gibt es in verschiedenen Varianten, das hat aber nichts mit Swing zu tun. Klassischerweise wird gemacht, was ich oben geschrieben habe: die View registriert sich als Listener (Observer/Beobachter-Pattern) beim Model und das Model informiert seine Beobachter über Änderungen.

Du willst ja etwas wie ein Gantt-Chart haben. Da sehe ich mal drei Modelle: eines für die Zeitachse, eines für die Ressourcenachse und eines für die Elemente/Aufgaben.

Machen wir mal das Modell für die Zeitachse:
Java:
public interface TimeAxisModel {
    void addPropertyChangeListener(PropertyChangeListener listener);
    void removePropertyChangeListener(PropertyChangeListener listener);

    YearMonth getYearMonth();
}
Eine Implementierung:
Java:
public class DefaultTimeAxisModel implements TimeAxisModel {
    private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);

    private YearMonth yearMonth = YearMonth.now();

    public void addPropertyChangeListener(PropertyChangeListenre listener) {
        pcs.addPropertyChangeSupport(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener) {
        pcs.removePropertyChangeListener(listener);
    }

    public YearMonth getYearMonth() {
        return yearMonth;
    }

    public void setYearMonth(YearMonth newYearMonth) {
        YearMonth oldValue = yearMonth;
        yearMonth = newYearMonth;
        pcs.firePropertyChange("yearMonth", oldValue, yearMonth);
    }
}
Die Komponente, die die Zeitachse zeichnet, erhält das Model, registriert sich selbst als Listener und ruft bei Änderungen repaint auf:
Java:
public class TimeAxis extends JComponent {
    private TimeAxisModel model;
    private PropertyChangeListener listener = e -> repaint();

    public TimeAxis(TimeAxisModel model) {
        this.model = model;      
    }

    private void registerListeners() {
        if (model != null) {
            model.addPropertyChangeListener(listener);
        }
    }

    private void unregisterListeners() {
        if (model != null) {
            model.removePropertyChangeListener(listener);
        }
    }

    @Override
    public void addNotify() {
        super.notifyAdded();
        registerListeners();
    }

    @Override
    public void removeNotify() {
        super.notifyRemoved();
        unregisterListeners();
    }

    // weitere Methoden z. B. getModel, setModel, getPreferredSize, paintComponent etc.
}

Wenn Du jetzt die Komponente TimeAxis verwendest, informiert das DefaultTimeAxisModel über einen PropertyChangeEvent eben diese Komponente über Änderungen, d. h. über Aufrufe von setYearMonth - und die Komponente zeichnet sich dann selbst.

Ich habe hier mal PropertyChangeListener verwendet, weil der nichts mit Swing zu tun hat. Du kannst natürlich auch eigene Events und Listener verwenden.
 

AndiE

Top Contributor
Ich habe das jetzt erstmal soweit gelöst.

Java:
package presence;

    
 
public class PresenceGrid {
    /**
     * Create the GUI and show it.  For thread safety,
     * this method should be invoked from the
     * event-dispatching thread.
     */
    
    public static void main(String[] args) {
        //Schedule a job for the event-dispatching thread:
        //creating and showing this application's GUI.
        
        //Anlegen und Bekanntmachen der Komponenten
        GridView gv= new GridView();
        GridData gd= new GridData();
        gv.getData(gd);
        gv.start();
    }
    
    
}

Java:
package presence;

import javax.swing.JFrame;

public class GridView {

        private GridData gd;
        
        public void getData( GridData data) {
            gd=data;
        }
        private void createAndShowGUI() {
            //Create and set up the window.
            JFrame frame = new JFrame("HelloWorldSwing");
            
            
    
            Grid g= new Grid();
            g.setData(gd);
            g.setParent(frame);
            
          
              frame.setContentPane(g);
            //Display the window.
            frame.pack();
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        }
    
        public void start(){
            javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGUI();
            }
        });
    }
    
}

Java:
package presence;

public class GridData {
    
    //Verweis auf Ansicht
    private Grid grid;
    
    
    public GridData() {
        
    }

    public String Marker(int zeile, int spalte) {
        // TODO Auto-generated method stub
        return "*";
    }

    public int getCount() {
        // TODO Auto-generated method stub
        return 20;
    }

    public int getDaysofMonth() {
        // TODO Auto-generated method stub
        return 31;
    }

    public void getPanel(Grid g) {
        grid=g;
        
    }
    
    

}

Code:
package presence;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.MouseEvent;

import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.event.MouseInputListener;

public class Grid extends JPanel implements MouseInputListener {
    
    private int x;
    private    int y;
    private JFrame parent;
    private GridData gd;
    private int count;
    private int daysofmonth;
    
    
        
    public  Grid() {
        setPreferredSize( new Dimension( 1000,600));
        this.addMouseListener(this);
        x=0;
        y=0;
        
    }
    
    public void setParent(JFrame f) {
        parent=f;
    }
    
    protected void paintComponent(Graphics g) {
        
        int HEIGHT=25;
        int NAMESPACE=200;
            
        super.paintComponent(g);
        
        int i;
        int j;
        int xPos;
        int yPos;
        
        
        
        for(j=0;j<daysofmonth;j++) {
            xPos=NAMESPACE+HEIGHT*j;
            g.drawRect(xPos,0,HEIGHT,HEIGHT);
            g.drawString(Integer.toString(j+1),xPos,HEIGHT);
            
        }
        for(i=0;i<count;i++) {
            g.drawRect(0,(i+1)*HEIGHT,HEIGHT,HEIGHT);
            g.drawString(Integer.toString(i+1),0,(i+2)*HEIGHT);
            g.drawRect(0,(i+1)*HEIGHT,NAMESPACE,HEIGHT);
            for(j=0;j<daysofmonth;j++) {
                xPos=NAMESPACE+j*HEIGHT;
                yPos=(i+1)*HEIGHT;
                g.drawRect(xPos,yPos,HEIGHT,HEIGHT);
                g.drawString(checkPos(i,j),xPos+2,yPos+HEIGHT-2);
                }
        }
                    
        
    }

    public void setData(GridData griddata) {
        gd=griddata;
        count=gd.getCount();
        daysofmonth=gd.getDaysofMonth();
        gd.getPanel(this);
    }
    
    public String checkPos(int zeile,int spalte) {
        return gd.Marker(zeile,spalte);
    }
    
    @Override
    public void mouseClicked(MouseEvent e) {
        x=e.getX();
        y=e.getY();
        
        JDialog d = new JDialog(parent, "dialog Box");
        
        // create a label
        JLabel l = new JLabel("this is a dialog box");
        d.add(l);
        d.setSize(100, 100);
        d.setVisible(true);        // create a label
      
        // set visibility of dialog
        d.setVisible(true);   
        
        
        this.repaint();
    }

    @Override
    public void mousePressed(MouseEvent e) {
        // TODO Auto-generated method stub
        
    }

    @Override
    public void mouseReleased(MouseEvent e) {
        // TODO Auto-generated method stub
        
    }

    @Override
    public void mouseEntered(MouseEvent e) {
        // TODO Auto-generated method stub
        
    }

    @Override
    public void mouseExited(MouseEvent e) {
        // TODO Auto-generated method stub
        
    }

    @Override
    public void mouseDragged(MouseEvent e) {
        // TODO Auto-generated method stub
        
    }

    @Override
    public void mouseMoved(MouseEvent e) {
        // TODO Auto-generated method stub
        
    }

    

}

Ich finde das noch nicht seh schön. Aber da jetzt ein Observer-Pattern zu implementieren, traue ich mir nicht zu.

Einiges ist auch erst angedacht, aber für mich ist die Richtung klar.
 

mihe7

Top Contributor
Aber da jetzt ein Observer-Pattern zu implementieren, traue ich mir nicht zu.
Warum nicht? Du brauchst einfach a) ein Interface für das Model. Das hat Methode(n) zum Registrieren von Observern (aka LIstener in Swing) und b) eine View, die eben einen Listener registriert. Die Implementierung des Models muss lediglich die Methode des Listeners aufrufen.

Du könntest z. B. auch einfach etwas machen wie
Java:
interface UpdateListener {
    void updated();
}
und im Model
Java:
private List<UpdateListener> listeners = new ArrayList<>();

public void addUpdateListener(UpdateListener listener) { listeners.add(listener); }
protected void fireUpdated() {
    for (UpdateListener listener : listeners) {
        listener.update();
    }
}

Wenn immer sich etwas am Model geändert hat, ruft das Model halt fireUpdated auf.

Die View registriert einfach einen UpdateListener. Die Implementierung macht einfach ein repaint.
Java:
model.addUpdateListener( () -> repaint() );
 

Neue Themen


Oben