Abhandeln bestimmter Ereignisse (Designfrage)



  • Vorweg: ich bin der Meinung, dass ich im richtigen Unterforum bin, da es hier nicht speziell um die WinAPI geht.

    Ich bin momentan dabei, mit einem Kumpel ein kleines Spiel zu programmieren. Das soll mit C++ und der WinAPI geschehen. Nun gibt es ja in jedem C++-Programm eine main (bei WinAPI eben die WinMain, aber das ist völlig irrelevant). Nun gibt es bei Windows auch die sogenannte WndProc (Window procedure), die von Windows aufgerufen wird und der bestimmte Parameter des aufgetretenen Events übergeben werden. Diese Events können Mausklicks, Tastendrücke oder sonst was sein.

    Nun gibt es in dem Spiel zum einen ein paar Menüs, über die man Dinge konfigurieren kann, und zum anderen das eigentliche Spiel, in dem man die Spielfigur bewegen kann usw.

    Das Menü besteht aus Buttons, von denen jeder eine ID (z. B. XYZ) hat. Bei Klick wird die WndProc z. B. mit der Nachricht "Button XYZ wurde geklickt" aufgerufen. Das wird dann per switch/case abgefragt und entsprechend darauf reagiert.

    Nun mein Problem: Die ganze Logik, sowohl für Menü als auch für das eigentliche Spiel, befinden sich momentan in der WndProc. Ich möchte aber gerne abgrenzen, sodass diese beiden Bereiche voneinander abgegrenzt sind. Jetzt war meine Idee, dass ich mir jeweils eine Klasse schreibe (Menu und Game), die jeweils eine Methode "WndProc" implementieren. Je nachdem in welchem Spielstatus ich mich befinde, wird die WndProc des entsprechenden Objektes (Menu oder Game) aufgerufen. Allerdings finde ich dabei blöd, dass die Klassen dann jeweils nur genau eine Aufgabe erledigen (entweder die Menü- oder die Spiellogik abhandeln). Ich dachte mal gelesen zu haben, dass man das nicht so machen soll, sondern dass Klassen durch die Daten, die man ihnen gibt, ihren Zweck erfüllen. Meine Klassen hätten aber jeweils nur genau eine festgelegte Aufgabe.

    Es geht hier lediglich um die Frage, ob die beschriebene Lösung designtechnisch gut ist und das der gängige Weg ist, oder ob man das eleganter lösen kann.

    Ich hoffe, dass ihr mich verstanden habt und freue mich auf eure Antworten!



  • idR macht man es so, dass du Funktionen mit Nachrichten verknuepfst. Das nennt sich dann signal/slot Konzept.

    Du hast wahrscheinlich eine Klasse die Fenster repraesentiert. Wenn ich nun ein Fenster erstelle registriere ich beim Fensterobjekt einfach Nachrichten und Callbackfunktionen:

    void menu_clicked(WPARAM wParam, LPARAM lParam) {
       if(lParam!=ID_MENU1) return;
       ...
    }
    
    Window wnd;
    wnd.registerMessage(WM_COMMAND, menu_clicked);
    

    In der WndProc wird dann die eben geschaut wer alles auf welche Message "listened" Deshalb nennt sich das Konzept auch oft "Listener" oder "Observer" bzw. "Subscriber"

    Das Code Beispiel ist natuerlich absolut minimal und du solltest besser abstrahieren. Aber ich hoffe du hast erstmal genug Stoff zum googlen 🙂



  • Dann hätte ich aber auch wieder das Problem, dass das Zeug nicht voneinander abgetrennt ist. Ich hätte dann für bestimmte/alle Messages eine eigene Funktion, aber nicht pro "Zustand" (Menü, Spiel).

    Du meinst es wohl so, dass man zwei Funktionen hat: eine menu_clicked und eine game_clicked (nur als Beispiel). Nur finde ich die Abfrage am Anfang der Funktion dann auch unschön. Beispielsweise würde ich dann wohl sowas haben:

    void game_clicked()
    {
        if (state != STATE_GAME)
        {
            return;
        }
    
        // Messages abhandeln
    }
    
    void menu_clicked()
    {
        if (state != STATE_MENU)
        {
            return;
        }
    
        // Messages abhandeln
    }
    

    Da finde ich das mit den Klassen aus meinem ersten Beitrag eleganter, da dann die Zuständigkeiten klar sind und bereits innerhalb der WndProc (in der Haupt-Klasse) abgefragt wird, in welchem Status das Spiel sich befindet und anhand dieser Information die Methode des richtigen Objektes aufruft. Was meinst du?



  • Nachdenker schrieb:

    Dann hätte ich aber auch wieder das Problem, dass das Zeug nicht voneinander abgetrennt ist. Ich hätte dann für bestimmte/alle Messages eine eigene Funktion, aber nicht pro "Zustand" (Menü, Spiel).

    Das ist nur eine Frage der Abstraktion. uU hilft dir hier das State Design Pattern.

    Du meinst es wohl so, dass man zwei Funktionen hat: eine menu_clicked und eine game_clicked (nur als Beispiel). Nur finde ich die Abfrage am Anfang der Funktion dann auch unschön. Beispielsweise würde ich dann wohl sowas haben:

    Google mal nach den Stichworten die ich dir gegeben habe. Mein Beispiel war minimalst. Normalerweise registrierst du das clicked Event nicht beim Fenster, sondern beim Knopf den du klicken willst.

    Da finde ich das mit den Klassen aus meinem ersten Beitrag eleganter, da dann die Zuständigkeiten klar sind und bereits innerhalb der WndProc (in der Haupt-Klasse) abgefragt wird, in welchem Status das Spiel sich befindet und anhand dieser Information die Methode des richtigen Objektes aufruft. Was meinst du?

    Du brauchst viel viel viel mehr Abstraktion. Die WndProc sollte nur deligieren und sonst nichts machen. Und was sie wohin deligiert wird per signal/slot Prinzip konfiguriert.

    Schau dir dazu einfach ein beliebiges GUI Toolkit an zB FLTK.



  • falls ich das noch richtig auf die kette bekomme ist es ja so das die wndproc - funktionen eigentlich nur events filtern und irgendwelche "aktionen" auslösen sollen.
    falls das stimmt könntest du auch eine schicht zwischen die "winapi - ebene" und die spiellogik schieben die nichts anderes macht als daten anzunehmen und diese durchzureichen. das ganze nennt sich dann "facade - pattern". wenn du dann iwann von winapi auf sagen wir mal qt oder dx oder sonstwas wechselt bleiben die aufrufe in der fasade die gleichen. nur die stellen, von wo sie aufgerufen werden sind dann eben andere. der "trick" hierbei ist der, das du "komplexere" abläufe in deiner logik hinter einem aufruf in der fasade "verstecken" kannst.
    ob sich das ganze dann aber in schlechterer performance auswirkt kann ich nicht sagen.



  • Danke für die Infos, ich denke damit kann ich erst mal was aufbauen und sehen ob es sich bewährt. Dass die WndProc die Ereignisse nur weiterleiten soll, habe ich verstanden und es macht für mich auch Sinn. Das Signal/Slot-Zeugs sieht auf den ersten Blick auch ganz vernünftig aus.

    Das mit dem facade-Pattern werde ich mir bei Gelegenheit auch mal ansehen - danke dafür!



  • Ich hab mir ein paar Gedanken dazu gemacht und das Ganze dann in Code umgesetzt.

    Das Prinzip ist so:

    Die Klasse Window beinhaltet eine statische Funktion, die WncProc. In dieser WndProc werden die Nachrichten entsprechend an Funktionen weitergeleutet, die vorher registriert wurden. Die Methode zum Registrieren sieht so aus:

    void Component::registerSignal(UINT message, Signal signal)
    {
    	this->signals[message].push_back(signal);
    }
    

    Wobei this->signals das hier ist:

    std::map<UINT, std::vector<Signal>> signals;
    

    und Signal das:

    typedef void (*Signal)(WPARAM wParam, LPARAM lParam);
    

    Die WndProc sieht dann so aus:

    LRESULT CALLBACK Window::WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
    	if (Window::windows.find(hWnd) == Window::windows.end())
    	{
    		Window::windows[hWnd] = Window::current;
    	}
    
    	Window *wnd = Window::windows[hWnd];
    
    	// Funktionen fuer die Windows-Nachrichten aufrufen
    	if (wnd->signals.find(message) != wnd->signals.end())
    	{
    		auto it = wnd->signals[message].begin();
    
    		std::for_each(it, wnd->signals[message].end(), [wParam, lParam](const Signal &signal)
    		{
    			(*signal)(wParam, lParam);
    		});
    	}
    
    	return DefWindowProc(hWnd, message, wParam, lParam);
    }
    

    So weit, so gut. Nun kann man sich über eine Window-Instanz für jede Windows-Nachricht beliebig viele Funktionen registrieren, die dann entsprechend aufgerufen werden.

    Jetzt gibt es noch die Klasse Button. Diese kann entweder mit einem Window als Parent erstellt werden oder auch ohne. Nimmt man den Konstruktor, der den Button mit einem Parent-Window erstellt, registriert sich der Button einen Handler für WM_COMMAND (die Nachricht, die bei Button-Klick gesendet wird):

    Button::Button(Window &parentWindow, int id, int top, int left, int width, int height, const std::string &caption)
    	: Component(parentWindow.getHandle(), parentWindow.getParentInstance(), id, top, left, width, height, caption, "Button")
    	, parentWindow(&parentWindow)
    {
    	if (!Button::wmCommandHandlerAdded[parentWindow.getHandle()])
    	{
    		parentWindow.registerSignal(WM_COMMAND, Button::wmCommandHandler);
    		Button::wmCommandHandlerAdded[parentWindow.getHandle()] = true;
    	}
    
    	this->initButton(id);
    }
    

    Über eine Methode "registerClickHandler" des Buttons kann man dann wiederum Handler registrieren, die beim Button-Klick aufgerufen werden:

    void Button::registerClickHandler(ClickHandler clickHandler)
    {
    	this->clickHandlers.push_back(clickHandler);
    }
    

    Die clickHandler werden innerhalb der riseClickEvent() aufgerufen (siehe unten).

    Zum Schluss noch der wmCommandHandler des Buttons:

    void Button::wmCommandHandler(WPARAM wParam, LPARAM lParam)
    {
    	int id = LOWORD(wParam);
    
    	if (Button::buttons.find(id) != Button::buttons.end())
    	{
    		Button::buttons[id]->riseClickEvent(wParam, lParam);
    	}
    }
    

    Nun meine Frage: geht das völlig an dem vorbei, wie man es richtig macht oder bin ich auf nem guten Weg?



  • Hat keiner irgendwelche Anregungen?



  • Mach ich was falsch und ihr schreibt deswegen absichtlich nix? 😕 😃



  • Ne, das passt schon.
    Schoenheitspreis gewinnt es keinen, aber sieht OK aus.



  • Shade Of Mine schrieb:

    Ne, das passt schon.
    Schoenheitspreis gewinnt es keinen, aber sieht OK aus.

    Was meinst du denn mit Schönheitspreis?


Anmelden zum Antworten