Step 02 - OOP und was nun noch gesagt werden muss (Teil 2/3)

 

Ein Multiformular-Spiel - Einführung in die Arbeit mit mehreren Formular-Klassen

Wir Delphianer programmieren sehr gerne dynamisch. Daher könnte es sein, dass das Wort static erstmal total unbekannt ist, sofern es nicht von einer anderen Programmiersprache wie z.B. C++ oder Java bekannt ist. Um es genau erklären zu können müssen wir uns zunächst mal anschauen was passiert wenn wir ein Objekt - eine Instanz einer Klasse - erstellen.
Angefangen beim compilieren:
Unser Programm weiß noch nicht genau, wann dieses Objekt erstellt bzw. der Speicher hierfür reserviert werden soll. Beim Klicken eines Buttons soll z.B. eine Meldung - ein eigener Dialog - gezeigt werden. Rein theoretisch wäre es doch nun sinnvoll, wenn genau dann beim Klicken dieser Speicher reserviert wird. Dies wäre der dynamische Weg - d.h. ich bin selbst verantwortlich, dass es erstellt (Create) und wieder aus dem Speicher gelöscht (Destroy/Free) wird.
Andererseits - wie ist es wenn es sich bei diesem Dialog um ein eigenes Fenster aus einer eigenen Klasse handelt? Es sollen Benutzereingaben erfasst werden, welche dann später - vom Hauptformular aus - ausgewertet werden sollen. (Okay, hierfür würden wir eher ein virtuelles Objekt als "Datenbank" benutzen) Worauf ich hierbei hinaus wollte: bereits beim Laden des Programmes wird - bei Standardeinstellungen - dieses "Dialog"formular erstellt und ist von da an statisch im Speicher.
 

Mehrere Formulare - und wie Zugriffsfehler entstehen können

An folgendem einfachen Beispiel möchte ich hier auf einige wichtige Dinge mit dem Umgang von "mehreren Fenstern" hinweisen.
  • Hierfür erstellen wir zunächst ein neues Projekt, welches wir "LittleWindowGame" nennen. ("MainUnit" nicht vergessen!)
  • Nachdem wir das Formular "MainForm" benannt haben, fügen wir nun unter "Datei" -> "Neu" -> "Formular" unserem Projekt ein neues Formular hinzu, welches wir "RedForm" nennen. Die pas-Datei benennen wir mit "frmRedForm"
  • Nach dem gleichen Prinzip gehen wir für ein "BlueForm" sowie für ein "GreenForm" vor.
  • Die Namen der Formulare stellen gleichzeitig auch die Farbe des Hintergrundes dar.
  • Auf Red Green und Blue setzen wir jeweils ein TShape-Objekt und nennen es jeweils "shpLight". (ja, jedes hat den gleichen Namen!) Hiernach noch bei Shape "stCircle" einstellen und bei Width den Wert 50 eintragen.

Nachdem wir nun sicherheitshalber auf "Alles Speichern" geklickt haben, wenden wir uns dem Hauptformular zu:

  • Damit BlueForm und die Anderen gefunden werden, müssen wir noch die anderen Units bei den Uses hinzufügen. Alternative ist, dass wir nach dem Schreiben der Objekte via Rechtsklick direkt durch Refactoring -> Unit suchen... die Unit automatisch einbinden lassen.
implementation

uses
  frmRedForm, frmGreenForm, frmBlueForm;
  • Wir erstellen per Doppelklick auf das Ereignis "OnCreate" die MainFormCreate-Prozedur und tragen dort folgendes ein:
  with BlueForm do
  begin
    BorderStyle := bsNone;
    Color       := clBlue;
    Width       := 250;
    Height      := 250;
    Top         := 50;
    Left        := 50;

    shpLight.left        :=100;
    shpLight.top         :=100;
    shpLight.Brush.Color := clBlue;

    Show;
  end;

  // Hier die anderen Formulare ... ;-)

  Left := 250;
  top  := 250;
  • Das aller gleiche machen wir sowohl mit GreenForm als auch mit RedForm und ändern jeweils die Farben (clGreen bzw. clRed statt clBlue) sowie die Links-Abstände wie folgt:
    • Bei RedForm: 50;
    • Bei GreenForm: 350;
    • Bei BlueForm: 650;

Wenn wir das alles nun ordnungsgemäß und richtig gemacht haben, dann werden wir auf einen Fehler stoßen wie:
"Im Projekt LittleWindowGame.exe ist eine Exception der Klasse EAccessViolation mit der Meldung 'Zugriffsverletzung bei Adresse 0044A2C3 in Modul 'LittleWindowGame.exe'. Lesen von Adresse 00000271' aufgetreten."
Dieser Fehler taucht auf, da wir uns folgendem Gedankengang entzogen haben:
Wenn Speicher für Objekte reserviert und diese sozusagen erstellt werden, dann passiert das in einer bestimmten Reihenfolge. Im Projektquelltext können wir diese sehen und ggf. bearbeiten. (Projekt -> Quelltext anzeigen) Allerdings bearbeiten wir nichts, da sonst wieder ein kleiner ungewollter Nebeneffekt auftaucht, sondern wir verschieben alles in die Methode des OnShow-Ereignisses, welche wir im Objektinspektor noch erzeugen müssen.
Wieso nicht gleich richtig hier im Tutorial? Nun ja, in meinem Grundkurs gebe ich gerne auch die Idee hinter den vorkommenden Fehlern weiter. Wenn wir erstmal solche "unbekannten" Fehler kennen lernen, können wir gezielter auf sie reagieren, und uns ruhiger um deren Behebung kümmern, als beim plötzlichen Auftreten wie von der Tarantel gestochen "stehen zu bleiben". Hierbei geht es um einen Fehler, den ich gerne auch einfach als "Leichtsinnsfehler" bezeichnen mag, da er schnell gemacht werden kann, ohne dass die Meldung daraus sehr aussagekräftig ist, woran es denn liegen mag.

  • Zurück zum Hauptformular -> Objektinspektor zu Ereignissen wechseln und das Doppelklick auf das OnShow-Ereignis. Anschließend den gesamten Quellcode von OnCreate in den des OnShow-Ereignisses rein.
  • Nun können wir das Programm ohne Probleme ausführen.

Jetzt kommt die eigentliche Spielelogik:
Es sollen alle Formulare nebeneinander angezeigt werden. (was schon passiert) Dann soll ein Timer (den wir im nächsten Schritt implementieren) alle n Millisekunden die Farben der Shapes ändern.
Hiernach muss der benutzer auf das Formular klicken, welches diese angezeigte Farbe hat.
Hat er es geschafft bevor quasi die Zeit um ist, dann kommt er weiter und der Timer wird um x millisekunden verkleinert.
Hat er es nicht geschafft, ist das Spiel vorbei und n(anfang) - n(momentan) ist die Anzahl der Punkte.
Also, weiter rann ans Werk:

  • Wir fügen einen Timer (Name:=tmrGameTimer) beim Hauptformular ein, stellen das Intervall auf 4 Sekunden (4000 ms) und deaktivieren ihn gleich.
  • Dann benötigen wir noch zwei Buttons:
    • Der erste soll das Spiel starten (BtnStartGame),
    • der zweite soll das Programm schließen (BtnQuit).
  • BtnStartGame soll die erste Farbe setzen sowie den Timer starten:
procedure TMainForm.BtnStartGameClick(Sender: TObject);
var
  col: TColor;
begin
  // Zufallsfarbe wählen
  case random(3) of  
    0: col := clRed;
    1: col := clGreen;
    2: col := clBlue;
  end;  
  
  // Farbe  setzen:
  RedForm.shpLight.Brush.Color := col;
  GreenForm.shpLight.Brush.Color := col;
  BlueForm.shpLight.Brush.Color := col;
  
  // Timer einschalten
  tmrGameTimer.Enabled := true;
end;

Und nun zum Timer:
Es soll erst abgefragt werden, ob die aktuelle Farbe gleich der des geklickten Formulars ist (die wir noch irgendwo zwischenspeichern müssen!), und dann soll entweder eine neue Farbe sowie das neue Intervall n gesetzt oder das Spiel beendet und die Punkte ausgegeben werden.

  • Wir deklarieren also zunächst ein privates Feld in der TMainForm-Klasse und einen Setter:
  private
    fCurrentClickedColor : TColor;
  public
    procedure SetCurrentClickedColor( AColor: TColor);
  • Hierzu implementieren wir die Methode:
procedure TMainForm.SetCurrentClickedColor( AColor: TColor);
begin
  fCurrentClickedColor := AColor;
end;
  • Nun müssen wir dafür sorgen, dass dieser Wert auch gesetzt wird: Wir implementieren also in jedem Formular (ausgenommen dem Hauptformular) die OnClick-Methode, in der wir per MainForm.SetCurrentClickColor( Color) den Farbwert des eigenen Formulars zuweisen. (Achtung: Verbindung zu Mainform muss auch implementiert werden!) (Und noch ein Hinweis: TShape hat kein OnClick-Ereignis, hier können wir ja entweder MouseUp oder MouseDown verwenden.
  • Nun können wir den oben genannten Gedankengang einfach umsetzen. Da ihr auch etwas denken dürft, hier nur ein unvollständiger Teil des Pseudocodes:
  wenn MomentanGeclickteFarbe = BlauesFormular.Shape.Pinsel.Farbe dann
  starte
    SetzeNeueZufallsfarbe;
    VerringereIntervallDesTimersUm100;
  ende
  ansonsten
  starte
    DeaktiviereTimer;
    Anwendung.Meldungsbox(PChar('Du hast ' +
                                IntToStr(4000 -
                                         TimerIntervall) +
                                ' Punkte!') ,
                          'Game Over' ,
                          MB_OK+MB_ICONINFORMATION);
  ende;
  • Und schon ist das Spiel fertig 🤓
 
Die Sources hierfür sind alle unter Github zum Download bereit! Viel Spaß :)
 
Wer nun bei OOP noch tiefer in die Materie eingreifen möchte, kann sich in der Delphi-Hilfe folgende Themen mal anschaun. (Falls die Links nicht mehr gehen sollten: Einfach in die Delphi-Suche in Gänsefüßchen eintippen; das Thema heißt dann genau so, wie hier geschrieben)
Auch noch sehr interessant, aber schon sehr eng beim Experten-Niveau:
Ich hoffe ich konnte euch mit diesem Zweiteiler einen noch besseren Überblick zu den wichtigsten Prinzipien in der OOP-Welt liefern.