Exkurs - GUI-Objekte und Datenmodellobjekte

Problem der Datenhaltung

Betrachte noch einmal das Spiel chuck a luck.

Anwendungsfenster

Hier der Quelltext zur Erzeugung der gezeigten Benutzeroberfläche:

from tkinter import *
from random import randint
# Ereignisverarbeitung

def Button_Einsatz_Click():
    # Verwaltung der Daten
    konto = int(labelKonto.cget('text'))
    # Verarbeitung der Daten
    konto = konto - 1
    # Anzeige der Daten
    labelKonto.config(text=str(konto))

def Button_Wuerfel_Click():
    # Verarbeitung der Daten    
    wuerfelA = randint(1,6)
    wuerfelB = randint(1,6)
    wuerfelC = randint(1,6)
    # Verwaltung und Anzeige der Daten
    labelWuerfelA.config(text=str(wuerfelA))
    labelWuerfelB.config(text=str(wuerfelB))
    labelWuerfelC.config(text=str(wuerfelC))
    labelWuerfelA.config(image=imageListe[wuerfelA-1])
    labelWuerfelB.config(image=imageListe[wuerfelB-1])
    labelWuerfelC.config(image=imageListe[wuerfelC-1])
    
def Button_Gewinn_Click():
    # Verwaltung der Daten
    wuerfelA = int(labelWuerfelA.cget('text'))
    wuerfelB = int(labelWuerfelB.cget('text'))
    wuerfelC = int(labelWuerfelC.cget('text'))
    konto = int(labelKonto.cget('text'))
    zahl = spielzahl.get()
    # Verarbeitung der Daten
    gewinn = 0
    if zahl == wuerfelA:
        gewinn = gewinn + 1
    if zahl == wuerfelB:
        gewinn = gewinn + 1
    if zahl == wuerfelC:
        gewinn = gewinn + 1
    if gewinn > 0:
        konto = konto + (gewinn + 1)
    # Anzeige der Daten
    labelKonto.config(text=str(konto))
    
# Erzeugung der Komponenten
# Erzeugung des Fensters
tkFenster = Tk()
tkFenster.title("chuck a luck")
tkFenster.geometry("350x145")
# Rahmen Überschrift
frameUeberschrift = Frame(master=tkFenster,
                          background="#889E9D")
frameUeberschrift.place(x=5, y=5, width=340, height=30)
# Label für die Überschrift
labelUeberschrift = Label(master=frameUeberschrift,
                          text="chuck a luck",
                          foreground="white",
                          font="Arial 16",
                          background="#889E9D")
labelUeberschrift.place(x=5, y=5, width=330, height=20)
# Rahmen Konto
frameKonto = Frame(master=tkFenster,
                   background="#FFCFC9")
frameKonto.place(x=5, y=40, width=110, height=100)
# Label mit Überschrift für das Konto
labelUeberschriftKonto = Label(master=frameKonto,
                               text="Konto",
                               background="white")
labelUeberschriftKonto.place(x=5, y=5, width=100, height=20)
# Label Konto
labelKonto = Label(master=frameKonto,
                   text="100",
                   background="white")
labelKonto.place(x=40, y=35, width=30, height=30)
# Button zum Einzahlen
buttonEinsatz = Button(master=frameKonto,
                       text="Einsatz zahlen",
                       command=Button_Einsatz_Click)
buttonEinsatz.place(x=5, y=75, width=100, height=20)
# Rahmen Zahl
frameZahl = Frame(master=tkFenster,
                  background="#D5E88F")
frameZahl.place(x=120, y=40, width=110, height=100)
# Label mit Überschrift für die Zahl
labelUeberschriftZahl = Label(master=frameZahl,
                              text="Zahl",
                              background="white")
labelUeberschriftZahl.place(x=5, y=5, width=100, height=20)
# Radiobutton für die Zahl
spielzahl = IntVar()
radiobutton1 = Radiobutton(master=frameZahl,
                           text='1',
                           value=1,
                           variable=spielzahl)
radiobutton1.place(x=5, y=30, width=30, height=18)
radiobutton2 = Radiobutton(master=frameZahl,
                           text='2',
                           value=2,
                           variable=spielzahl)
radiobutton2.place(x=40, y=30, width=30, height=18)
radiobutton3 = Radiobutton(master=frameZahl,
                           text='3',
                           value=3,
                           variable=spielzahl)
radiobutton3.place(x=75, y=30, width=30, height=18)
radiobutton4 = Radiobutton(master=frameZahl,
                           text='4',
                           value=4,
                           variable=spielzahl)
radiobutton4.place(x=5, y=52, width=30, height=18)
radiobutton5 = Radiobutton(master=frameZahl,
                           text='5',
                           value=5,
                           variable=spielzahl)
radiobutton5.place(x=40, y=52, width=30, height=18)
radiobutton6 = Radiobutton(master=frameZahl,
                           text='6',
                           value=6,
                           variable=spielzahl)
radiobutton6.place(x=75, y=52, width=30, height=18)

# Button zum Auswerten
buttonGewinn = Button(master=frameZahl,
                      text="Gewinn auszahlen",
                      command=Button_Gewinn_Click)
buttonGewinn.place(x=5, y=75, width=100, height=20)
# Rahmen Würfel
frameWuerfel = Frame(master=tkFenster,
                     background="#FBD975")
frameWuerfel.place(x=235, y=40, width=110, height=100)
# Label mit Überschrift für die Würfel
labelUeberschriftWuerfel = Label(master=frameWuerfel,
                                 text="Würfel",
                                 background="white")
labelUeberschriftWuerfel.place(x=5, y=5, width=100, height=20)
# Bilder
imageListe = [
    PhotoImage(file="w1.gif"),
    PhotoImage(file="w2.gif"),
    PhotoImage(file="w3.gif"),
    PhotoImage(file="w4.gif"),
    PhotoImage(file="w5.gif"),
    PhotoImage(file="w6.gif")
    ]
# Label Würfel A
labelWuerfelA = Label(master=frameWuerfel, image=imageListe[0])
labelWuerfelA.place(x=5, y=35, width=30, height=30)
# Label Würfel B
labelWuerfelB = Label(master=frameWuerfel, image=imageListe[0])
labelWuerfelB.place(x=40, y=35, width=30, height=30)
# Label Würfel C
labelWuerfelC = Label(master=frameWuerfel, image=imageListe[0])
labelWuerfelC.place(x=75, y=35, width=30, height=30)
# Button zum Würfeln
buttonWuerfel = Button(master=frameWuerfel,
                        text="Wuerfel werfen",
                        command=Button_Wuerfel_Click)
buttonWuerfel.place(x=5, y=75, width=100, height=20)

# Aktivierung des Fensters
tkFenster.mainloop()

Zur Durchführung des Spiels müssen folgende Daten verwaltet werden:

Im Programm oben sind hierfür bestimmte Objekte zuständig:

GUI-Objekte sind eigentlich nicht für die Datenhaltung einer Anwendung gedacht. Ihre Aufgabe ist es vielmehr, die Daten von GUI-Komponenten zu verwalten. Deshalb ist die oben gezeigte Lösung, mit Hilfe von Label-Objekten Spieldaten indirekt mitzuverwalten, keine gute Lösung.

Dies fällt besonders bei der Verwaltung der Würfelergebnisse auf. In der Ereignisverarbeitungsprozedur Button_Wuerfel_Click wird die Anweisung labelWuerfelA.config(text=str(wuerfelA)) benötigt, um das zuvor ermittelte Würfelergebnis zwischenzuspeichern, damit man bei der Ermittlung des Gewinns hierauf zurückgreifen kann. Für die Ausgabe dieses Würfelergebnisses würde die Anweisung labelWuerfelA.config(image=wuerfelbilder[wuerfelA-1]) ausreichen.

Einzig die Verwaltung der Spielzahl durch ein eigenständiges IntVar-Objekt entspricht der Forderung, dass Objekte klar begrenzte Zuständigkeiten haben sollen und dass Aufgaben von hierfür zuständigen Objekten übernommen werden sollten. Dieser Lösungsansatz ist aber nicht auf die anderen Daten übertragbar, so dass man zur Lösung des Datenhaltungsproblems einen anderen Weg gehen muss.

Verwaltung der Spieldaten mit globalen Variablen

Zur Verwaltung der Spieldaten könnte man geeignete Variablen einführen. Diese Variablen müssten dann global definiert sein, um in den betreffenden Ereignisverarbeitungsprozeduren zugänglich zu sein.

from tkinter import *
from random import randint

# Datenmodell
konto = 100
wuerfelA = 1
wuerfelB = 1
wuerfelC = 1
zahl = 0

# Ereignisverarbeitung

def Button_Einsatz_Click():
    global konto
    # Verarbeitung der Daten
    konto = konto - 1
    # Anzeige der Daten
    labelKonto.config(text=str(konto))

def Button_Wuerfel_Click():
    global wuerfelA, wuerfelB, wuerfelC
    # Verarbeitung der Daten    
    wuerfelA = randint(1,6)
    wuerfelB = randint(1,6)
    wuerfelC = randint(1,6)
    # Verwaltung und Anzeige der Daten
    labelWuerfelA.config(image=imageListe[wuerfelA-1])
    labelWuerfelB.config(image=imageListe[wuerfelB-1])
    labelWuerfelC.config(image=imageListe[wuerfelC-1])
    
def Button_Gewinn_Click():
    global wuerfelA, wuerfelB, wuerfelC, konto, zahl
    # Verwaltung der Daten
    zahl = spielzahl.get()
    # Verarbeitung der Daten
    gewinn = 0
    if zahl == wuerfelA:
        gewinn = gewinn + 1
    if zahl == wuerfelB:
        gewinn = gewinn + 1
    if zahl == wuerfelC:
        gewinn = gewinn + 1
    if gewinn > 0:
        konto = konto + (gewinn + 1)
    # Anzeige der Daten
    labelKonto.config(text=str(konto))
    
# Erzeugung der Komponenten
# Erzeugung des Fensters
tkFenster = Tk()
tkFenster.title("chuck a luck")
tkFenster.geometry("350x145")
# Rahmen Überschrift
frameUeberschrift = Frame(master=tkFenster,
                          background="#889E9D")
frameUeberschrift.place(x=5, y=5, width=340, height=30)
# Label für die Überschrift
labelUeberschrift = Label(master=frameUeberschrift,
                          text="chuck a luck",
                          foreground="white",
                          font="Arial 16",
                          background="#889E9D")
labelUeberschrift.place(x=5, y=5, width=330, height=20)
# Rahmen Konto
frameKonto = Frame(master=tkFenster,
                   background="#FFCFC9")
frameKonto.place(x=5, y=40, width=110, height=100)
# Label mit Überschrift für das Konto
labelUeberschriftKonto = Label(master=frameKonto,
                               text="Konto",
                               background="white")
labelUeberschriftKonto.place(x=5, y=5, width=100, height=20)
# Label Konto
labelKonto = Label(master=frameKonto,
                   text=str(konto),
                   background="white")
labelKonto.place(x=40, y=35, width=30, height=30)
# Button zum Einzahlen
buttonEinsatz = Button(master=frameKonto,
                       text="Einsatz zahlen",
                       command=Button_Einsatz_Click)
buttonEinsatz.place(x=5, y=75, width=100, height=20)
# Rahmen Zahl
frameZahl = Frame(master=tkFenster,
                  background="#D5E88F")
frameZahl.place(x=120, y=40, width=110, height=100)
# Label mit Überschrift für die Zahl
labelUeberschriftZahl = Label(master=frameZahl,
                              text="Zahl",
                              background="white")
labelUeberschriftZahl.place(x=5, y=5, width=100, height=20)
# Radiobutton für die Zahl
spielzahl = IntVar()
radiobutton1 = Radiobutton(master=frameZahl,
                           text='1',
                           value=1,
                           variable=spielzahl)
radiobutton1.place(x=5, y=30, width=30, height=18)
radiobutton2 = Radiobutton(master=frameZahl,
                           text='2',
                           value=2,
                           variable=spielzahl)
radiobutton2.place(x=40, y=30, width=30, height=18)
radiobutton3 = Radiobutton(master=frameZahl,
                           text='3',
                           value=3,
                           variable=spielzahl)
radiobutton3.place(x=75, y=30, width=30, height=18)
radiobutton4 = Radiobutton(master=frameZahl,
                           text='4',
                           value=4,
                           variable=spielzahl)
radiobutton4.place(x=5, y=52, width=30, height=18)
radiobutton5 = Radiobutton(master=frameZahl,
                           text='5',
                           value=5,
                           variable=spielzahl)
radiobutton5.place(x=40, y=52, width=30, height=18)
radiobutton6 = Radiobutton(master=frameZahl,
                           text='6',
                           value=6,
                           variable=spielzahl)
radiobutton6.place(x=75, y=52, width=30, height=18)

# Button zum Auswerten
buttonGewinn = Button(master=frameZahl,
                      text="Gewinn auszahlen",
                      command=Button_Gewinn_Click)
buttonGewinn.place(x=5, y=75, width=100, height=20)
# Rahmen Würfel
frameWuerfel = Frame(master=tkFenster,
                     background="#FBD975")
frameWuerfel.place(x=235, y=40, width=110, height=100)
# Label mit Überschrift für die Würfel
labelUeberschriftWuerfel = Label(master=frameWuerfel,
                                 text="Würfel",
                                 background="white")
labelUeberschriftWuerfel.place(x=5, y=5, width=100, height=20)
# Bilder
imageListe = [
    PhotoImage(file="w1.gif"),
    PhotoImage(file="w2.gif"),
    PhotoImage(file="w3.gif"),
    PhotoImage(file="w4.gif"),
    PhotoImage(file="w5.gif"),
    PhotoImage(file="w6.gif")
    ]
# Label Würfel A
labelWuerfelA = Label(master=frameWuerfel, image=imageListe[wuerfelA-1])
labelWuerfelA.place(x=5, y=35, width=30, height=30)
# Label Würfel B
labelWuerfelB = Label(master=frameWuerfel, image=imageListe[wuerfelB-1])
labelWuerfelB.place(x=40, y=35, width=30, height=30)
# Label Würfel C
labelWuerfelC = Label(master=frameWuerfel, image=imageListe[wuerfelC-1])
labelWuerfelC.place(x=75, y=35, width=30, height=30)
# Button zum Würfeln
buttonWuerfel = Button(master=frameWuerfel,
                        text="Wuerfel werfen",
                        command=Button_Wuerfel_Click)
buttonWuerfel.place(x=5, y=75, width=100, height=20)

# Aktivierung des Fensters
tkFenster.mainloop()

Eine solche Implementierung mit globalen Variablen ist durchaus möglich (siehe Quelltext). Die Verwendung globaler Variablen wird jedoch in der Regel vermieden, da sie Programme schwer durchschaubar macht. Statt globaler Variablen verwendet man besser ein eigenständiges Objekt zur Verwaltung der Spieldaten.

Ein Objekt zur Verwaltung der Spieldaten

In diesem Abschnitt wird gezeigt, wie man ein eigenständiges Objekt zur Verwaltung der Spieldaten erzeugt. Mehr über diesen objektorientierten Ansatz erfährst du im Kapitel Objektorientierte Programmierung mit Python.

Zunächst muss man einen Bauplan (eine Klasse) für das Objekt, das die Daten eines chuck-a-luck-Spiels verwalten soll, entwickeln. Der folgende Quelltext - den man in einer eigenen Datei (z.B. chuckaluck_datenmodell.py) abspeichert - zeigt, wie das geht.

from random import randint

class ChuckALuck(object):
    def __init__(self):
        self.konto = 100
        self.spielzahl = 1
        self.wuerfelA = 1
        self.wuerfelB = 1
        self.wuerfelC = 1

    def einsatzZahlen(self):
        self.konto = self.konto - 1

    def spielzahlSetzen(self, zahl):
        self.spielzahl = zahl

    def wuerfelWerfen(self):
        self.wuerfelA = randint(1,6)
        self.wuerfelB = randint(1,6)
        self.wuerfelC = randint(1,6)

    def gewinnAuszahlen(self):
        gewinn = 0
        if self.spielzahl == self.wuerfelA:
            gewinn = gewinn + 1
        if self.spielzahl == self.wuerfelB:
            gewinn = gewinn + 1
        if self.spielzahl == self.wuerfelC:
            gewinn = gewinn + 1
        if gewinn > 0:
            self.konto = self.konto + (gewinn + 1)

Auch wenn einige Schreibweisen zunächst etwas merkwürdig sind, so kann man doch die Bedeutung der Bestandteile dieser Klassendeklaration nachvollziehen.

Die Operation __init__ führt geeignete Attribute zur Verwaltung der Spieldaten ein und versieht sie mit Anfangswerten.

Die Operation einsatzZahlen verringert den Wert des Konto-Attributs um 1.

Die Operation spielzahlSetzen setzt den Wert der Spielzahl auf den übergebenen Parameterwert.

Die Operation wuerfelWerfen erzeugt mit Hilfe des Zufallsgenerator neue Werte für die Attribute zur Verwaltung der Würfel.

Die Operation gewinnAuszahlen schließlich bestimmt den Gewinn bezüglich der aktuellen Spieldaten und verbucht ihn beim Konto-Attribut.

Wenn man diese Klassendeklaration ausführt, dann lässt sich folgender Python-Dialog zur Simulation eines chuck-a-luck-Spiels führen:

>>> spiel = ChuckALuck()
>>> spiel.konto
100
>>> spiel.einsatzZahlen()
>>> spiel.konto
99
>>> spiel.spielzahlSetzen(6)
>>> spiel.spielzahl
6
>>> spiel.wuerfelWerfen()
>>> spiel.wuerfelA
3
>>> spiel.wuerfelB
6
>>> spiel.wuerfelC
6
>>> spiel.gewinnAuszahlen()
>>> spiel.konto
102

Beachte, dass hier zunächst ein Objekt spiel der Klasse ChuckALuck erzeugt wird. Die einzelnen Spielaktionen führt man durch, indem man entsprechende Operationen des spiel-Objekts aufruft.

Beachte auch, dass die Klasse ChuckALuck so konzipiert ist, dass sie ohne grafische Benutzeroberfläche benutzt werden kann. Sie liefert also ein reines Datenmodell.

Aufgabe 1

Führe selbst einen solchen Python-Dialog zum chuck-a-luck-Spiel durch.

Das Datenmodell als eigenständiger Baustein

Die Klasse ChuckALuck bildet das sogenannte Datenmodell zum chuck-a-luck-Spiel und stellt einen abgeschlossenen und eigenständigen Programm-Baustein dar. Diesen Baustein kann man in anderen Programmen nutzen. Das folgende Beispielprogramm zeigt, wie man dabei vorgeht.

# Datenmodell
from chuckaluck_datenmodell import ChuckALuck
spiel = ChuckALuck()
# Programm zum Test des Spiels
spiel.einsatzZahlen()
spiel.spielzahlSetzen(6)
spiel.wuerfelWerfen()
spiel.gewinnAuszahlen()
# Ausgabe der Spieldaten
print("Spielzahl:", spiel.spielzahl)
print("Würfel A:", spiel.wuerfelA)
print("Würfel B:", spiel.wuerfelB)
print("Würfel C:", spiel.wuerfelC)
print("Konto:", spiel.konto)

Mit der Anweisung from chuckaluck_datenmodell import ChuckALuck wird der Baustein ChuckALuck im aktuellen Programm verfügbar gemacht. Der Bezeichner chuckaluck_datenmodell ist der Name der Datei (ohne die Endung .py), in der die Klassendeklaration ChuckALuck abgespeichert ist.

Die Datei chuckaluck_datenmodell.py muss vom Python-System gefunden werden. Das erreicht man am einfachsten, indem man sie im selben Ordner ablegt, in dem auch das aktuelle Testprogramm gespeichert wird.

Aufgabe 2

Ergänze das Testprogramm so, dass das chuck-a-luck-Spiel wiederholt mit einer fest vorgegebenen Spielzahl durchgeführt wird. Interessant ist dabei die Klärung der Frage, ob das Spiel fair ist.

Datenmodell-Objekt und GUI-Objekte

Mit Hilfe der Klasse ChuckALuck lässt sich also ein Datenmodell-Objekt spiel erzeugen, das für die Datenhaltung und Datenverarbeitung zuständig ist. Der folgende Quelltext-Auszug zeigt, wie dieses Objekt gemeinsam mit den GUI-Objekten die Aufgaben der Benutzeroberfläche erledigt.

# Erzeugung des Datenmodell-Objekts
from chuckaluck_datenmodell import ChuckALuck
spiel = ChuckALuck()

# Ereignisverarbeitung

def Button_Einsatz_Click():
    # Verarbeitung der Daten
    spiel.einsatzZahlen()
    # Anzeige der Daten
    labelKonto.config(text=str(spiel.konto))

def Button_Wuerfel_Click():
    # Verarbeitung der Daten    
    spiel.wuerfelWerfen()
    # Anzeige der Daten
    labelWuerfelA.config(image=wuerfelbilder[spiel.wuerfelA-1])
    labelWuerfelB.config(image=wuerfelbilder[spiel.wuerfelB-1])
    labelWuerfelC.config(image=wuerfelbilder[spiel.wuerfelC-1])
    
def Button_Gewinn_Click():
    # Verarbeitung der Daten
    spiel.gewinnAuszahlen()
    # Anzeige der Daten
    labelKonto.config(text=str(spiel.konto))

def Radiobutton_Click():
    # Verwaltung der Daten
    zahl = spielzahl.get()
    spiel.spielzahlSetzen(spielzahl.get())
    
# Erzeugung der GUI-Objekte
from tkinter import *
# Erzeugung des Fensters
...

Beachte, dass der Programmabschnitt zur Erzeugung der GUI-Objekte nicht verändert werden muss. Das ChuckALuck-Objekt spiel tritt nur bei der Ereignisverarbeitung in Aktion.

Aufgabe 3

Integriere das ChuckALuck-Objekt spiel in dein eigenes chuck-a-luck-Programm.

Trennung zwischen Datenmodell und GUI

Die folgende Grafik verdeutlicht das Zusammenspiel der beiden Programmteile Deklaration der Datenmodell-Klasse und Erzeugung der GUI.

Verdeutlichung

Zur Erzeugung des Datenmodell-Objekts spiel wird die Klasse CuckALuck (als Bauplan) benutzt. Die Anweisung from chuckaluck_datenmodell import ChuckALuck ermöglicht die Nutzung dieses Datenmodell-Bausteins.

Bei der Entwicklung der Klasse ChuckALuck ist ein wichtiges Entwurfsprinzip benutzt worden, das es ermöglicht, Klassen flexibel zu nutzen.

Datenmodell-Klassen sollten immer so konzipiert werden, dass sie auch ohne eine GUI benutzt werden können. Hierdurch wird ermöglicht, dass diese Klassen in verschiedenen Programmen als Baustein benutzt werden kann. Dieses Prinzip der Trennung zwischen Datenmodell und GUI werden wir ständig anwenden und im Abschnitt Fachkonzept - Trennung zwischen Datenmodell und GUI näher erläutern.

X

Fehler melden

X

Suche