Autor Wątek: Pisanie własnego silnika (biblioteki) GUI  (Przeczytany 5877 razy)

Offline Shelim

  • Użytkownik
    • Homepage

# Grudzień 29, 2007, 18:48:54
Witam!

Mam pomysł na stworzenie serii gier tekstowych różnych gatunków (roguelike, 4X, itp). Żeby ułatwić sobie zadanie, postanowiłem napisać własny silnik do obsługi "konsoli". Cudzysłów jest bardzo potrzebny, bo silnik wcale z konsoli nie korzysta, tylko otwiera własne okienko renderujące tekst (ew. pełny ekran). Zalety rozwiązania to większy wybór kolorów, fontów, rozdzielczości i ilości znaków na cal kwadratowy monitora (nawet 150 x 70 znaków!) :D
Powstał już pełen silnik niskiego poziomu (odczyt czcionek, rysowanie w różnych kolorach, odczyt danych z bufora, obsługa myszki [odwraca kolory znaku na którym spoczywa kursor], obsługa klawiatury [sprawdzenie czy w danej chwili konkretny klawisz jest wciśnięty], itp.) - całość korzysta z biblioteki Allegro i jest całkowicie przenośna (na pewno pod Windowsem działa bez żadnych trudności i nie powinno być kłopotów z kompilacją pod Linuxem ;) ). Dorzuciłem do tego bibliotekę DyConnect do obsługi sieci i postanowiłem wziąć się za funkcje i struktury (biblioteka jest strukturalna, wiem - antyk - ale się sprawdza ;) ) wysokiego poziomu. Co ma się tutaj znaleźć? GUI.
I tutaj mam ogromny problem jak to zaprojektować pod względem podziału logicznego.
Jakbym to widział?
Podstawową jednostką GUI są okna (struktury), każde zawierające bufor tego co się na nim pierwotnie znajdowało, oraz listę (vector) okien potomnych. Nadrzędnym oknem, tworzonym od razu przy inicjalizacji trybu wysokiego poziomu byłby sam ekran. Okno może mieć ramkę pojedynczą lub podwójną, może także nie mieć jej wcale. Jeżeli ma jego ramkę to górna jej część służy za Caption i może zawierać jego nazwę. W prawym górnym rogu może się znaleźć znany z Windowsa "X". Wymiary okna podawane są w znakach (szerokość x wysokość), istnieje też funkcja która przelicza procent wymiaru na liczbę znaków (użyteczne jeżeli okno ma zabierać np. 1/3 ekranu niezależnie od rozdzielczości). Okno potomne nie może być większe od macierzystego, nie może także wchodzić na jego ramkę. W momencie stworzenia okna powstaje jego bufor znaków (rozmiarów okna) i jest on czyszczony. Do okna możemy się odwołać funkcjami rysującymi mażąc po nim ile chcemy (czyt. rysując znaki). Każde okno ma swoją pętlę komunikatów (funkcję zwrotną ustalaną przy tworzeniu okna). Istnieje kilka predefiniowanych funkcji zwrotnych dla okien konkretnego typu (np. przycisków, czy editboxów). Odbieranie i wysyłanie komunikatów w programie działa tak samo jak w WinAPI w wersji PeekMessage (a więc non-blocking). A, program jest jednowątkowy, nie licząc DyConnect'a.
Teraz input od usera - zakładam że zawsze jedno okno ma focusa (przy czym jeżeli ma je okno potomne, to można założyć że nadrzędne ma "pół-focusa"). Okna są zapisane w kolejności ich rysowania, dlatego można odwołać się do spodniego okna zmieniając ich kolejność. Każde kliknięcie myszką w obrębie jednego okna posyła informacje w komunikacie. Kliknięcie poza ekranem każe przeszukać okna aby zobaczyć czy zmienił się focus. Cały input z klawiatury wędruje do okna, z wyjątkiem klawisza tab (ale można ten feature wyłączyć), który zmienia aktywne okno w obrębie jednego okna-rodzica. Problemem była początkowo optymalizacja rysowania: Załóżmy że rysujemy okna wg ich kolejności (tzn. najpierw ekran i jego bufor, potem każde z okien potomnych i potomnych jego potomnych [drzewo, przechodzenie preorder] ). Problem zaczyna się, gdy coś zmienimy w którymkolwiek z okien - skąd wiedzieć które fragmenty odrysowywać? - Powstałaby konieczność rysowania wszystkiego od nowa. Rozwiązaniem byłoby zapisanie kolejnego bufora aktywnego ekranu - dla każdej litery na ekranie jest zapisane do jakiego okna należy. Jeżeli rysujemy coś w pozycji 5 x 7 i należy to do okna 2, a my wiemy że w tej pozycji mamy okno 4, to ekran nie musi być odświeżany.

Tak, inspirowałem się mocno WinAPI ;)

Pytanie - czy to co mówię jest sensowne, czy bredzę jak pijany? Jak byście to rozwiązali? ;)

PS> Planuje wypuszczenie mojej biblioteki jako Open-Source, kiedy już mi się ją uda dopracować. Może ułatwi ona paru osobom pisanie tekstowych roguelike'ów ;D

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline Moriturius

  • Użytkownik

# Grudzień 29, 2007, 19:19:39
Hmm prawdę mówiąc, to zrozumiałem połowę albo i mniej z całego posta ;)

Wzorowanie się na WinAPI jest raczej średnim pomysłem. Ja właśnie całkiem niedawno zajmowałem się tworzeniem własnego GUI i mam to zorganizowane tak, że mam sobie klasę wirtualną (nie całkowicie) guiControl. Po niej dziedziczą wszystkie kontrolki łącznie z oknami. Każda kontrolka może przechowywać wiele dzieci i być dzieckiem innej kontrolki.

Dzięki takiemu zorganizowaniu po kliknięciu myszką wystarczy wysłać event do aktywnego okna, a on wysyła do każdej kontrolki potomnej dopóki któraś nie zwróci mu true co oznacza, że ona (albo jedno z jej dzieci) obsłużyła komunikat. Podczas renderowania oczywiście też renderowane są wszystkie dzieci.

Jeśli chodzi o reakcje na działania to użyłem FastDelegate. Każda kontrolka ma mapę (std::map) własnych komunikatów i odpowiadających im delegatów. Kiedy wywołany jest komunikat CM_CLICK (np w przycisku) to dany obiekt sprawdza swoją mapę i wywołuje odpowiednią funkcję (jeśli jakaś została przypisana).

Offline Xion

  • Redaktor
    • xion.log

# Grudzień 29, 2007, 19:19:46
Przede wszystkim odniosę się do tego:
Cytuj
Tak, inspirowałem się mocno WinAPI Wink
Cóż, widać :) Bierzesz sporo nienajlepszych elementów, a mianowicie:

Cytuj
Każde okno ma swoją pętlę komunikatów (funkcję zwrotną ustalaną przy tworzeniu okna)
A po co, ja się pytam? Okno powinno mieć delegatów do podpięcia funkcji obsługujących zdarzenia (OnClick, OnKeyDown, itp.), a rozdzielaniem zdarzeń do poszczególnych okien powinien się zajmować jakiś obiekt nadrzędny (System, Manager, FatherDirector ;D). Będzie on przerabiał zdarzenia dochodzące z systemu operacyjnego (czy tam biblioteki) do aplikacji na zdarzenia GUI.

Cytuj
Problem zaczyna się, gdy coś zmienimy w którymkolwiek z okien - skąd wiedzieć które fragmenty odrysowywać? - Powstałaby konieczność rysowania wszystkiego od nowa
W grze przecież i tak wszystko rysujesz od nowa, więc zupełnie problemu nie widzę :)

Projektując GUI faktycznie dobrze jest się oprzeć na istniejącej bibliotece, ale - jak mówiłem - WinAPI nie jest dobrym wyborem. Polecam raczej przyjrzeć się Windows Forms czy VCL. U ciebie zadanie jest nieco uproszczone, bo okienka są raczej wielkimi textboxami wielowierszowymi i odpada sprawa z zarządzaniem pojedynczymi kontrolkami i przekazywaniem do nich zdarzeń.
Nie wiem na ile ci się to przyda, ale rzucam dla spokoju sumienia kilkoma notkami nt. systemu GUI, które kiedyś popełniłem, gdy pisałem własny (klasyczny, nie taki konsolowy): http://xion.org.pl/?s=%22system+GUI%22 .


@Moriturius:
Cytuj
Dzięki takiemu zorganizowaniu po kliknięciu myszką wystarczy wysłać event do aktywnego okna, a on wysyła do każdej kontrolki potomnej dopóki któraś nie zwróci mu true co oznacza, że ona (albo jedno z jej dzieci) obsłużyła komunikat. Podczas renderowania oczywiście też renderowane są wszystkie dzieci.
Nie polecam, bo rozprasza to kod obsługi zdarzeń w bardzo wielu miejscach i czyni go trudniejszym do zrozumienia. Zapewnienie, by dane zdarzenie trafiło do odpowiedniej kontrolki może być z powodzeniem realizowane na najwyższym poziomie drzewka kontrolek. Poza tym takie propagowanie wszystkich zdarzeń "wgłąb" jest chyba średnio efektywne :)

Cytuj
Jeśli chodzi o reakcje na działania to użyłem FastDelegate. Każda kontrolka ma mapę (std::map) własnych komunikatów i odpowiadających im delegatów. Kiedy wywołany jest komunikat CM_CLICK (np w przycisku) to dany obiekt sprawdza swoją mapę i wywołuje odpowiednią funkcję (jeśli jakaś została przypisana).
I kto tu się nie wzoruje na WinAPI ;]
« Ostatnia zmiana: Grudzień 29, 2007, 19:28:30 wysłana przez Xion »

Offline Kos

  • Użytkownik
    • kos.gd

# Grudzień 29, 2007, 19:29:00
Dwa warianty ode mnie:
1. Odrysować cały obszar okna :) Każde okno ma swój własny bufor, zgadza się? Więc funkcja rysująca okno imo wygląda mniej więcej tak - tworzysz nowy bufor takich samych rozmiarów, co bazowe okno, kopiujesz do niego 'treść' okna, a następnie rekurencyjnie wywołujesz takie właśnie funkcje rysujące dla wszystkich okien potomnych wskazując im ten właśnie bufor oraz potrzebne offsety. Okna potomne dorysowują się na tym buforze. W tym momencie masz bufor wielkości bazowego okna, a w nim treść tego okna, zasłoniętą tam gdzie trzeba :) Pozostaje skopiować treść tego bufora w odpowiednie miejsce na ekran.
2. Jeśli chcesz optymalniej, można testując wymiary okien wyznaczyć dokładnie obszar, który musi być wyrysowany. Jak taki obszar opisać? Potrzebny będzie wektor lub lista prostokątów, zwykłych structów {int, int, int, int}. Wstawiasz do niej wymiary okna podstawowego. Później wywołujesz dla wszystkich okien potomnych (też można rekurencyjnie po drzewku), by odjęły od tej listy swoje rozmiary.
Jak? Potrzebna będzie funkcja, która dla dwóch prostokątów (4x int) zwróci ich różnicę, czyli od jednego prostokąta (w wypadku gdy się nie pokrywają) do czterech mniejszych (nie pokrywających się) prostokątów, których suma równa jest różnicy pierwszego prostokąta i drugiego. (trochę gmatwam, wiem :P mogę Ci rozrysować jeśli będzie taka potrzeba). Ta funkcja powinna wywołać się dla każdego okna potomnego.
Po tym wszystkim, gdy funkcja wykona się dla każdego okna potomnego naszego okna bazowego, w liście będziesz miał szereg prostokątów, które w sumie opisują obszar faktycznie widoczny. Kolejno kopiujesz dla każdego "podprostokąta" fragment bufora okna na ekran, i to wszystko :)
Wtedy nie będziesz odrysowywał całego obszaru wyznaczonego przez okno, które się zmienia, a tylko te jego fragmenty, które nie są zasłonięte przez okna pochodne.

Offline Shelim

  • Użytkownik
    • Homepage

# Grudzień 29, 2007, 21:32:48
Właśnie odwoływania sie do OnClick'ów wolałbym uniknąć;
Jeżeli mamy np. predefiniowaną kontrolkę EditBox, to można od niej uzyskać zawartość przez odpytanie jej komunikatem. Poza tym rozwiązanie bazujące na funkcjach przetwarzających komunikaty pozwoliłoby na bardzo łatwą rozbudowywalność (stosowałem podobne rozwiązanie w PHP i było jak marzenie, zwłaszcza kiedy chciałem dodać nowy element ;) )
Xion, dzięki za artykuły, poczytałem sobie. Jakoś nie przyszło mi do głowy sprawdzić twojego bloga, robiłem tylko search po Googlach i natrafiłem na tonę artykułów o programowaniu w konkretnych GUI  :P
No dobra, zbrojny w nową wiedzę, zabieram się za tworzenie :)

Offline Moriturius

  • Użytkownik

# Grudzień 30, 2007, 11:25:04
Cytuj
Dzięki takiemu zorganizowaniu po kliknięciu myszką wystarczy wysłać event do aktywnego okna, a on wysyła do każdej kontrolki potomnej dopóki któraś nie zwróci mu true co oznacza, że ona (albo jedno z jej dzieci) obsłużyła komunikat. Podczas renderowania oczywiście też renderowane są wszystkie dzieci.
Nie polecam, bo rozprasza to kod obsługi zdarzeń w bardzo wielu miejscach i czyni go trudniejszym do zrozumienia. Zapewnienie, by dane zdarzenie trafiło do odpowiedniej kontrolki może być z powodzeniem realizowane na najwyższym poziomie drzewka kontrolek. Poza tym takie propagowanie wszystkich zdarzeń "wgłąb" jest chyba średnio efektywne :)

Co masz na myśli mówiąc o "realizowaniu na najwyższym poziomie drzewa kontrolek" ? ^^

Cytuj
Jeśli chodzi o reakcje na działania to użyłem FastDelegate. Każda kontrolka ma mapę (std::map) własnych komunikatów i odpowiadających im delegatów. Kiedy wywołany jest komunikat CM_CLICK (np w przycisku) to dany obiekt sprawdza swoją mapę i wywołuje odpowiednią funkcję (jeśli jakaś została przypisana).
I kto tu się nie wzoruje na WinAPI ;]
[/quote]

Samo uzycie delegatów to już technologia daleko wyprzedzająca WinAPI ;) Poza tym nie mogę się domyslić gdzie w tym rozwiązaniu dopatrzyłeś się jakiejś analogi to WinAPI ?

Offline Xion

  • Redaktor
    • xion.log

# Grudzień 30, 2007, 12:04:52
Cytuj
Co masz na myśli mówiąc o "realizowaniu na najwyższym poziomie drzewa kontrolek" ? ^^
Ano to, że wpierw ustalamy kontrolkę, do której ma trafić zdarzenie (czyli np. kontrolkę z focusem albo wykonujemy hit-test dla zdarzeń myszy), a potem je do niej wysyłamy. U ciebie najpierw wysyłamy zdarzenie do kontrolek nadrzędnych; te albo propagują je dalej - do wszystkich potomnych, albo ignorują. I tak zdarzenie "rozłazi się" po całym drzewie.

Cytuj
Samo uzycie delegatów to już technologia daleko wyprzedzająca WinAPI Wink Poza tym nie mogę się domyslić gdzie w tym rozwiązaniu dopatrzyłeś się jakiejś analogi to WinAPI ?
W przekazywaniu komunikatów oczywiście, zamiast wywołań typu control->OnClick() :)

Offline Mic

  • Użytkownik

# Grudzień 30, 2007, 17:27:44
Jakiś czas temu, trochę bawiłem się przeróżnymi bibliotekami gui pod linuksa - nie wszystkim zbyt dokładnie, ale wydaje mi się, że zanim napiszesz własną bibliotekę warto im się przyjżeć. W WinAPI nigdy nie kodziłem, jednakże nie słyszałem zbyt pochlebnych opinii na temat tworzenia w nim graficznego interfesu użytkownika. Niemniej jednak nie mam także porównania. W każdym bądź razie szczególnie polecam zapoznać się z GTK - kodzi się w nim bardzo wygodnie i szybko.
Ponadto masz dostęp do kodu źródłowego, można się wiele z niego nauczyć ( chociaż kod jest strukturalny co nie każdemu odpowiada, po za tym nie jest to mały projekt).
Inną biblioteką, którą się zainteresowałem było FLTK, i tutaj jego największą zaletą jest jego prostota. Nawet początkujący koder jest w stanie wgryźć się w jego kod źródłowy( a zaawansowany bardzo szybko) i dlatego polecam spędzić chociaż jeden wieczór nad nim, a napewno wiele ci się rozjaśni.

Oczywiście warto implementować własne pomysły, jednak pare rzutów oka na kod tych bibliotek z pewnością pozwoli ci uniknąć wiele błędów.


A właśnie komunikaty, tudzież nazywane sygnałami - jak najlepiej je połączyć z funkcją callbackową ( tzn., np. sygnał myszki LButtonClicked  z funckcją OnButtonClicked).
rozwiązanie pierwsze to użycie biblioteki w rodzaju sigc, a drugie to wskaźnik na odpowiednią funkcję dla każdego sygnału, czy istnieją jakieś inne ciekawe rozwiązania?

//edit
Cytuj
Cytuj
Co masz na myśli mówiąc o "realizowaniu na najwyższym poziomie drzewa kontrolek" ? ^^
Ano to, że wpierw ustalamy kontrolkę, do której ma trafić zdarzenie (czyli np. kontrolkę z focusem albo wykonujemy hit-test dla zdarzeń myszy), a potem je do niej wysyłamy. U ciebie najpierw wysyłamy zdarzenie do kontrolek nadrzędnych; te albo propagują je dalej - do wszystkich potomnych, albo ignorują. I tak zdarzenie "rozłazi się" po całym drzewie.

A to akurat fajnei widać w Irrlichtu (btw. też można na niego spojrzeć :) )

Cytuj
Okno potomne nie może być większe od macierzystego, nie może także wchodzić na jego ramkę
Tutaj chyba bym się nie zgodził, dlaczego miałoby nie być większe? co innego jak tworzysz okno dialogowe, natomiast w innym przypadku chyba niekoniecznie.
« Ostatnia zmiana: Grudzień 30, 2007, 17:32:38 wysłana przez Mic »

bies

  • Gość
# Grudzień 30, 2007, 17:29:25
Drobna uwaga, kod gtk jest pisany co prawda w strukturalnym języku (C) ale jest do bólu obiektowy.

Offline Mic

  • Użytkownik

# Grudzień 30, 2007, 17:33:27
Cytuj
Drobna uwaga, kod gtk jest pisany co prawda w strukturalnym języku (C) ale jest do bólu obiektowy.
ah co prawda to prawda

Offline Moriturius

  • Użytkownik

# Grudzień 30, 2007, 17:54:34
Cytuj
Co masz na myśli mówiąc o "realizowaniu na najwyższym poziomie drzewa kontrolek" ? ^^
Ano to, że wpierw ustalamy kontrolkę, do której ma trafić zdarzenie (czyli np. kontrolkę z focusem albo wykonujemy hit-test dla zdarzeń myszy), a potem je do niej wysyłamy. U ciebie najpierw wysyłamy zdarzenie do kontrolek nadrzędnych; te albo propagują je dalej - do wszystkich potomnych, albo ignorują. I tak zdarzenie "rozłazi się" po całym drzewie.

Ale nie uważam tego rozłażenia się ich za jakiś wielki problem i tak przekazuję tylko wskaźnik na wypełniony event ^^
Dla kontrolki guiEditBox u mnie zdarzenia klawiatury są wysyłane bezpośrednio do niej.

Cytuj
Samo uzycie delegatów to już technologia daleko wyprzedzająca WinAPI Wink Poza tym nie mogę się domyslić gdzie w tym rozwiązaniu dopatrzyłeś się jakiejś analogi to WinAPI ?
W przekazywaniu komunikatów oczywiście, zamiast wywołań typu control->OnClick() :)

U mnie nie ma takich metod w ogóle ;)
Jest tylko sendMessage(), które wygląda tak:
Kod: (cpp) [Zaznacz]
void guiControl::sendMessage( int message )
{
    (delegateMap[ message ])();
}

A każda kontrolka sama wywołuje sendMessage przy odpowietnim zdarzeniu więc nie trzeba sobie tym w ogóle zaprzątać głowy.

Offline Humman

  • Użytkownik

# Grudzień 30, 2007, 18:10:44
API windowsa to niezbyt wdzięczny wzór do naśladowania głównie że jest stworzone w nieobiektowym jezyku C i to w taki sposób że nie da się odwołać bezpośrednio do struktury opisującej kontrolkę a jedynie przez wysłanie w petli komunikatów informacji o instancji tej kontrolki i rzeczy co z nią zrobić. Tylko że nie zawsze to działa a żeby stworzyć nową kontrolkę trzeba cudować z superclassingiem zamiast jak w każdym przyzwoitym języku obiektowym stworzyć klasę dziedziczącą po podstawowej kontrolce.
Lepiej już wzoruj się na GUI z Irllicht-a, jest tam cały kod dostępny i opisany.

Ja w swoim GUI komunikaty rozwiązałem jak i całą resztę za pomocą drzewa, menadżer gui sprawdza nad którą  kontrolką jest kursor przez rekurencje po kolejnych poziomach drzewa i dla wybranej kontrolki sprwdza czy zaszło zdarzenie jak np. kliknięcie LPM. Jeśli tak kontrolka wysyła komunikat do swego rodzica z jej wskaźnikiem i typem zdarzenia, rodzic albo przepuszcza go do swojego rodzica itd. aż do korzenia lub reaguje na niego i zastępuje go własnym. W ten sposób wystarczy napisać obsługę przycisku zamykania w klasie okna a nie bawić się tym na poziomie całego gui.

Cytuj
Okno potomne nie może być większe od macierzystego, nie może także wchodzić na jego ramkę
Jakby całe je zasłaniało to niedostępne by były zdarzenia i focus z okna macierzystego ale chodziłao by normalnie.

Offline Esidar

  • Użytkownik

# Grudzień 30, 2007, 18:43:15
Jest tylko sendMessage(), które wygląda tak:
Kod: (cpp) [Zaznacz]
void guiControl::sendMessage( int message )
{
    (delegateMap[ message ])();
}

A co się stanie jak wywołam control->sendMessage( 123987984 ); ? :)

Cytuj
Samo uzycie delegatów to już technologia daleko wyprzedzająca WinAPI

Powyżej dałeś przykład przekazywania komunikatu do kontrolki. A jak masz zrobione w drugą stronę ? Czyli jak kontrolka przysyła do aplikacji komunikat np. "OnItemSelected" ?

Offline Xion

  • Redaktor
    • xion.log

# Grudzień 30, 2007, 19:12:35
Esidar: Jak sądzę, ta mapa może zawierać delegatów ustalonych przez aplikację, więc w ten sposób może one reagować na zdarzenia.

Offline Moriturius

  • Użytkownik

# Styczeń 02, 2008, 10:20:12
Jest tylko sendMessage(), które wygląda tak:
Kod: (cpp) [Zaznacz]
void guiControl::sendMessage( int message )
{
    (delegateMap[ message ])();
}

A co się stanie jak wywołam control->sendMessage( 123987984 ); ? :)

Nic. Mapa stworzy sobie pusty delegat. Potem zostanie wywołany operator (), ale skoro delegat jest pusty to zupełnie nic się nie stanie.
Tak samo jest jak wysyła się do kontrolki message której ona nie obsługuje (czyli jeśli nie połączę zdarzenia kliknięcia z jakąś metodą lub funkcja).

Cytuj
Samo uzycie delegatów to już technologia daleko wyprzedzająca WinAPI

Powyżej dałeś przykład przekazywania komunikatu do kontrolki. A jak masz zrobione w drugą stronę ? Czyli jak kontrolka przysyła do aplikacji komunikat np. "OnItemSelected" ?

Nie przesyła. Wywołuje operator () dla odpowiedniego delegata, a więc wywołuje po prostu jakąś funkcję. Jaką? To ustala się przez wywołanie
control->connect( /*...*/ );
które ma za zadanie połączyć numer zdarzenia z delegatem na funkcję.