Was du hier lernst
- Sinus und Kosinus
- Grad und Bogenmaß
- Koordinaten auf dem Bildschirm
- Animationsschleife
- Modul datetime
- globale Variablen
So funktioniert die Uhr
Eine Uhr zu zeichnen klingt banal – ein Kreis und drei Striche. Die eigentliche Frage ist: Wo genau endet ein Zeiger? Um 15 Uhr zeigt der Stundenzeiger nach rechts, um 18 Uhr nach unten. Das Programm braucht also eine Regel, die aus einer Uhrzeit einen Punkt auf dem Bildschirm macht. Diese Regel ist der Kern des ganzen Programms, alles andere ist Drumherum.
1. Maße und Farben als Konstanten
Ganz oben stehen alle Größen, die das Aussehen bestimmen: Fenstergröße, Rand, Farben und die Längen der drei Zeiger. Die Zeigerlängen werden aus der Fenstergröße berechnet – machst du das Fenster größer, wachsen die Zeiger automatisch mit.
windowCenter = windowWidth / 2, windowHeight / 2
hourCursorLength = windowWidth / 2.0 - windowMargin - 140
secondCursorLength = windowWidth / 2.0 - windowMargin - 10
windowCenter ist ein Tupel aus zwei Zahlen, dem x- und dem y-Wert der Mitte. Farben sind ebenfalls Tupel: (255, 0, 0) bedeutet volles Rot, kein Grün, kein Blau.
2. Von der Uhrzeit zum Winkel
Ein voller Kreis hat 360 Grad. Das Zifferblatt teilt ihn für Minuten und Sekunden in 60 Teile, für Stunden in 12. Die Funktion getCursorPositionDegrees rechnet eine Position auf dieser Skala in Grad um:
def getCursorPositionDegrees(position, scale):
cursorOffset = -90 # 12 Uhr liegt bei -90 Grad
degrees = 360 / scale * position + cursorOffset
return degrees
15 Minuten ergeben 360 / 60 * 15 = 90 Grad. Warum dann noch -90? In der Mathematik liegt 0 Grad bei »3 Uhr«, auf der Uhr soll die Zählung aber oben bei der 12 beginnen. Die Verschiebung um −90 Grad dreht die Skala an die richtige Stelle.
Warum läuft die Uhr im Uhrzeigersinn?
In der Schule zeigt die y-Achse nach oben, auf dem Bildschirm zeigt sie nach unten. Dadurch drehen wachsende Winkel hier im Uhrzeigersinn – genau so, wie eine Uhr es braucht. Ein glücklicher Zufall, der das Programm kurz hält.
3. Vom Winkel zum Punkt: Sinus und Kosinus
Jetzt kommt die Trigonometrie. Ein Punkt auf einem Kreis mit Radius r um die Mitte (cx, cy) liegt bei
x = cx + cos(Winkel) · ry = cy + sin(Winkel) · r
Pythons math.cos und math.sin erwarten den Winkel allerdings im Bogenmaß, nicht in Grad. Ein voller Kreis sind im Bogenmaß 2π, also gilt Bogenmaß = Grad / 180 · π. Genau das rechnet gradToBogenmass, und getCirclePoint setzt alles zusammen:
def getCirclePoint(position, scale, cursorLength):
degrees = getCursorPositionDegrees(position, scale)
bogenmass = gradToBogenmass(degrees)
xPos = round(math.cos(bogenmass) * cursorLength + windowCenter[0])
yPos = round(math.sin(bogenmass) * cursorLength + windowCenter[1])
return (xPos, yPos)
4. Drei Zeiger mit einer Funktion
drawCursor zeichnet eine Linie von der Mitte zu dem Punkt, den getCirclePoint liefert. Alle drei Zeiger nutzen dieselbe Funktion, nur Farbe, Dicke, Länge und Skala unterscheiden sich:
drawCursor(hourColor, 15, hourCursorLength, hour + minute / 60.0, 12)
drawCursor(minuteColor, 8, minuteCursorLength, minute + second / 60.0, 60)
drawCursor(secondColor, 3, secondCursorLength, second + micro / 1000000.0, 60)
Der Trick steckt im vierten Argument. Der Stundenzeiger bekommt nicht nur die Stunde, sondern hour + minute / 60.0. Um 14:30 Uhr steht er deshalb genau zwischen der 2 und der 3 – wie bei einer echten Uhr. Beim Sekundenzeiger sorgen die Mikrosekunden dafür, dass er gleitet statt springt.
5. Die Hauptschleife
Eine Animation ist nichts anderes als sehr viele Einzelbilder. Die Schleife in main() zeichnet jedes Mal das komplette Bild neu: erst den Hintergrund mit Rand und Zifferblatt, dann die Zeiger, zuletzt den kleinen Knopf in der Mitte, der die Zeigerenden verdeckt.
while True:
drawBackground()
drawCurrentTime()
drawForeground()
await screen.frame(60)
await screen.frame(60) zeigt das fertige Bild an und wartet, bis das nächste an der Reihe ist – höchstens 60 Bilder pro Sekunde. Die Reihenfolge der Zeichenbefehle ist wichtig: Was später gezeichnet wird, liegt oben.
Genau hingeschaut
Im Zeitraffer-Modus rechnet timeGoesOn() mit if second > 60. Dadurch zählt die virtuelle Uhr bis 60, bevor sie umspringt – jede virtuelle Minute hat 61 Sekunden. Mit >= 60 wäre es korrekt. Bei der echten Uhrzeit spielt das keine Rolle, denn die liefert datetime fertig.
Probier mal
Jede Variante ändert den Code oben. Ein Klick auf »Ausprobieren« übernimmt die Änderung in den Editor und startet das Programm. »Zurücksetzen« holt das Original zurück.
-
01Zeitraffer: Die Uhr rast
Statt der echten Zeit zählt jetzt
timeGoesOn()eine eigene, virtuelle Zeit hoch – eine halbe Sekunde pro Bild. Bei 60 Bildern pro Sekunde läuft die Uhr also dreißigmal so schnell. MitvirtualSpeedkannst du noch einen Gang höher schalten.- useVirtualTimer = False + useVirtualTimer = True -
02Ein Zifferblatt mit Strichen
Die Uhr bekommt 60 Markierungen, jede fünfte etwas länger und dicker. Das Schöne: Du brauchst keine neue Mathematik.
getCirclePointliefert die Punkte für die Striche genauso wie für die Zeigerspitzen – einmal mit kleinerem, einmal mit größerem Radius.- screen.ellipse(inner, inner, windowWidth - inner * 2, windowWidth - inner * 2, clockBackgroundColor) + screen.ellipse(inner, inner, windowWidth - inner * 2, windowWidth - inner * 2, clockBackgroundColor) + for i in range(60): + start = getCirclePoint(i, 60, 228 if i % 5 == 0 else 240) + end = getCirclePoint(i, 60, 252) + screen.line(start, end, (200, 200, 170), 5 if i % 5 == 0 else 1) -
03Tick, tick statt Gleiten
Im Original gleitet der Sekundenzeiger, weil die Mikrosekunden mit eingerechnet werden. Lass sie weg, und der Zeiger springt wie bei einer Bahnhofsuhr im Sekundentakt.
- second + micro / 1000000.0 + second -
04Digitalanzeige dazu
Eine zweite Anzeige schreibt die Uhrzeit als Text ins Zifferblatt.
strftimeformatiert die Zeit,screen.textzeichnet sie mittig.- def drawForeground(): - screen.ellipse(windowWidth / 2.0 - 9, windowHeight / 2.0 - 9, 18, 18, clockMarginColor) + def drawForeground(): + screen.ellipse(windowWidth / 2.0 - 9, windowHeight / 2.0 - 9, 18, 18, clockMarginColor) + text = datetime.datetime.now().strftime("%H:%M:%S") + screen.text(text, (windowCenter[0], windowCenter[1] + 80), (200, 200, 170), size=34, align="center", font="mono") -
05Neue Farben
Alle Farben stehen als Konstanten ganz oben – ein Tupel aus Rot, Grün und Blau, jeweils von 0 bis 255. Hier eine Variante in Nachtblau und Orange. Probiere danach eigene Werte.
- secondColor = (255, 0, 0) + secondColor = (255, 140, 0) - minuteColor = (100, 200, 0) + minuteColor = (230, 230, 255) - hourColor = (100, 200, 0) + hourColor = (230, 230, 255) - clockMarginColor = (130, 130, 0) + clockMarginColor = (70, 90, 160) - clockBackgroundColor = (20, 40, 30) + clockBackgroundColor = (15, 20, 45)
Vom Buch in den Browser
Im Buch zeichnet die Uhr mit pygame in ein eigenes Fenster, geschrieben für Python 2.6. Im Browser gibt es kein Fenster und kein pygame – deshalb zeichnet die Uhr hier mit c4f, einer kleinen Zeichen-API dieser Website, auf eine Leinwand. Die Logik ist unverändert: dieselben Funktionen, dieselben Namen, dieselbe Rechnung.
| Im Buch (pygame) | Hier (c4f) |
|---|---|
pygame.display.set_mode((600, 600)) |
screen = Screen(600, 600) |
pygame.draw.ellipse(screen, farbe, (x, y, b, h)) |
screen.ellipse(x, y, b, h, farbe) |
pygame.draw.line(screen, farbe, start, ende, dicke) |
screen.line(start, ende, farbe, dicke) |
pygame.display.flip() + pygame.time.delay(10) |
await screen.frame(60) |
| Beenden per Taste oder Mausklick | der Knopf »Stopp« |
Die wichtigste Änderung ist das await. Ein Browser zeichnet nur, wenn ihm das Programm kurz die Kontrolle zurückgibt. Deshalb ist main() jetzt eine async-Funktion, und await screen.frame(60) macht die Pause, in der das Bild erscheint. Am Ende startet await main() das Programm – im Browser ist await auch außerhalb von Funktionen erlaubt. Auf dem eigenen Rechner würdest du dafür asyncio.run(main()) schreiben.
Außerdem:
global screensteht jetzt inmain(). Im Buch fehlte die Zeile im abgedruckten Listing (das ist ein Eintrag der damaligen Errata), in der Datei auf der Buch-CD war sie enthalten.globalsteht indrawCurrentTime()am Anfang der Funktion statt mitten imif-Zweig. Python 3 akzeptiert beides, am Anfang ist es aber übersichtlicher.- Die Ereignisbehandlung (
handleEvents) entfällt, weil sie nur dazu diente, das Fenster zu schließen.
Original aus dem Buch ansehen clock.py · Python 2
import pygame, sys, math, datetime
windowMargin = 30
windowWidth = 600
windowHeight = windowWidth
windowCenter = windowWidth/2, windowHeight/2
clockMarginWidth = 20
secondColor = (255, 0, 0)
minuteColor = (100, 200, 0)
hourColor = (100, 200, 0)
clockMarginColor = (130, 130, 0)
clockBackgroundColor = (20, 40, 30)
backgroundColor = (255, 255, 255)
hourCursorLength = windowWidth/2.0-windowMargin-140
minuteCursorLength = windowWidth/2.0-windowMargin-40
secondCursorLength = windowWidth/2.0-windowMargin-10
virtualSpeed = 1
useVirtualTimer = False
def handleEvents():
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit(0)
elif event.type == pygame.KEYDOWN:
sys.exit(0)
elif event.type == pygame.MOUSEBUTTONDOWN:
sys.exit(0)
def getCirclePoint(position, scale, cursorLength):
degrees = getCursorPositionDegrees(position, scale)
bogenmass = gradToBogenmass(degrees)
xPos = round(math.cos(bogenmass)*cursorLength+windowCenter[0])
yPos = round(math.sin(bogenmass)*cursorLength+windowCenter[1])
return (xPos, yPos)
def gradToBogenmass(degrees):
# python bietet auch die Funktion math.radians(degrees),
# welche die Umrechnung genauso ausfuehrt, aber so wird
# der Sachverhalt deutlicher
return degrees/180.0*math.pi
def getCursorPositionDegrees(position, scale):
cursorOffset = -90 # 12 o'Clock is -90 degrees
degrees = 360 / scale * position + cursorOffset
return degrees
def drawCursor(color, width, length, position, scale):
end = getCirclePoint(position, scale, length);
pygame.draw.line(screen, color, windowCenter, end, width)
def drawBackground():
screen.fill(backgroundColor)
pygame.draw.ellipse(screen, clockMarginColor, (windowMargin, windowMargin, windowWidth-2*windowMargin, windowWidth-2*windowMargin))
pygame.draw.ellipse(screen, clockBackgroundColor, (windowMargin+clockMarginWidth/2, windowMargin+clockMarginWidth/2, windowWidth-(windowMargin+clockMarginWidth/2)*2, windowWidth-(windowMargin+clockMarginWidth/2)*2))
def drawForeground():
pygame.draw.ellipse(screen, clockMarginColor, (windowWidth/2.0-9, windowHeight/2.0-9, 18, 18))
hour = 0
minute = 0
second = 0
micro = 0
def timeGoesOn():
global hour, minute, second, micro
micro += virtualSpeed
if micro >= 2: # halve seconds - not micro seconds
second += 1
micro %= 2
if second > 60:
minute += 1
second %= 60
if minute > 60:
hour += 1
minute %= 60
if hour > 12:
hour %= 12
def drawCurrentTime():
if useVirtualTimer:
global hour, minute, second, micro
timeGoesOn()
else:
now = datetime.datetime.now()
micro = now.microsecond
hour = now.hour
minute = now.minute
second = now.second
drawCursor( hourColor, 15, hourCursorLength, hour+minute/60.0, 12)
drawCursor(minuteColor, 8, minuteCursorLength, minute+second/60.0, 60)
drawCursor(secondColor, 3, secondCursorLength, second+micro/1000000.0, 60)
def main():
# Initialise screen
global screen
pygame.init()
screen = pygame.display.set_mode((windowWidth, windowHeight), pygame.HWSURFACE | pygame.DOUBLEBUF);
pygame.display.set_caption('Analog Clock');
# Event loop
while True:
handleEvents()
drawBackground()
drawCurrentTime()
drawForeground()
pygame.display.flip()
pygame.time.delay(10)
if __name__ == '__main__':
main()