HSG |
|
Nach dem Vorbild einer früheren Delphi-Realisierung sollen Methoden zum Zeichnen von Ampeln entwickelt werden.
Die Programme ampel0.py und ampel2.py zeigen Lösungsmöglichkeiten auf. Beide Programme haben eine strikte MVC-Struktur. Während ampel0.py eine eigene Klasse 'Ampelcanvas' einführt, die die TKinter-Canvas spezialisiert, verwendet ampel2.py Klassenmethoden, die eine Referenz der Canvas benötigen, worauf sie zeichnen sollen.
Die Zustände werden durch Strings beschrieben, die in einem unverändlichen (immutable) Tupel zusammengefasst sind. Durch das Tupel erhalten die Zustände Nummern. Die Nummer des aktuellen Zustands steht in dem Attribut zustand.
# model class Model(object): def __init__(self): self.zustaende = ('rot','gelbrot','grün','gelb') self.zustand = 0 def weiter(self): self.zustand = (self.zustand+1)%4
Der Controller 'hängt' die Methode weiter als Callback bei der Erzeugung von view ein, dh. ein Druck auf den Button löst einen Aufruf von weiter aus. weiter gibt die Aufforderung an model weiter und erfragt den aktuellen Zustand. Je nach Zustand wird dann die Ampel in view neu gezeichnet. Der Moment für ein Neuzeichnen ist richtig gewählt, da model gerade den Zustand gewechselt hat.
# controller class Controller(object): def __init__(self): self.model = Model() self.view = View(self.weiter) # Ereignisbehandlung einhängen Ampel.zeichne(self.view.c,75,20,20,True,False,False) self.view.mainloop() def weiter(self): self.model.weiter() # weiterleiten an model zustand = self.model.zustand # Zustand von model erfragen # je nach Zustand Ampel in view neu zeichnen if zustand == 0: Ampel.zeichne(self.view.c,75,20,20,True,False,False) elif zustand == 1: Ampel.zeichne(self.view.c,75,20,20,True,True,False) elif zustand == 2: Ampel.zeichne(self.view.c,75,20,20,False,False,True) elif zustand == 3: Ampel.zeichne(self.view.c,75,20,20,False,True,False)
Soll die Ampel selbstständig laufen, so benötigt sie einen Zeitgeber, einen Timer. Ein Timer ist ein sehr einfach aufgebauter Thread. Nach Ablauf der Zeit interval wird die Funktion routine aufgerufen. Das was ein Thread nach seinem Start tun soll, bestimmt die Methode run. Dh. run muss überschrieben werden. In unserem Fall wird der Thread zunächst mit sleep schlafengelegt und führt dann die Funktion routine aus. Gestartet wird ein Thread durch die Methode start(). Im vorliegenden Fall erzeugt die Routine weiter am Ende einen neuen Thread, der wieder weiter aufruft. Es entsteht also eine Endlosschleife. Bitte beachten, wie der Timer auf die jeweils aktuelle Phasenlänge programmiert wird.
# model import threading,time class Timer(threading.Thread): def __init__(self,interval,routine): threading.Thread.__init__(self) self.interval = interval self.routine = routine def run(self): time.sleep(self.interval) self.routine() class Model(object): def __init__(self): self.zustaende = ('rot','gelbrot','grün','gelb') self.phasenlaengen = (5,0.5,5,1) self.zustand = 0 self.timer = Timer(self.phasenlaengen[self.zustand],self.weiter) self.timer.start() def weiter(self): self.zustand = (self.zustand+1)%4 self.timer = Timer(self.phasenlaengen[self.zustand],self.weiter) self.timer.start()
Wenn model einen selbstständig laufenden Timer enthält, kann der Controller nicht mehr wissen, wann die Ampel in view neu gezeichnet werden soll. Eine mögliche Lösung ist, dass ein callback von view periodisch den Zustand von model abfragt (polling) und die Ampel entsprechend zeichnet.
Was ist eigentlich an ampel4.py so schlecht? Schaue dir die CPU-Auslastung an!
Eine weitere Möglichkeit besteht darin, auf das Polling zu verzichten und stattdessen in model ein callback OnChange einzubauen. Das Neuzeichnen der Ampel sollte dann aber threadsicher gemacht werden.
class Model(object): def __init__(self,OnChange): self.zustaende = ('rot','gelbrot','grün','gelb') self.phasenlaengen = (5,0.5,5,1) self.zustand = 0 self.OnChange = OnChange self.timer = Timer(self.phasenlaengen[self.zustand],self.weiter) self.timer.start() def weiter(self): self.zustand = (self.zustand+1)%4 self.OnChange() self.timer = Timer(self.phasenlaengen[self.zustand],self.weiter) self.timer.start()
Es ist reizvoll, die serielle Schnittstelle zur Ansteuerung einer externen Ampel zu verwenden. In folgendem Programm wird alle 20ms der Zustand des CTS-Eingangs abgefragt und gegebenenfalls ein Callback ausgelöst. Der Destruktor des Views sorgt dafür, dass auch bei einem unsauberen Beenden des Programms die Schnittstelle wieder geschlossen wird.
# -*- coding: iso-8859-1 -*- # mk, 22.4.09 # model import threading,time class Timer(threading.Thread): def __init__(self,interval,routine): threading.Thread.__init__(self) self.interval = interval self.routine = routine def run(self): time.sleep(self.interval) self.routine() class Model(object): def __init__(self,OnChange): self.zustaende = ('dauergrün','gelb','rot','gelbrot','grün') self.phasenlaengen = (0,0.5,5,1,5) self.zustand = 0 self.OnChange = OnChange def weiter(self): self.zustand = (self.zustand+1)%5 if self.OnChange != None: self.OnChange() if self.zustand != 0: self.timer = Timer(self.phasenlaengen[self.zustand],self.weiter) self.timer.start() # view import serial class View(object): def __init__(self,cbTaste): self.cb = cbTaste self.s = serial.Serial(0) self.t = Timer(0.02,self.poll) self.t.start() def __del__(self): # schließt sicher die Schnittstelle self.s.close() def rot(self,level): self.s.setRTS(level) def gelb(self,level): self.s.setBreak(level) def gruen(self,level): self.s.setDTR(level) def poll(self): if self.s.getCTS(): if self.cb != None: self.cb() self.t = Timer(0.02,self.poll) self.t.start() # controller class Controller(object): def __init__(self): self.model = Model(self.update) self.view = View(self.taste) self.z = None self.update() def update(self): za = self.z self.z = self.model.zustand if za != self.z: if self.z == 0: self.view.rot(0) self.view.gelb(0) self.view.gruen(1) elif self.z == 1: self.view.rot(0) self.view.gelb(1) self.view.gruen(0) elif self.z == 2: self.view.rot(1) self.view.gelb(0) self.view.gruen(0) elif self.z == 3: self.view.rot(1) self.view.gelb(1) self.view.gruen(0) elif self.z == 4: self.view.rot(0) self.view.gelb(0) self.view.gruen(1) def taste(self): self.z = self.model.zustand if self.z == 0: self.model.weiter() # Hauptprogramm c = Controller()