Ein Toolkit auf SDL rendern

Du hast eine Idee für ein Projekt?
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Xynon1, ohne wirklich pygame studiert zu haben sieht es wirklich so aus, dass da schon Support für diverse Grundoperationen vorhanden ist. Von Rect ableiten würde ich dennoch nicht. Vererbung ist in der Regel kein gutes Design, besser ist eine Komposition, siehe mein Beispiel, wo Views ein Rect haben, aber kein Rect sind. Gleiches gilt für Button, den du nicht von Label ableiten willst, sondern der bestenfalls ein Label enthält. Was, wenn du eine Grafik im Button darstellen willst? Eine zweite Button-Unterklasse? Keine gute Idee. Zu Sprites kann ich nichts sagen, aber wenn du eh X mal pro Sekunde das UI darstellst, ist Überlappung kein Problem, es wird ja in der richtigen (immer gleichen) Reihenfolge alles dargestellt.

Focusverwaltung ist IMHO einfach, siehe meine "active_view"-Variable für eine absolute Minimallösung. Statt ein und die selbe Variable für Maus und Tastatur zu benutzen, sollten das aber zwei sein und dann kann jede Komponente fragen, ob sie fokussiert ist und sich anders darstellen. Z.B. hat nur das fokussierte Texteingabefeld einen Cursor. Nicht fokussierte Eingabefelder zeigen zudem meist eine Selektion in einer anderen Farbe an, als wenn sie fokussiert wären. Andere Komponenten wie Buttons haben meist noch eine Strichellinie.

Stefan
Xynon1
User
Beiträge: 1267
Registriert: Mittwoch 15. September 2010, 14:22

Hui, danke für die Mühe und die interessanten Ausführungen. Ich werde das ganze wohl noch einmal gründlich lesen müssen, aber ein paar Anmerkungen hätte ich dennoch schon.

1. Wieso sollte man zum Beispiel nicht von "Rect" erben? Dies würde mir besonders die Positionierung wesentlich erleichtern. In deiner View müsste ich sagen "view.rect.center = 45", wieso also nicht gleich "view.center = 45". Positionen und Größen sind doch eindeutige Eigenschaften eines Widgets. Was spricht da so gegen Vererbung?
Bei dem Button stimme ich dir allerdings voll und ganz zu, ich hatte jetzt hin und her überlegt wie man die Strucktur von den verschieden Buttons(und dem Label) möglichst einfach halten kann und bin auch nur zu diesem Schluss gekommen.

2. Maus Enter/Exit wäre IMHO ein Fokus und sollte leicht mit einer Methode ermöglicht werden können. Werde ich mir auch gleichmal näher anschauen.

3. Wieso das beim TextEdit wirklich mit *echten* Strings gestalten? Sollte es nicht effektiver sein den Text als Liste vorzuhalten und immer beim "redraw" mit ".join" diesen zuverbinden, als ihn bei jedem zeichnen zu zerstückeln und zu addieren. Allerdings müsste ich das erstmal ausprobieren. (Warscheinlich ist deines schneller - so wie ich mich mit meinen Vermutungen kenne)

Ich habe heute auch schon angefangen und würde mal behaupten ich bin schon ein gutes Stück weiter. Du hast das warscheinlich auch nur mal schnell nebenbei geschrieben.:mrgreen: Ich werden so schnell es mir möglich ist meine jetzige Version hochladen. Es würde mich freuen wenn du die Zeit findest dann mal drüber zuschauen.
btw. ich habe immer noch keinen Namen wie ich das Toolkit nennen könnte.
Traue keinem Computer, den du nicht aus dem Fenster werfen kannst.
Xynon auf GitHub
Benutzeravatar
snafu
User
Beiträge: 6740
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

@Xynon1: Vererbung stellt normalerweise eine Ist-Ein-Beziehung dar. Und ein View ist kein verändertes Rect, sondern ein View nutzt das Rect als Hilfsmittel zur Darstellung. Auch wenn es dir wie unnötige Tipparbeit vorkommt, so finde ich es durchaus sauberer, die besagte Trennung zu vollziehen. Guck dir mal an, wie andere Frameworks das machen, z.B. in Qt das QRect, welches an die Basisklasse QWidget gehangen wird.

Du solltest halt im Hinterkopf behalten, dass so ein View, wie es beispielhaft von sma gezeigt wurde, noch etwas mehr tut, als sich bloß um die Positionierung zu kümmern. Es vereint sozusagen mehrere Einzelaufgaben miteinander. Im Beispiel springt einem ja `self.color` direkt ins Auge. Wenn du da zuviel miteinander vermischt und wenn dies auch noch von den eigentlichen Widgets als Basis genutzt werden soll, dann verliert man irgendwann schnell den Überblick. Zudem verschwimmt damit die Einteilung in Aufgabenbereiche, die für Klassen eigentlich üblich ist.

Übrigens lässt sich die Problematik ja relativ gut abstrahieren, ohne dass man bloße Forwarding-Methoden, wie `center()`, zur Positionierung einbauen muss. Ein Label etwa könnte mit dem Attribut `alignment` erstellt werden. Für die eigentlich Positionierung der Widgets könnte man Layout-Manager zur Verfügung stellen. Das ist ohnehin sauberer.

Mir ist natürlich klar, dass man ein Spiel-UI nicht direkt mit einem "normalen" GUI-Framework vergleichen kann, ich denke aber trotzdem, dass man durchaus auf bewährte Methoden zurückgreifen sollte. Zum Einen haben diese sich als praktikabel erwiesen und zum Anderen sind Programmierer, die schon mal etwas mit grafischen Oberflächen zu tun hatten, mit den entsprechenden "Gepflogenheiten" vertraut.
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Ich hatte Lust noch einen `ScrollView` zu bauen. Zuerst führe ich noch ein `Layout`-Objekt ein, welches die optimale Größe eines Views unter Berücksichtigung der Größen der Kinder bestimmen kann (`best_size`) und alle Kinder anordnen kann (`do_layout`):

Code: Alles auswählen

    class Layout:
        def best_size(self, view):
            return view.rect.size
        
        def do_layout(self, view):
            pass

    class VBox(Layout):
        def best_size(self, view):
            width, height = 0, 0
            for subview in view.subviews:
                w, h = subview.best_size()
                width = max(width, w)
                height += h
            return width, height
        
        def do_layout(self, view):
            top = view.rect.top
            width, height = view.rect.size
            for subview in view.subviews:
                _, h = subview.best_size()
                subview.set_rect(Rect(view.rect.left, top, view.rect.width, h))
                subview.do_layout()
                top += h
Nun kann ich meinen `View` ändern, damit er ein `Layout` benutzt:

Code: Alles auswählen

    class View:
        def __init__(self, ...):
            self.layout = Layout()
            self.need_layout = False
        
        def add(self, view):
            self.need_layout = True
            self.subviews.append(view)
            view.superview = self
        
        def remove(self):
            self.superview.need_layout = True
            self.superview.subviews.remove(self)
            self.superview = None
        
        def best_size(self):
            return self.layout.best_size(self)
        
        def do_layout(self):
            if self.need_layout:
                self.layout.do_layout(self)
                self.need_layout = False
        
        def set_rect(self, rect):
            if self.rect != rect:
                self.set_pos(rect.topleft)
                self.set_size(rect.size)
        
        def set_pos(self, pos):
            if self.rect.topleft != pos:
                self.move(pos[0] - self.rect.left, pos[1] - self.rect.top)
        
        def move(self, dx, dy):
            self.rect = self.rect.move(dx, dy)
            for view in self.subviews:
                view.move(dx, dy)
        
        def set_size(self, size):
            if self.rect.size != size:
                self.rect = Rect(self.rect.topleft, size)
                self.need_layout = True
        
        def pack(self):
            self.set_size(self.best_size())
            self.do_layout()
Die Methode `best_size` delegiert die Arbeit nun an das Layout-Objekt. In `do_layout` tue ich nur etwas, wenn auch ein Layout notwendig ist - und das ist es nur, wenn ich Kind-Views hinzufüge oder entferne oder die Größe ändere, was ich in `set_rect` prüfe und in `set_size` mache. Ich muss noch den Fall berücksichtigen, dass ich den View verschiebe. In diesem Fall muss ich auch alle Kinder verschieben, da ich nur ein gemeinsames Koordinatensystem habe. Meist haben UI-Rahmenwerke lokale Koordinaten, d.h. Kinder sind immer relativ zu ihren Eltern positioniert. Vielleicht sollte ich subsurfaces benutzen, damit wäre es vielleicht einfacher.

Für einen `Label` kann ich nun `best_size` überschreiben:

Code: Alles auswählen

    class Label:
        def best_size(self):
            return self.font.size(self.text)
Nun kann ich eine Liste von Labels erzeugen und automatisch anordnen:

Code: Alles auswählen

    labels = ui.View(pygame.Rect(0, 0, 0, 0), (50, 100, 200))
    labels.layout = ui.VBox()
    for i in range(10):
        labels.add(ui.Label(pygame.Rect(0, 0, 0, 0), "Zeile %d" % i))
    view.add(ui.ScrollView(pygame.Rect(200, 5, 70, 90), labels))
Ein `ScrollView` kann nun so einen View als Kind bekommen, darf aber nicht alles darstellen, sondern nur den Ausschnitt, der seinem Rect entspricht. Darüber mal ich dann noch einen Scroll-Indicator. Daher überschreibe ich `do_draw`.

Code: Alles auswählen

    class ScrollView(View):
        def __init__(self, rect, content):
            View.__init__(self, rect, (192, 192, 192))
            content.set_pos(rect.topleft)
            content.pack()
            self.add(content)
        
        def do_draw(self, surface):
            surface.set_clip(self.rect)
            self.subviews[0].do_draw(surface)
            self.draw(surface)
            surface.set_clip(None)
        
        def draw(self, surface):
            content = self.subviews[0]
            bh = self.rect.height - 6
            ch = content.rect.height
            th = self.rect.height * bh / ch
            ty = min((self.rect.top - content.rect.top) * bh / ch, bh - th)
            bar = Rect(self.rect.left + self.rect.width - 6, self.rect.top + 3, 3, bh)
            pygame.draw.rect(surface, (128, 128, 128), bar)
            thumb = Rect(bar.left, bar.top + ty, 3, th)
            pygame.draw.rect(surface, (224, 224, 224), thumb)
Was jetzt noch fehlt ist die Möglichkeit zum Scrollen. Ich reagiere einfach mal auf Cursorbewegungen:

Code: Alles auswählen

    def key_down(self, key, ch):
        content = self.subviews[0]
        if key == 274 and content.rect.bottom > self.rect.bottom:
            content.move(0, -10)
        if key == 273 and content.rect.top < self.rect.top:
            content.move(0, +10)
Ich hatte nachgelesen, dass Scrollen als Klick von Mousebutton 4 bzw. 5 weitergegeben wird, das könnte genauso funktionieren. Da mir das Verhalten vom iPhone, auf zu weites scrollen durch zurückfedern zu reagieren, recht gut gefällt, bräuchte ich spätestens jetzt einen Mechanismus derartige Animationen zu programmieren. Einen anklickbaren Rollbalken halte ich jedenfalls (wie ja auch Ubuntu 11.04) für unnötig, der hat denn noch eine Maus ohne Scroll-Rad bzw. Scroll-Funktion per Touch.

Stefan
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

[So, "einen" hab' ich noch...]

Wenn ich z.B. die Farbe eines Views über den Zeitraum von zwei Sekunden von rot in grün ändern will, brauche ich dafür eine `Animation`, genauer eine `PropertyAnimation`, genauer eine `ColorPropertyAnimation`. Alle Animationen verwalte ich in einem `Animator`:

Code: Alles auswählen

    class Animator:
        def __init__(self):
            self.animations = []
        
        def schedule(self, animation):
            self.animations.append(animation)
            animation.base = pygame.time.get_ticks()
        
        def unschedule(self, animation):
            if animation in self.animations:
                self.animations.remove(animation)
        
        def tick(self):
            time = pygame.time.get_ticks()
            for animation in self.animations:
                animation.tick(time - animation.base)
    
    Animator = Animator()
    
    class Animation:
        def __init__(self, duration=1000):
            self.duration = duration
    
        def tick(self, time):
            pass
    
    class PropertyAnimation(Animation):
        def __init__(self, target, name, duration=1000):
            Animation.__init__(self, duration)
            self.target, self.name = target, name
    
    class ColorPropertyAnimation(PropertyAnimation):
        def __init__(self, target, name, to_color, duration=1000):
            PropertyAnimation.__init__(self, target, name, duration)
            self.from_color = getattr(target, name)
            self.to_color = to_color
        
        def tick(self, time):
            if time >= self.duration:
                setattr(self.target, self.name, self.to_color)
                Animator.unschedule(self)
            
            p = float(time) / self.duration
            setattr(self.target, self.name, (
                self.interpolate(self.from_color[0], self.to_color[0], p),
                self.interpolate(self.from_color[1], self.to_color[1], p),
                self.interpolate(self.from_color[2], self.to_color[2], p)))
        
        def interpolate(self, from, to, p): return to * p + from * (1 - p)
Die Formel, wie ich Farben ineinander übergehen lasse, ist nicht perfekt, da sie nicht einfach RGB benutzen darf, aber das sei egal. Auch könnte man sich überlegen, dass nicht nur eine lineare Funktion für den Animationsverlauf benutzt werden soll. Mir geht es eher um den Mechanismus, dass ich nun in `mouse_down` eines Buttons z.B. dies machen kann (und meine alte `draw`-Methode einfach entfernen kann):

Code: Alles auswählen

    class Button(View):
        def mouse_down(self, pos):
            Animator.unschedule(self.a)
            self.a = ColorPropertyAnimation(self, "color", (250, 250, 0), 250)
            Animator.schedule(self.a)
        
        def mouse_up(self, pos):
            Animator.unschedule(self.a)
            self.a = ColorPropertyAnimation(self, "color", (120, 120, 240), 250)
            Animator.schedule(self.a)
In meiner Hauptschleife muss ich vor dem `do_draw` jetzt noch die Animationen laufen lassen:

Code: Alles auswählen

    ui.Animator.tick()
    
    view.do_draw(screen)
Will ich jetzt z.B. meinem `ScrollView` scrollen, brauche ich:

Code: Alles auswählen

    class MoveAnimation(Animation):
        def __init__(self, target, pos, duration=1000):
            Animation.__init__(self, duration)
            self.target = target
            self.to_pos = pos
            self.from_pos = target.rect.topleft
        
        def tick(self, time):
            p = float(time) / self.duration
            if p >= 1:
                self.target.set_pos(self.pos)
                self.unschedule()
            target.set_pos((
                self.interpolate(self.from_pos[0], self.to_pos[0], p),
                self.interpolate(self.from_pos[1], self.to_pos[1], p)))
War eigentlich gar nicht schwer... :)

Stefan
Xynon1
User
Beiträge: 1267
Registriert: Mittwoch 15. September 2010, 14:22

Ich seh schon jetzt muss ich selbst erstmal was posten, sonst hast du bald eine komplette eigene GUI :mrgreen: Allerdings ist bei mir noch nicht viel Sichtbares, da ich immer noch am Focushandling und der Eventsteuerung arbeite. Sieh es dir mal, wenn du Zeit hast, an. Über Verbesserungsvorschläge wäre ich natürlich froh, noch steht ja nicht allzuviel fest :D


Ich habe mal meinen aktuellen Stand hochgeladen: https://github.com/xynon/pygame_gui
Traue keinem Computer, den du nicht aus dem Fenster werfen kannst.
Xynon auf GitHub
Xynon1
User
Beiträge: 1267
Registriert: Mittwoch 15. September 2010, 14:22

Ich möchte einfach nur mal schreiben das ich an meiner GUI weiter baue, jetzt unter dem Namen Iyaon und hier zu finden https://github.com/xynon/iyaon. Es geht langsam vorwärts, auch wenn noch viel einfach zu grob und noch nicht dokumentiert ist, so kann man dennoch schon erste Ergebnisse sehen.

Ist Prinzipiel für Python3.x gebaut, sollte aber ab Python2.6 funktionieren. Numpy ist nicht nötig, empfiehlt sich aber wenn man eine transparente GUI haben möchte.
Traue keinem Computer, den du nicht aus dem Fenster werfen kannst.
Xynon auf GitHub
Antworten