Pygame sucks unter OS/X. Big times. Das vorkompilierte Binary will ich mir nicht installieren, da man das Zeug nie wieder aus dem System bekommt und ich zudem auch gar nicht das System-Python benutze. Schon gar keines, was ich von python.org als Binary installiert habe. Homebrew kennt kein Rezept für pygame und Macports beginnt aufgrund der Abhängigkeit zu numpy erst mal GCC und Fortran und allen möglichen X11-Scheiß zu installieren. Da ging es schneller, Ubuntu in einer VM zu starten...
Ich habe noch nie etwas mit pygame gemacht und auch nicht wirklich die Dokumentation gelesen doch das hier öffnet ein schwarzes Fenster und ist der Startpunkt für mein Experiment:
Code: Alles auswählen
import pygame
pygame.init()
screen = pygame.display.set_mode((400, 400))
while True:
event = pygame.event.poll()
if event.type == pygame.QUIT:
break
screen.fill((0, 0, 0))
pygame.display.flip()
Den Event-Kram scheine ich zu brauchen, um das Fenster per Klick wieder schließen zu können. Als Hintergrundfarbe für mein Fenster habe ich ein freundliches Schwarz gewählt. Mehr als den/die/das Offscreen-Surface-Objekt "screen" immer wieder zu füllen und anzuzeigen mache ich nicht. Bin trotzdem stolz drauf
Nun kann ich meine GUI-Bibliothek in einer Datei "ui.py" schreiben:
Code: Alles auswählen
import pygame
class View:
def __init__(self, rect, color=(240, 120, 0)):
self.rect = rect
self.color = color
self.subviews = []
self.superview = None
def add(self, view):
self.subviews.append(view)
view.superview = self
def remove(self):
self.superview.subviews.remove(self)
self.superview = None
def do_draw(self, surface):
self.draw(surface)
for view in self.subviews:
view.do_draw(surface)
def draw(self, surface):
pygame.draw.rect(surface, self.color, self.rect)
Fertig. Ein View hat eine Position und Größe, kann weitere Views als Kinder haben und kann sich selbst als Rechteck darstellen. Nicht viel, aber ein Anfang für ein GUI-Rahmenwerk. Mehr als ein Kompositum-Entwurfsmuster steckt da nicht hinter.
So kann ich's einbauen:
Code: Alles auswählen
import pygame, ui
pygame.init()
screen = pygame.display.set_mode((400, 400))
view = ui.View(pygame.Rect(10, 20, 30, 40))
view.add(ui.View(pygame.Rect(20, 20, 20, 10), (240, 0, 120)))
while True:
event = pygame.event.poll()
if event.type == pygame.QUIT:
break
screen.fill((0, 0, 0))
view.do_draw(screen)
pygame.display.flip()
Der nächste Schritt wären jetzt Layouts, mit denen ein View seine Kinder automatisch anordnen kann und die jedem View eine optimale Größe geben können. Layout möchte ich zudem nur machen, wenn es notwendig ist, d.h. ich muss schauen, wann mein rect geändert wird. Dann könnte ich auch das Zeichnen optimieren und zu jedem View ein surface-Objekt als Cache verwalten. Dazu müssen dann set_need_draw()-Aufforderungen durch die View-Hierarchie wandern. Braucht man aber vielleicht alles nicht, weil auch so schnell genug. Also will ich euch damit nicht langweilen. Und mein Lieblingslayout, welches flexible Grids benutzt, ist auch nicht so einfach zu schreiben.
Ich zeige lieber noch ein Label als weitere UI-Komponente:
Code: Alles auswählen
class Label(View):
def __init__(self, rect, text="", color=(255, 255, 255)):
View.__init__(self, rect, color)
self.text = text
self.font = pygame.font.SysFont("liberationsans", 16)
def draw(self, surface):
surface.blit(self.font.render(self.text, True, self.color), self.rect)
Ohne Layout und Größenbestimmung ist das allerdings nur der halbe Spaß. Ich müsste eigentlich schauen, wie groß der Text ist und dann diesen innerhalb des Rechtecks zentrieren oder irgendwie anders anordnen. Mehrzeiligen Text kann pygame wohl gar nicht, da müsste man sowieso viel rechnen. Auch für unterschiedliche Schriften, die man am besten alle an der Baseline ausrichtet, muss man noch mal nachdenken. Ich weiß auch nicht, was pygame bei unicode statt str macht... (Die Qualität der Schriftdarstellung ist übrigens trotz Antialiasing nicht so doll. Da bin ich besseres von OS X gewöhnt. Wie schade, denn damit steht und fällt viel bei einem UI-Rahmenwerk)
Für einen Button muss ich Mausklicks verarbeiten können. Dies ist meine 3-min-Lösung:
Code: Alles auswählen
...
event = pygame.event.poll()
if event.type == pygame.QUIT:
break
if event.type == pygame.MOUSEBUTTONDOWN:
active_view = view.subview_at(event.pos)
if active_view:
active_view.mouse_down(event.pos)
if event.type == pygame.MOUSEBUTTONUP:
if active_view:
active_view.mouse_up(event.pos)
Wie man sieht, suche ich bei einem Klick den angeklickten View und wenn ich einen finde, sage ich diesem, dass er angeklickt wurde. Wird die Maus wieder losgelassen, suche ich NICHT den passenden View, sondern sage dies dem alten. Das ist das übliche Mouse Capture-Verhalten, was zwar nicht für D&D (nicht das Rollenspiel sondern Drag 'n' Drop) passt, aber den Button schön einfach macht.
Dann kann ich die Klasse View wie folgt erweitern:
Code: Alles auswählen
def subview_at(self, pos):
for view in self.subviews:
subview = view.subview_at(pos)
if subview:
return subview
if self.rect.collidepoint(pos):
return self
def mouse_down(self, pos): pass
def mouse_up(self, pos): pass
Nun ist ein Button mit dem typischen Verhalten, dass man ihn drücken kann und das er eine Aktion auslöst, wenn man die Maus über ihm wieder loslässt, nicht weiter schwer:
Code: Alles auswählen
class Button(View):
def __init__(self, rect, color=(120, 120, 240)):
View.__init__(self, rect, color)
self.pressed = False
def mouse_down(self, pos):
self.pressed = True
def mouse_up(self, pos):
self.pressed = False
if self.rect.collidepoint(pos):
print "click"
def draw(self, surface):
View.draw(self, surface)
if self.pressed:
pygame.draw.rect(surface, (250, 250, 0), self.rect, 2)
Ich habe mich dafür entschieden, das Drücken mit einem gelben Rahmen zu visualisieren. Und die Aktion ist einfach ein "print". Das Prinzip sollte aber klar sein. Schön wäre, wenn man bei jeder Mausbewegung prüft, ob sie noch über dem Button ist und da dann noch einen weiteren Status verwaltet (mouse enter und mouse exit), doch das überlasse ich euch.
Wer einen Button mit Text haben will, soll ein Label hinzufügen. Nicht effizient, aber generisch und damit per Definition gut.
Nun bleiben eigentlich nur noch zwei Dinge, die etwas schwerer sind: Ein mehrzeiliges Texteingabefeld und ein ScrollView. Damit und einem passenden Layout kann man dann auch Listen und Tabellen bauen. Einen ScrollView bleibe ich euch schuldig, aber ein paar Zeichen eingeben, wie schwer kann das schon sein.
Trial und error zeigt, ich brauche zwei Argumente für eine key_down-Methode, über die ich mir einen Tastendruck schicke: eines mit dem Key-Code, an dem ich z.B. Cursorbewegungen erkenne und eines mit dem Zeichen, das eingegeben wurde:
Code: Alles auswählen
class TextEdit(Label):
def __init__(self, rect, text=""):
Label.__init__(self, rect, text)
self.cursor = 0
def key_down(self, key, ch):
if key == 275 and self.cursor < len(self.text):
self.cursor += 1
elif key == 276 and self.cursor > 0:
self.cursor -= 1
elif key == 8 and self.cursor > 0:
self.cursor -= 1
self.text = self.text[:self.cursor] + self.text[self.cursor + 1:]
elif key >= 32 and ch:
self.text = self.text[:self.cursor] + ch + self.text[self.cursor:]
self.cursor += 1
def draw(self, surface):
Label.draw(self, surface)
w, h = self.font.size(self.text[:self.cursor])
pygame.draw.rect(surface, (240, 40, 40), pygame.Rect(self.rect.left + w, self.rect.top, 2, h))
Ich stelle einen roten Cursor über dem durch die von Label geerbte Methode gezeichneten String dar. Blinkt zwar nicht, ist aber so sehr einfach. Eigentlich müsste ich auch nicht 30 mal pro Sekunde (oder wie häufig pygame das flip() macht) die Größe bestimmen, sondern muss das nur machen, wenn der Benutzer mal ein Zeichen eingibt, doch optimieren wollte ich nicht.
Daher sage ich auch einfach, den (unsichtbaren) Fokus hat immer die angeklickte Komponente, d.h. ich kann meine Hauptschleife wie folgt erweitern:
Code: Alles auswählen
if event.type == pygame.KEYDOWN:
if active_view:
active_view.key_down(event.key, event.unicode)
Und fertig ist mein Texteingabefeld, in dem ich den Cursor bewegen kann (nachdem ich es einmal angeklickt habe), wo ich mit Backspace (nicht aber DEL) ein Zeichen löschen und ansonsten beliebige Zeichen eingeben kann. Leider funktioniert kein Tastatur-Repeat. Ach, und es wäre natürlich schön, wenn ich auch noch mit der Maus den Cursor setzen könnte.
Jetzt vielleicht nur eine Selektion? Und bitte mehr als eine Zeile. Und natürlich Wortumbruch. Und Cut/Copy/Paste. Dafür dann bitte ein Kontextmenü. Und dann einen blinkenden Cursor.
Allgemein Animationen wären nett. Wenn ich die Farbe eines Buttons ändere, weil er angeklickt wurde, soll das bitte nicht sofort passieren, sondern innerhalb von 0,5 Sekunden. Und wenn die Animation noch nicht abgeschlossen ist und ich nochmals die Farbe ändere, muss ich die erste abbrechen können und es darf zu keinem Farbsprung kommen. So schwer kann das eigentlich auch alles nicht sein. Ich vermute, ein guter Ansatz ist wie der von Apples UIKit, wo die UIView-Hierarchie noch einmal intern komplett als CALayer-Hierarchie gedoppelt wird, die sich dann um alle Animationen kümmert.
Stefan