GUI Entwicklung - Events



  • Hi,

    Ich arbeite an einer 2d Isometric Engine, basierend auf SDL2 für einen SimCity 2000 Klon. Hier der Link zu meinem Projekt:
    https://github.com/JimmySnails/IsometricEngine

    (Falls euch das Thema interessiert, ich suche auch nach Leuten die Lust haben mitzumachen 🙂 )

    Bei dem Projekt wage ich mich auch zum ersten Mal an UI Entwicklung. Das ist ein ziemlich interessantes, aber auch überaus komplexes Thema. Ein Framework möchte ich nicht verwenden, da ich dabei auch was lernen möchte.

    In meiner aktuellen Implementation habe ich eine Klasse (UIManager), die das UI Layout aus einem JSON File ausliest, anschließend alle UI Objekte instanziert und in einem Vector<UIElement> (das ist die Basisklasse für alle UI Objekte) speichert.
    In der Eventloop frage ich bei jedem Event, alle Objekte in dem Vektor, ob sie geklickt sind. Falls ja, wird intern darauf reagiert und ich bekomme das Objekt aus dem Vektor zurück, das geklickt wurde.
    Die Texturen der UI Elemente (normal, clicked, hovered) behandle ich in einer extra schleife.

    Das ganze war eine quick and dirty lösung, die zwar funktioniert, aber nicht sonderlich elegant ist. Nun würde ich es gerne richtig machen.

    Ich habe eine nette template class für Signal/Slots gefunden (ähnlich der von QT).
    Allerdings bin ich mir nun nicht wirklich sicher, wie ich das am besten implementieren kann. Soweit ich jetzt gelesen habe, ist das Signal Slots konzept eher für Callbacks, die ausgelöst werden, wenn der Button ein click signal emitted. Also eher das was "nach" dem event handling passiert.

    Nun frage ich mich, wie man am besten mit Events umgeht. In SDL2 gibt es das SDL_Event objekt, dass all diese Infos bereitstellt. Eine Lösung wäre z.b., den Objekten einen Pointer auf dieses Objekt mitzugeben, und intern auszuwerten.

    Mein ursprüngliches Konzept für die Signal/Slot implementation, die ich dann allerdings wieder verworfen habe, wäre ein Interface für UI Objekte, dass Funktionen wie onClick() definiert. Dann eine "SignalHandler" Klasse als Singleton, mit deren Hilfe die Event Loop click Signale (mit bildschirmkoordinaten) emitten kann, und der UI Manager die Objekte beim erstellen anmeldet.
    Aber über diesen Ansatz bin ich mir jetzt leider gar nicht mehr sicher....

    Was denkt ihr?



  • @jimmysnails sagte in GUI Entwicklung - Events:

    Allerdings bin ich mir nun nicht wirklich sicher, wie ich das am besten implementieren kann. Soweit ich jetzt gelesen habe, ist das Signal Slots konzept eher für Callbacks, die ausgelöst werden, wenn der Button ein click signal emitted. Also eher das was "nach" dem event handling passiert.

    Ich weiß nicht, worauf du insgesamt hinauswillst. Oder überhaupt... Callback ist erstmal grundsätzlich schon etwas, was hier passt. Du könntest boost::signals2 verwenden, oder was ähnliches implementieren (kann auch viel einfacher sein).
    Signal/Slot ist für mich erstmal eigentlich kein konkretes eigenständiges Konzept. Wenn das jemand sagt, will ich trotzdem erstmal Code sehen, damit ich was drunter vorstellen kann.
    Und in Qt passiert es nicht "danach", das ist eigentlich ein Implementierungsdetail. Da sieht die Kapselung etwas anders aus. Grob gesagt ruft die Event Loop eine virtuelle event Funktion auf dem Button auf, der hat da ein switch-case, und bei einem entsprechenden Event Typ wird das clicked Signal emitiert, und an dem hängen dann erst die Event Handler.
    Ich seh darin aber keinen Grund, warum das in deinem Fall nicht passen sollte.



  • @mechanics Ich hab befürchtet, dass die Frage ein wenig vage ist... Wie gesagt, das Thema ist ziemlich komplex für mich.

    Ich möchte an dieser Stelle mal zwischen zwei Dingen unterscheiden. Wenn der Button geklickt wird und dann das Ereignis stattfindet (callback funktion) und was ich eigentlich meinte, wie der Button überhaupt weiß, dass er geklickt ist.

    Verwenden möchte ich vorerst mal diese Implementation von Signals verwenden:
    https://github.com/larspensjo/SimpleSignal

    Mein Ansatz wäre hier (pseudocode) folgender:

    // Die Klasse, die die Objekte instanziert und in einem vector "hält".
    class UiManager {
      Signal clickSignal; // Signal Objekt erzeugen
    
      for (... Objekte erzeugen) {
        Button *button = new Button; // Button objekt (eigtl unique_ptr)
        clickSignal.connect(button, &Button::onClick) // onClick funktion des Buttons registrieren
      }
      
      
      // Funktion, die in der Event loop aufgerufen wird, wenn ein click stattfindet.
      void emitSignal(Event details) {
        clickSignal.emit(details);
      }
    }
    
    // Hier werden die events bearbeitet, nicht nur für das UI, auch für das Spiel
    class EventManager {
      void eventLoop() {
        if (event == click)
        {
          UiManager.emitSignal(eventDetails);
        }
      }
    }
    
    // Basisklasse / Interface für Events.
    class eventListener {
      void handleEvents(eventDetails) {
        if (eventDetails == imButton())
          onClick(eventDetails);
        
          
      }
      virtual void onClick(eventDetails);
    }
    
    // Button 
    class Button : eventLister{
      void onClick(eventDetails) {
        // interne textur ändern und später dann eine Button callback funktion zur Verfügung stellen, die hier aufgerufen wird.
      }
    }
    

    Ich werde das heute Abend ausprobieren. Meine Frage ist allerdings, ob das ein guter Weg ist, um Events im Button (und andren elementen) zu bearbeiten oder ob man das sinnvoller machen könnte / sollte. Bzw. ob signals so eingesetzt werden sollten, oder ich die handleEvents funktion lieber direkt aufrufen sollte...

    Ein Nachteil ist z.b., das immer alle UI Objekte über events (auch mouse move) benachrichtigt werden und diese dann immer auswerten müssen und ich mir jetzt noch keine Gedanken darüber gemacht habe, was passiert wenn zwei Elemente übereinander liegen....



  • Ich würds jetzt nicht als komplex bezeichnen... Was mir an der Fragestellung eher unklar war/ist, was dir jetzt daran konkret wichtig ist. z.B. hast du dich anscheinend am Begriff "Callback" gestört und ich war dann erstmal irritiert, ob´s dir um irgendwelche Haarspalterei mit dem Begriff geht, oder ob du einen ganz anderen technischen Ansatz suchst... Einen "Event-Handler" kann man immer als "Callback" bezeichnen.

    Deinen Aufbau finde ich erstmal ungewohnt... Zum einen schon mal den Namen "onClick" im Button. Das ist deinem Aufbau relativ konsequent, aber ich weiß nicht, ob das eine gute Idee ist. Es ist normalerweise so, dass nicht der Button der eigentliche Konsument ist. Ich hätte eher erwartet, dass der Button einen Signal clickSignal hat, und die eigentlichen Konsumenten sich damit verbinden und deren Funktion von mir aus "onClick" heißt.
    Und der Button wiederum hätte einen anderen Mechanismus, um vom Event Manager (die Rolle des UI Managers leuchtet mir hier dann eigentlich auch nicht ein) über den stattgefundenen Klick benachrichtigt zu werden. Wie gesagt, in Qt wär das so, dass der Event Dispatcher (ist denke ich auch ein besserer Name als Manager) dann bestimmt, welcher Button geklickt wurde (irgendwo muss man sich die Koordinaten und die z-Order anschauen), und dann auf dem Button event Aufruft. Und die Implementierung der event Funktion würde dann das Signal emitieren.

    Das wäre ein möglicher Ansatz...

    Wenn du willst, könntest du auf die Signale der einzelnen Objekte auch verzichten und z.B. nur über den Event Manager/Dispatcher gehen. Der hätte dann ein Signal clicked(button*). Allerdings willst du wahrscheinlich schon auch im Button selber auf den Klick reagieren und den z.B. anders zeichnen, während er gedrückt ist. Deswegen würden sich auch alle Buttons am Event Manager registrieren und du hättest dadurch erstmal keinen Vorteil.



  • Mal so ganz am Rande: Ein Button sollte kein Event auslösen, wenn er angeklickt wurde. Erst beim Loslassen der Maustaste über dem Button sollte es geschehen und auch nur, wenn der Button zuvor gepusht wurde. Ähnlich auch Tasten wie die Leertaste, erst beim Loslassen und wenn zwischenzeitlich keine anderen Tasten (auch Maustasten) gedrückt wurden.
    Man sieht es natürlich immer wieder, ich halte es aber dennoch für falsch.



  • Ja richtig, ein click kommt erst nach dem Loslassen.
    Üblicherweise gibts auch aber auch die Zwischenevents, z.B. mousePressEvent und mouseReleaseEvent.



  • Danke für eure Antworten!

    Ja, das mit dem MouseRelease über dem Button (wo schon der Click selbst über dem Button sein muss) ist mir schon klar. Das hab ich aktuell auch so im master implementiert:

    Hier ist der "Handler" für die Mouse Events fürs Zeichnen:
    https://github.com/JimmySnails/IsometricEngine/blob/805ee881cda8d89b7125eeadfbbe1e9951b20624/src/engine/uiManager.cxx#L131

    Hier ist die Funktion im UI Manager, die das (erste) Element, dass unter dem Button liegt zurückliefert:
    https://github.com/JimmySnails/IsometricEngine/blob/805ee881cda8d89b7125eeadfbbe1e9951b20624/src/engine/uiManager.cxx#L185

    Und hier ist die Funktion im Event Manager, die sich um die tatsächlichen Aktionen kümmert, die der Button auslösen soll:
    https://github.com/JimmySnails/IsometricEngine/blob/805ee881cda8d89b7125eeadfbbe1e9951b20624/src/engine/eventManager.cxx#L104

    @mechanics sagte in GUI Entwicklung - Events:

    (die Rolle des UI Managers leuchtet mir hier dann eigentlich auch nicht ein)

    Der UI Manager ist u.a. dafür da, JSON Files (mit dem ganzen UI Layout, das soll mod-bar werden) zu parsen, und dementsprechend die UI Elemente zu instanzieren.

    @mechanics sagte in GUI Entwicklung - Events:

    Ich würds jetzt nicht als komplex bezeichnen... Was mir an der Fragestellung eher unklar war/ist, was dir jetzt daran konkret wichtig ist. z.B. hast du dich anscheinend am Begriff "Callback" gestört und ich war dann erstmal irritiert, ob´s dir um irgendwelche Haarspalterei mit dem Begriff geht, oder ob du einen ganz anderen technischen Ansatz suchst... Einen "Event-Handler" kann man immer als "Callback" bezeichnen.

    @mechanics sagte in GUI Entwicklung - Events:

    Ganz konkret hab ich mich jetzt auf die Aktion die beim Button Click ausgeführt werden soll bezogen. Mein Problem ist, dass ich mich noch nie mit UI Entwicklung beschäftigt habe und mir die Konzepte unklar sind. Drum bitte um Nachsicht 🙂

    Deinen Aufbau finde ich erstmal ungewohnt... Zum einen schon mal den Namen "onClick" im Button. Das ist deinem Aufbau relativ konsequent, aber ich weiß nicht, ob das eine gute Idee ist. Es ist normalerweise so, dass nicht der Button der eigentliche Konsument ist. Ich hätte eher erwartet, dass der Button einen Signal clickSignal hat, und die eigentlichen Konsumenten sich damit verbinden und deren Funktion von mir aus "onClick" heißt.

    Ja, der Button würde dann auch eine registerClickSignal() funktion bekommen, auf der man eine Callback Funktion auf das Click Signal registrieren kann. Diese (zweite, sich im Button selbst befindliche) Click Signal würden dann von der OnClick() Funktion emitted werden. Ich hab ja extra dazugeschrieben, dass ich mich bei meinem Pseudo Code Beispiel nur darauf beziehe, wie dieses Click Event am Button ausgelöst werden könnte.

    @mechanics sagte in GUI Entwicklung - Events:

    Allerdings willst du wahrscheinlich schon auch im Button selber auf den Klick reagieren und den z.B. anders zeichnen, während er gedrückt ist.

    Richtig, das mach ich momentan extrem umständlich über ein enum von aussen in dem uiManager (erster Link) und das würde ich lieber im Button machen.

    @mechanics sagte in GUI Entwicklung - Events:

    Und der Button wiederum hätte einen anderen Mechanismus, um vom Event Manager (die Rolle des UI Managers leuchtet mir hier dann eigentlich auch nicht ein) über den stattgefundenen Klick benachrichtigt zu werden. Wie gesagt, in Qt wär das so, dass der Event Dispatcher (ist denke ich auch ein besserer Name als Manager) dann bestimmt, welcher Button geklickt wurde (irgendwo muss man sich die Koordinaten und die z-Order anschauen), und dann auf dem Button event Aufruft. Und die Implementierung der event Funktion würde dann das Signal emitieren.

    Wenn ich das richtig verstehe, wäre das die Alternative, die ich kurz angesprochen habe.

    Also statt den Button selbst auf die Signale zu connecten, in der EventListener Basisklasse die handleEvents funktion aufzurufen, die dazugehörige (z.b. onMouseButtonRelease) aufzurufen, falls die Koordinaten (und die z-ORder...) richtig sind.

    Das kommt meiner Meinung nach auch auf dasselbe raus, nur halt ohne Signale.

    //eventloop
    
    if (clickEvent) {
      for (auto it : UiElementsVector)
        it->handleEvents(Eventdetails);
    }
    

    Die Signale selbst, würden dann nur für die Callback Funktion im Button verwendet werden. (Und nicht, um das Click-Event im Button selbst auszulösen!) Von aussen, (z.b. im UI Manager nach dem Instanzieren) würde dann eine Funktion (z.b. quitGame()) mit dem Button über die registerClickSignal() funtkion verknüpft werden.

    Ist das so sinnvoller? Wirklich einen Vorteil von den Signalen seh ich jetzt auch nur dann, wenn ich später eine Callback Funktion (die beim Knopfdruck ausgelöst werden soll) einfach zu verknüpfen. Beim selbst-auslösen des Events eher weniger.

    Sry, falls ich mich ein wenig kompliziert ausdrücke und danke für die Hilfe!