PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : [Java] Wie mein Programm für MVC auslegen?


mittelding
2011-05-30, 21:25:10
Hallo!

Gerade programmiere ich an einer Art Paint-Klon in Swing, sprich ein Grafiktool mit geringem Funktionsumfang. Möglich ist neben der Freihandzeichnung das Zeichnen der typischen geometrischen Primitive - also wirklich nichts großartiges. Gerne würde ich dem Code allerdings etwas Struktur geben, d.h. entweder mittels MVC oder Schichten abbilden. Leider habe ich hierbei ein paar Probleme, da ich dies zum ersten mal so machen will.

Swing stellt ja ein Graphics-Objekt zum Zeichnen bereit. Darauf kann man Zeichenoperationen, wie z.B. DrawLine() etc. anwenden. Da es sich nur um Methoden handelt (die werden einmal aufgerufen und vergessen), wäre die Zeichnung nach dem nächsten repaint allerdings verschwunden, deshalb habe ich die Zeichenoperationen in Klassen gekapselt und sie somit sozusagen stateful gemacht, und zwar folgendermaßen:

Ich habe eine abstrakte "Pen"-Klasse, von welcher ich konkrete "Pens" (also Stifte) ableite. Beispielsweise wären das FreeHand, Rectangle, Line und so weiter. Die abgeleiteten Klassen implementieren ihre eigene Logik und Daten (schließlich brauche ich für eine Linie nur 2 Koordinaten, für eine Freihandstrecke jedoch eine ganze Liste an Koordinaten). Desweiteren erbt jede Pen-Klasse eine Methode "draw(Graphics2D g2)" von der abstrakten Pen-Klasse.

Der Rest ist einfach: ich habe eine Liste vom Typ der abstrakten Pen-Klasse, in welche nach und nach meine gezeichneten Objekte in Form der konkreten Pen-Objekte abgespeichert werden. Meine Zeichenfläche (ein JPanel) muss bei jedem Neuzeichen nur über die Pen-Liste iterieren und auf jedes Zeichenobjekt die Methode draw aufrufen, wobei das Graphics-Objekt übergeben wird. Intern in den Pen-Objekten wird dann mithilfe des Graphic-Objekts gezeichnet.


Mein aktuelles Problem: eigentlich sind die Pen-Klassen ja eher Geschäftslogik, gehören also ins Model (denke ich zumindest). Sie benutzen aber intern das Graphics-Objekt des JPanels, welches sie bei jedem draw-Aufruf übergeben bekommen, und dieses Graphics-Objekt gehört ja eigentlich der View, von welchem das Model nichts kennen sollte.


Ich hoffe das Problem ist klar. Wie würdet ihr das lösen? Sollte ich einen ganz anderen Ansatz wählen? Was ich an meinem Ansatz gut finde ist, dass die komplette, Pen-spezifische Zeichenlogik in den jeweiligen Pen-Klassen gekapselt ist und man von außen nur draw aufrufen muss, ohne die "Innereien" zu kennen.


Danke!

edit: noch etwas Code

Malen tu ich so im JPanel:


Graphics2D g2 = (Graphics2D)g;

for(AbstractPen p : PenObjects) {
p.draw(g2);
}



Eine konkrete Pen-Klasse sieht z.B. so aus:


public class LinePen extends PenObject {

public LinePen(PenSettings ps) {
super(ps);
}

@Override
public void draw(Graphics2D g2) {
g2.setColor(ps.getColor());
g2.drawLine(ps.getMouseStartPosition().x, ps.getMouseStartPosition().y, newMousePosition.x, newMousePosition.y);
}

}

Monger
2011-05-30, 22:12:05
...
Mein aktuelles Problem: eigentlich sind die Pen-Klassen ja eher Geschäftslogik, gehören also ins Model (denke ich zumindest). Sie benutzen aber intern das Graphics-Objekt des JPanels, welches sie bei jedem draw-Aufruf übergeben bekommen, und dieses Graphics-Objekt gehört ja eigentlich der View, von welchem das Model nichts kennen sollte.

Du hast ja hier eine Erweiterung der Grafik Bibliothek geschrieben - das ist ganz klar Teil des Models. Eigentlich ist es nichtmal das: den Bereich könntest du komplett aus deiner Software isolieren und in eine eigene DLL stopfen.

MVC ist ein Modell für Fensterapplikationen. Nicht alle Applikationen lassen sich so schön in drei oder vier Schichten teilen. Aber ich würde es so sehen: zur "View" wird es erst dann, wenn es am Layout teil nimmt. Das GUI Design ist nunmal eine sehr eigene Disziplin. Das trifft aber auf deine "Pen" Klasse eigentlich nicht zu, weil du ja keine statischen Pen Objekte auf deine Oberfläche designst. Die entstehen ja erst zur Runtime durch den Anwender.

Trap
2011-05-30, 23:51:45
Grundsätzlich ist Graphics2D in Java nicht zwangsläufig GUI. Man kann z.B. auch auf BufferedImages mit Graphics2D zugreifen.

Daher wär ich nicht der Meinung, dass man allein mit Graphics2D im Interface die Trennung zwischen Model und GUI kaputtmacht.

Ectoplasma
2011-05-31, 08:38:25
Wie du schon richtig erkannt hast, sollten die Pens auf gar keinen Fall am Repaint beteiligt sein. Dieses Konstrukt wäre etwas schräg, weil ein Pen zwar auf einer Zeichenfläche zeichnet, aber nicht selbst Teil dieser ist. Das ist in der Realität ja auch nicht so. Warum speicherts du das Gezeichnete nicht in einer Bitmap? Jedenfalls ist der Pen Teil der Geschäftslogik. Es ist weder ein Model noch ein View oder Controller. Es ist ein Malwerkzeug, etwas ganz Eigenständiges.

peanball
2011-05-31, 09:20:16
Für einen einfachen Paint-Klon wäre dein Modell das Bild (die Bitmap-Daten). Hinzu kommt noch ein Model für die komplette Werkzeugauswahl (Vordergrundfarbe, Füllmuster, Strichstärke, dein Pen, etc.)

Die Zeichenlogik der Pens wäre interessanterweise Teil des controllers, weil dieser das Model beeinflusst. Alle UI Elemente zur Auswahl der pens, Farben und sonstigem wären ebenfalls mit dem Controller verbunden, der die jeweiligen Status im Werkzeug-Model setzt.

Der View ist die dargestellte Bitmap-Fläche und die Werkzeugleiste. Bitmap-Fläche zeigt das Bitmap-Model an, die Werkzeugleiste den Zustand des Werkzeug-models.

Wie Trap geschrieben hat, ist Graphics2D nicht automatisch GUI und damit View. Ein BufferedImage ist ein "Model" für eine Bitmap, die nackten Daten und nicht die Darstellung auf dem Bildschirm.

Deine pens können nach wie vor auf ein Graphics2D malen, das aber nicht vom Swing control (JPanel oder was auch immer du verwendest) sondern vom BufferedImage kommen.

Wenn du dann noch undo-management machen willst, benötigst du mehrere alte Kopien des BufferedImage um diese wiederherzustellen.

Viel Erfolg weiterhin, ein Java-Paint als Übung im coden und SW design ist eine schöne Idee.

mittelding
2011-05-31, 11:59:30
Vielen Dank für die Antworten, bin gerade etwas in Eile aber werde mich im Laufe des Tages wieder konkret damit beschäftigen.

Was ich verschwiegen habe bezüglich eurer Frage, warum ich nicht direkt in eine Bitmap zeichne: das ganze ist etwas mehr als ein Paint-Klon, nämlich so eine Art Netzwerk-Whiteboard mit Client/Server (via RMI), sprich die Clients können alle gleichzeitig malen und die Bilder sind trotzdem mehr oder weniger synchron.

Da dachte ich, ein paar Koordinaten serialisiert zu übertragen (pro "Mal-Aktion" bzw. Pen) ist sicher um ein vielfaches performanter als eine ganze Bitmap, zumal man Bitmaps ja nur schwer bis garnicht abgleichen kann. Bisher konnte ich ja einfach die konkreten Pen-Objekte mittels RMI direkt an den Server schicken, welcher diese an alle Clients verteilt hat und diese die erhaltenen Pen-Objekte nur in ihre "Zeichenliste" aufnehmen mussten und die Sache war gegessen.

peanball
2011-05-31, 16:04:35
Das selbe Prinzip ist weiterhin möglich. Der Pen rendert wie gesagt nur nicht in das Graphics2D vom UI Element sondern in ein BufferedImage.

Jeder Client hat entsprechend lokal eine Bitmap. Damit ist dann auch refresh nach einem Resize und so schneller und einfacher drin. Dann brauchst du nämlich nicht permanent ALLE Renderschritte nacheinander ausführen wenns eine invalidation gibt.

Monger
2011-05-31, 16:55:37
In deinem Fall bietet es sich an, die Anwendung nicht als 3-Tier, sondern als 4-Tier Applikation zu sehen... ob du jetzt deine Daten unten übers Netzwerk rausserialisierst, oder auf Festplatte raus macht letztendlich nicht so den Unterschied.

Manchmal wird einem einiges klarer, wenn man sich frühzeitig überlegt wie das Datenformat aussehen könnte. Deine Pen-Daten sollten ja möglichst leichtgewichtig sein, und damit nicht Änderung in deiner Programmlogik gleich deine Datenschicht brechen, macht es evtl. Sinn die Pen-Daten getrennt von der Pen Logik zu halten.

Das kommt halt auch darauf an, was für Ansprüche da bestehen, und wie groß das alles wird.

mittelding
2011-06-01, 18:17:05
Wie von euch vorgeschlagen habe ich das Zeichnen jetzt ins Model verlagert und nutze dort ein BufferedImage dafür, funktioniert super und macht auch Sinn jetzt, danke.

Das führt mich zu einer Frage:

Das BufferedImage "merkt" sich im Gegensatz zur Zeichenfläche des JPanels alle Zeichenoperationen. Das mag nützlich erscheinen, hat aber auch folgenden Nachteil:
Wenn ich gerade dabei bin, ein Objekt zu zeichnen - nehmen wir beispielsweise mal eine simple Linie - dann ist diese Linie während dem Zeichenvorgang (also während ich bei gedrückter Maustaste die Maus bewege) ja noch ständig in Bewegung bis ich die Maustaste loslasse und die Linie somit fixiert ist.

In diesem Zeitraum bleibt mir aber wohl nichts anderes übrig, als in kürzester Zeit hunderte neue BufferedImages erstellen zu müssen, denn ich will ja nicht die "Spur" sehen, die ich beim Positionieren der Linie hinter mir herziehe.

Geht das besser und falls nicht, könnte das Vorgehen performancetechnisch Probleme bereiten? Also um das nochmal deutlicher auszudrücken, somit erzeuge ich ja im Extremfall pro mit der Maus zurückgelegtem Pixel ein neues BufferedImage.

Etwas Beispielcode. continueDrawing bedeutet, dass der User gerade dabei ist, ein grafisches Objekt "aufzuziehen". Und dabei sehe ich gerade keine andere Möglichkeit, als jedesmal ein neues BufferedImage zu verwenden.


public void continueDrawing(Point newMousePosition) {
if(isDrawing) {
newImage(500,500);
toDraw.setNewMousePosition(newMousePosition);
toDraw.draw(gfx2D);
notifyImageObservers();
}
}

public void newImage(int width, int height) {
bi = gConfig.createCompatibleImage( width, height );
gfx2D = bi.createGraphics();
}


Wie ich zusätzlich dazu noch die bereits existierenden Zeichnungen da draufmale wäre dann nochmal so ein Thema.

Danke!

Trap
2011-06-01, 20:28:07
Du könntest zwischen Preview und festem Eintrag unterscheiden. Das Preview machst du dann eben nur auf den Bildschirm.

mittelding
2011-06-01, 21:31:57
Die Idee hatte ich auch schon, aber damit drehe ich mich im Kreis und stehe wieder vor dem Problem aus dem Startpost. Ich weiß leider nicht, wie ich Zeichenlogik und Daten im Model belasse und die Preview trotzdem im View direkt zeichnen kann (momentan habe ich Zeichenlogik und Daten ja wie gewollt im Model, nur die kostspielige Preview, die ich ebenfalls per BufferedImage aus dem Model abhole, ist das Problem).

Habe interessehalber mal das ganze ausgetestet wie es momentan ist, also auf meinem Rechner läuft es zwar absolut flüssig, aber bei wildem herumwerfen der Maus auf einem 1000x700 BufferedImage geht der Speicherverbrauch auf maximal 480mb hoch. Denke das ist doch zu viel des guten, auch wenn sich keinerlei Probleme anmerken lassen.

Monger
2011-06-02, 00:47:51
Für eine Preview gibt es mehrere Möglichkeiten. So manches Framework bietet da auch schon feste Lösungen an, hab allerdings keine Ahnung was Java da bietet...

Auf jeden Fall ersparst du dir viel Stress, wenn du deine Preview nicht ins bestehende Bild paintest. Eine Möglichkeit z.B. wäre ein "Glas Panel", das genau die selben Dimensionen wie die Zeichenfläche hat, allerdings standardmäßig ein Bild mit vollem Transparenzkanal enthält. Gezeichnet wird beim Mouse Over aufs Glas, und beim loslassen wird der Inhalt des Glas Buffered Image mit dem richtigen Image kombiniert.

Der_Donnervogel
2011-06-02, 01:05:08
Habe interessehalber mal das ganze ausgetestet wie es momentan ist, also auf meinem Rechner läuft es zwar absolut flüssig, aber bei wildem herumwerfen der Maus auf einem 1000x700 BufferedImage geht der Speicherverbrauch auf maximal 480mb hoch. Denke das ist doch zu viel des guten, auch wenn sich keinerlei Probleme anmerken lassen.
Man könnte da schon noch optimieren. Erstens mal könntest du versuchen die Anzahl der Redraws zu reduzieren indem du die gezeichneten Bilder pro Sekunde begrenzt. Wenn du nicht bei jedem Event automatisch malst sondern erst prüfst ob seit dem letzten Redraw z.B. 100 ms (die genaue Zeitspanne müsste man austesten) vergangen sind, dann würde das je nach Anzahl der Events einiges sparen.

Weiters könntest du wenn der User ein MouseDown macht, das Bitmap zu diesem Zeitpunkt merken. Bei jedem folgenden Redraw während der User noch die Taste fest hält kopierst du dann dieses Bitmap und zeichnest in diese Kopie hinein. Somit reicht es dann das letzte Kommando neu zu zeichnen, da der Rest ja schon gezeichnet ist. Bei mehreren Usern ist das ganze zwar etwas kniffliger, aber auch da müsste es gehen, wenn man als Zeitpunkt zum speichern jeweils den frühesten Mousedown aller User nimmt die gerade etwas zeichnen und dann alle zwischenzeitlich gemachten Sachen dazu zeichnet. Wenn er dann los lässt, werden alle seine Kommandos ins gemerkte Bitmap gezeichnet und dieses dann als neue Basis verwendet.

Meine praktische Erfahrung ist allerdings, dass wenn man sauber modelliert, das fast immer auf Kosten von Performance geht (mal mehr mal weniger). Sofern die Software dadurch aber nicht so langsam wird, dass der User Probleme bekommt, fällt das nicht weiter ins Gewicht. Falls doch, kann man immer noch gezielt die Problemstellen optimieren.

mittelding
2011-06-02, 17:32:33
Auf jeden Fall ersparst du dir viel Stress, wenn du deine Preview nicht ins bestehende Bild paintest. Eine Möglichkeit z.B. wäre ein "Glas Panel", das genau die selben Dimensionen wie die Zeichenfläche hat, allerdings standardmäßig ein Bild mit vollem Transparenzkanal enthält. Gezeichnet wird beim Mouse Over aufs Glas, und beim loslassen wird der Inhalt des Glas Buffered Image mit dem richtigen Image kombiniert.


Genau das hatte ich geplant. Das Problem der Architektur bleibt jedoch bestehen, wenn ich jetzt nicht ganz daneben liege ist das mit MVC wohl schlicht nicht möglich.

Betrachten wir es mal abstrakt: das, was sich das View aus dem Model holt ist immer schon ein fertiges Endprodukt. Die "Denkleistung" wurde schon im Model getätigt und die View kann ja auch nicht viel mehr als das angelieferte Endprodukt (leblose Materie sozusagen) anzuzeigen.

Will ich im View aber eine Vorschau machen, dann brauche ich dort wohl Zeichenlogik (die aus dem Model nämlich), die dort eigentlich nicht hingehört.


Weiters könntest du wenn der User ein MouseDown macht, das Bitmap zu diesem Zeitpunkt merken[..].

Das hatte ich ebenfalls eingeplant. Da mich die obige Problematik aber gerade noch aufhält, versuche ich erst mal den Tipp mit der Zeitmessung. Wobei ich schon etwas Bauchschmerzen bekomme - moderne 3D-Spiele mit zig Effekten rendern mit 40-60fps über den Schirm und mein simpler 2D-Strich hätte bei einer 100ms-Schranke 10fps :D

edit: okay, also mit einer Schranke von 30ms (mehr ms geht nicht, man sieht bei 30ms die Verzögerung schon) liege ich immer noch bei 320mb :/


Wie immer ein großes Danke an alle.

Monger
2011-06-02, 18:28:00
...
Betrachten wir es mal abstrakt: das, was sich das View aus dem Model holt ist immer schon ein fertiges Endprodukt.

MVC heißt ja: das Model kommuniziert über die Controller Schicht mit der View (und evtl. nach unten dann mit der Datenschicht). Der Sinn der Sache ist, dass bei einer Änderung der GUI allenfalls die Event Handler neu verbunden werden müssen, die eigentliche Programmlogik davon aber nicht berührt wird.

Da du ja an einem Zeichenprogramm bastelst, ist die Controller Schicht naturgemäß ziemlich komplex: male dies, male jenes, öffnen, speichern, schließen etc.

Da zählt natürlich auch ein Preview rein. Ein Preview verändert deine Sicht, aber nicht den inneren Zustand deines Models. Sie erzeugt allenfalls temporär ein zweites Model. Hat ja nie einer gesagt, dass es nur genau eine View, einen Controller und ein Model geben muss.

mittelding
2011-06-02, 20:48:12
Ich weiß gar nicht, warum ich das BufferedImage im Model bei jeder Mausbewegung neu erzeugen wollte. Belasse es jetzt bei einem einzigen BufferedImage und setze den Hintergrund bei jeder Mausbewegung auf Alphakanal bevor ich neu zeichne (ein "Reset" sozusagen). Speicherverbrauch von 480mb auf 70mb runter, CPU-Load bei wildem herumfuchteln ist maximal bei 35%, ich denke fürs erste wird das ok sein. Alles passiert nach wie vor im Model.

Danke Monger für deine Einwände, deswegen will ich es nicht dabei belassen und noch etwas mehr erfahren:

Da zählt natürlich auch ein Preview rein. Ein Preview verändert deine Sicht, aber nicht den inneren Zustand deines Models. Sie erzeugt allenfalls temporär ein zweites Model. Hat ja nie einer gesagt, dass es nur genau eine View, einen Controller und ein Model geben muss.


An diesem Punkt tu ich mich momentan sehr schwer. [UPS]Erazor hat ja oben bereits gesagt, dass die Zeichenlogik interessanterweise Teil des Controllers wäre. Aber das will nicht in meinem Kopf.

In jeder Lektüre liest man eigentlich, dass es nicht die Aufgabe des Controllers ist, Daten zu manipulieren. Während ich mein Preview zeichne, entstehen aber auch schon Daten. Denn wenn ich mein Preview abschließe, so habe ich ja eine Handvoll neuer Koordinaten für die geometrischen Formen als neues Wissen gewonnen. Und die werden doch dann ganz klar vom Controller erzeugt (falls ich es so mache wie von euch zwei beschrieben) oder nicht? Aber eigentlich laut Theorie sollte das ja nicht der Fall sein.

Desweiteren wird man oft darauf hingewiesen, dass der Controller zwar sehr wohl Logik enthält, aber eben nur zur Steuerung (Mapping der View/Gui-Eingaben aufs Model, reagieren auf Veränderungen des Models durch Anpassen des Views). Aber meine Zeichenstiftlogik (die normalerweise nur das Model manipuliert), die man bei eurem Vorschlag ja im Controller einsetzen müsste, ist doch auch etwas anderes als Steuerlogik.

Versteht mich nicht falsch: ich bin weder ein Sturkopf, noch will ich hier schlau sein. Habe nur Probleme, Theorie mit euren Ausführungen in Einklang zu bringen und bin gerade deshalb sehr dankbar für eure Antworten.

edit:

MVC heißt ja: das Model kommuniziert über die Controller Schicht mit der View (und evtl. nach unten dann mit der Datenschicht). Der Sinn der Sache ist, dass bei einer Änderung der GUI allenfalls die Event Handler neu verbunden werden müssen, die eigentliche Programmlogik davon aber nicht berührt wird.

Genau das ist der Punkt. Aber wenn mein Model nicht mehr als einzige Instanz für die Zeichenlogik zuständig ist, sondern nun auch der Controller für die Preview, dann ist die "Modularität" meines Models doch etwas angeknackst und ich kann es an anderer Stelle nicht wiederverwenden, ohne auch Teile des Controllers (nämlich die Previewfunktion) mitzunehmen.

edit2: spricht aus MVC-Sicht denn auch nur irgendetwas gegen meine bisherige Vorgehensweise?

Monger
2011-06-02, 21:29:52
In jeder Lektüre liest man eigentlich, dass es nicht die Aufgabe des Controllers ist, Daten zu manipulieren. Während ich mein Preview zeichne, entstehen aber auch schon Daten.

Klar entstehen Daten. Jeder Mausklick, jede Tasteneingabe ist eine Form von Information. Aber diese Daten haben keine dauerhafte Auswirkung auf deinen inneren Datenzustand.

Nehmen wir mal folgendes Beispiel: du hast eine Oberfläche mit einer Textbox. Mit der Textbox kann man einer Liste E-Mail Adressen hinzufügen.

Die Textbox ist also die View. Die Validierung (z.B. bei FocusLost) ob wirklich eine E-Mail Adresse da drin steht, ist Bestandteil des Controllers. Die Logik bzw. Datenhaltung ist eine Liste von Strings.

An dem Beispiel siehst du auch, dass der Controller alles andere als passiv ist. Gerade Validierung (prüfen auf E-Mail Schema, evtl. Darstellung normen, invalidieren, rot färben der Textbox etc.) ist ein Prozess wo viel Logik im Controller stecken kann. Die Model Logik an der Stelle ist relativ trivial: füge String x zu Liste hinzu. Das ist auch genau die Aufgabe des Controllers: die Angriffsfläche vom Model reduzieren, weil du dich eben unmöglich auf jeden beliebigen Fehlerfall in der View im Model vorbereiten kannst.

Im Fall deiner Preview: natürlich entstehen Daten solange du die Maus umherziehst. Aber sie schlagen erst bis zum Model durch, wenn du die Maus loslässt, und somit das Preview verlässt. (Das wäre das Äquivalent zum Validieren)


Aber meine Zeichenstiftlogik (die normalerweise nur das Model manipuliert), die man bei eurem Vorschlag ja im Controller einsetzen müsste, ist doch auch etwas anderes als Steuerlogik.

Wir haben es ja schon weiter oben geschrieben: du hast dir ja de facto hier dein eigenes GUI Control geschrieben. Das geht über eine klassische Fensterapplikation hinaus, und ist somit auch kein MVC mehr.


edit2: spricht aus MVC-Sicht denn auch nur irgendetwas gegen meine bisherige Vorgehensweise?
Wie jedes Pattern ist auch das hier keine exakte Wissenschaft! ;)

Denk einfach dran: das große Ziel hinter MVC ist Robustheit, durch die Reduzierung von Abhängigkeiten.