Graficzny interfejs może (i powinien) ładnie wyglądać, ale jego najważniejszą cechą jest oczywiście interaktywność. GUI musi przede wszystkim umożliwiać reakcję na poczynania użytkownika, a to jest możliwe przez odpowiednią obsługę zdarzeń.
Informację o tym, że kliknięto gdzieś myszką lub wciśnięto klawisz, pozyskać jest oczywiście bardzo łatwo. W Windows wystarczy w tym celu obsługiwać komunikaty systemowe przychodzące do okna. O wiele większym wyzwaniem jest przetłumaczenie informacji “kliknięto w punkt (458,89) obszaru klienta” na “kliknięto w obrębie kontrolki TextBox o nazwie Text1
“. Komunikat należy bowiem przekazać do odpowiedniej kontrolki (-ek) – tej, której on dotyczy.
Jest pewnie wiele sposób na zrealizowanie takiego przekazywania, jednak najbardziej sensowne wydają mi się dwa. Pierwszym z nich jest propagacja zdarzenia na kolejne poziomy drzewa systemu GUI. Kliknięcie jest więc przekazywane do okna i od tej pory cała odpowiedzialność za jego obsłużenie spada na to właśnie okno. Ono sprawdza, czy myszka trafiła w którąś z kontrolek potomnych; jeśli tak, to znów zdarzenie jest przekazywane właśnie do tej kontrolki i okno się już nim nie zajmuje. Proces ten przebiega aż dojdziemy do najniższego możliwego poziomu, czyli kontrolki nie zawierającej już żadnych innych. Tam następuje właściwa obsługa zdarzenia.
Drugi sposób zakłada, że zdarzenie nie będzie “rozłazić” się po całym drzewie, tylko od razu trafiać do tej właściwej, docelowej kontrolki. Naturalnie nie zawsze da się tak zrobić. W przypadku wciśnięć klawiszy możemy pamiętać, która kontrolka ma tzw. fokus i do niej kierować komunikaty. Natomiast obsługa myszki wymaga wysłania swego rodzaju sondy wgłąb drzewa kontrolek w celu znalezienie tej najmniejszej, w którą trafił kursor (operacja ta jest znana jako hit test).
Generalnie zdarzenia z punktu widzenia ich obsługi można podzielić na dwie grupy: na zdarzenia myszki i na… wszystkie inne :) To właśnie te pierwsze przysparzają najwięcej kłopotów. Nie tylko wymagają rekurencyjnego przeszukiwania drzewa kontrolek, ale też mogą mieć globalne konsekwencje, jak np. zmiana fokusu. Zajmowanie się tymi konsekwencjami jest łatwiejsze, gdy wszystko odbywa się “na górze” – na poziomie głównych klas systemu GUI, a nie pojedynczych kontrolek. Jest to jeden z powodów przemawiających za wyborem drugiego sposobu przekazywania informacji o zdarzeniach.
Konstrukcje iteracyjne w C++ mają całkiem spore możliwości. Dotyczy to zwłaszcza instrukcji for
, która jest o wiele elastyczniejsza niż w innych językach. Mimo to w dziedzinie pętli C++ ma pewne niedostatki.
Zwykle najbardziej brakuje nam sposobu na przerwanie zagnieżdżonych pętli. W C# czy Javie jest to możliwe za pomocą specjalnych wariantów instrukcji break
. Lecz w C++ trzeba to robić albo z wykorzystaniem wyklętej (niezbyt zresztą słusznie) instrukcji goto
, albo poprzez zastosowanie zmiennych logicznych. Istnieje też sposób z wykorzystaniem wyjątków, ale można go traktować chyba tylko jako ciekawostkę.
Jest jeszcze jedno usprawnienie mechanizmu pętli, którego C++ nie posiada, ale dla odmiany ten brak nie jest aż tak dolegliwy. Chodzi tu o klauzulę else
występującą po pętli. Tę nietypową konstrukcję widziałem jak dotąd tylko w języku Python; wygląda ona następująco:
Fraza else
odnosi się tutaj do pętli, a nie do instrukcji if
. Jest ona wykonywana wtedy, gdy pętla kończy się normalnie, tzn. nie następuje wykonanie instrukcji break
. Jak widać oznacza to zwykle, że jakiś rodzaj przeszukiwania dotarł do końca wyznaczonego zakresu bez znalezienia pasującego elementu (w tym przypadku – dzielnika).
Większość języków nie posiada tej dziwnej konstrukcji, bo najczęściej można się bez niej obejść. Przeszukiwanie dokonywane w pętli jest bowiem często zamykane w funkcję, którą można zapisać bez użycia break
:
bool IsPrime(unsigned x)
{
for (unsigned i = 2; i < x; ++i)
if (x % i == 0) return false;
return true;
}[/cpp]
Pewne “nowoczesne” konstrukcje w nowszych językach programowania nie są więc zawsze lekarstwem na programistyczne bolączki. Czasami są zwykłym zawracaniem głowy.
Kiedy komukolwiek pokazuję fragment napisanego przez siebie kodu, reakcja jest zawsze dość podobna. Można ją streścić jako pytanie: “A dlaczego tu tak zielono?” :)
Faktycznie, dość intensywnie używam komentarzy. Rzeczywiście, nadużywam też stosowania przerw w postaci pustych wierszy. Zgadza się, że dwa wiersze “normalnego” kodu odpowiadają przeciętnie trzem wierszom napisanym przez mnie. Tak, to wszystko prawda. Co mam na swoje usprawiedliwienie?
Otóż… nic :P Wręcz przeciwnie, taki sposób pisania uważam za bardzo pożyteczny, a powodują mną takie oto motywy:
/********/
czy nagłówków opisujących pliki na pewno polepsza czytelność kodu.function1
, a klawisza Tab używa tylko w połączeniu z Altem :) Szkoda, że tego rodzaju kod widzi się stanowczo za często ;PTak więc, Wysoki Sądzie, z powodu swojego rozwlekłego stylu kodowania nie czuję najmniejszych wyrzutów sumienia i nie postanawiam nawet minimalnej poprawy ;P Co więcej, nie będę się krępował przed propagowaniem swoich poglądów na ten temat każdemu, kto będzie miał (nie)przyjemność oglądać napisany przeze mnie kod :)
Projektując klasy i kontrolki systemu GUI, wzoruję się na kilku istniejących rozwiązaniach. Większość wzorców czerpię jednak z Windows Forms, części platformy .NET. Tym, co ostatnio zwróciło w niej moją uwagę, to fakt, że wiele czynności można w niej wykonać na kilka sposób. Mówiąc ściślej, klasy wchodzące w skład WF posiadają często więcej niż jedną składową służącą osiągnięciu tego samego celu.
Przykład? Proszę bardzo. Do zmiany położenia i wymiarów kontrolki wystarczą dokładnie cztery właściwości. Mogą to być: Left (pozycja lewej krawędzi kontrolki), Top (górnej krawędzi), Width i Height. Tyle w zupełności wystarczy. Klasa Windows.Forms.Control ma też jednak inne:
Wszystkie je można oczywiście uzyskać z tych czterech, które wymieniłem na początku. Istnieje też rzecz jasna mnóstwo innych kombinacji z innymi właściwościami “pierwotnymi” i “pochodnymi”.
Można mieć jednak wątpliwości, czy istnienie różnych dróg dotarcia do podobnych danych jest konieczne i właściwe. Przy tak bogatym interfejsie (inny przykład: metoda Graphics.DrawImage() ma aż 30 (!) przeciążonych wersji) można bardzo długo zastanawiać się nad tym, którego sposobu mamy użyć w konkretnym przypadku.
To prawda, że interfejs zbyt ubogi jest zwykle o wiele większym problemem. Uważam jednak, że w programowaniu brzytwa Ockhama ma zastosowanie i że dróg prowadzących do tego samego celu nie należy mnożyć ponad potrzebę.
Na forum Warsztatu zdarzają się różne problemy. Część z nich dotyczy nieznanych przyczyn błędnego funkcjonowania programu lub jakiegoś kawałka kodu. Z pewnością nie jest tak, że takie wątki są z góry uznawane za niepożądane. To, co o tym decyduje, to przede wszystkim treść, opisowość i precyzja.
A z tym bywa kiepsko. Bardziej doświadczeni programiści wiedzą oczywiście, że do wyeliminowania błędu potrzebna jest dokładna wiedza, w jakich okolicznościach on występuje. A już zupełnie niezbędne jest określenie, co tak naprawdę się dzieje: błędny rezultat funkcji, wyjątek czasu wykonania, zawieszenie się programu, bluescreen, spalenie płyty głównej (no, może przesadzam ;)) ?…
Nierzadko jednak za cały opis ma wystarczać mgliste stwierdzenie, że coś nie działa. “Serio?” – chce się odpowiedzieć – “więc idź i to napraw ;P”. Przy tak skąpo opisanych objawach trudno przecież oczekiwać, żeby ktokolwiek mógł wywróżyć, co tak naprawdę jest ich przyczyną.
Czasem ta lakoniczność jest spowodowana tym, że dana osoba traktuje fakt niedziałania napisanego przez siebie kodu wręcz jako życiowe niepowodzenie lub – co gorsza – osobistą zniewagę. A gdy w grę wchodzą takie emocje, z zebraniem potrzebnych informacji może być kłopot…
Próbuję się tu wczuć w taką postawę, ale prawdę mówiąc zupełnie jej nie rozumiem. Może każdy na początku przygody z programowaniem reaguje podobnie, a ja zdążyłem już po prostu zapomnieć, że kiedyś mi się to zdarzało? A może jednak zależy to od charakteru i sprawia, że osoby biorące wszelkie niepowodzenia (w tym przypadku błędy) za bardzo do siebie mają trudności w zostaniu dobrymi programistami?
I czy faktycznie podejście emocjonalne tak bardzo przeszkadza?… Nie wiem, jak w jest w istocie, lecz wiem jedno: ostatnio zdecydowanie za często zajmuję się dziwnymi problemami :)
Dzisiaj pokażę pewną sztuczkę, mogącą nieco ułatwić życie programiście, który – jak wiadomo – zawsze ma za dużo pracy. Nie jest ona zbyt odkrywcza ani pomysłowa, ale ponieważ wielokrotnie zdarzyło mi się z niej korzystać (ostatni raz całkiem niedawno), sądzę że zasługuje na wzmiankę.
Sprawa dotyczy typów wyliczeniowych w C++ – czyli tworów, które pojawiają się często w większości programów. W moim ostatnim przypadku był to prosty typ definiujący wachlarz kolorów używanych w różnych miejscach systemu GUI:
Jest to podobny zestaw do tego, jaki można zobaczyć w Windows w oknie Właściwości: Ekran, na zakładce Wygląd. Naturalnie jest on dość okrojony, jako że nie potrzebujemy tutaj niczego aż tak skomplikowanego. Nie jest jednak wykluczone, że kiedyś się rozrośnie…
Każdemu z tych “systemowych” kolorów trzeba teraz przyporządkować kolor rzeczywisty – żeby wiedzieć, jak narysować konkretne kontrolki. Można zdefiniować do tego sporo zmiennych w stylu clActiveCaptionColor
czy clControlNormalColor
, ale lepszym rozwiązaniem jest tablica:
Problem w tym, że trzeba podać jej rozmiar. Można oczywiście wpisać tam na sztywno 5, ale doskonale wiadomo, jakie są skutki stosowania w kodzie “magicznych liczb”. Można zdefiniować sobie stałą, lecz wtedy też będziemy musieli ręcznie modyfikować jej wartość, jeżeli liczba kolorów się zmieni.
Potrzebujemy więc sposobu na automatyczne określenie liczby stałych zdefiniowanych w typie wyliczeniowym. W językach dysponujących dynamicznymi informacjami o typie – jak C# czy Java – byłoby to zapewne proste, lecz tutaj nie mamy tego komfortu. Zamiast tego możemy sobie jednak poradzić inaczej – dodając do typu wyliczeniowego kolejną stałą:
Kompilator nada jej wartość o jeden większą od poprzedniej. Ponieważ pierwszej stałej przypisał zero, GC_COUNT
będzie odpowiadało liczbie 5 i to będzie właśnie liczba użytecznych stałych w typie wyliczeniowym. Teraz można już zadeklarować potrzebną tablicę:
Podobnie można zrobić dla każdego typu wyliczeniowego. Najlepiej działa to wtedy, gdy kompilator sam numeruje jego stałe. Jeżeli sami to robimy, to oczywiście trik nadal będzie działał (zmienna *_COUNT
będzie zawsze miała największa wartość). W tablicy pojawią się jednak niewykorzystane elementy i te dziury ewentualnie trzeba będzie omijać.
Po kilku dniach niezbyt intensywnego zastanawiania się nad strukturą systemu GUI zdołałem wysmażyć coś, co można nazwać schematycznym modelem klas. Jest on wybitnie poglądowy i przedstawia się następująco:
Na pierwszy rzut oka może wydawać się nieco pokręcony, ale z odrobiną wysiłku można go odcyfrować :] To co jest w nim chyba najważniejsze, to dość wyraźny podział na część rysującą (lewa strona) i tę zawierającą kontrolki. Łączy je “most” złożony z głównej klasy systemu, nazwanej bardzo oryginalnie: System :) Taki podział zapewni między innymi prostsze rysowanie samych kontrolek, które będą renderowane przy pomocy takich poleceń jak: narysuj ramkę, narysuj wciśnięty przycisk, itp. To rozwiązanie zaczerpnąłem z Windows.Forms, gdzie jest zresztą ono posunięte o wiele dalej.
Lista kontrolek jest, jak widać, skromna i odpowiada tej, którą wcześniej ustaliłem jako minimalną. Oprócz abstrakcyjnej klasy dla wszystkich kontrolek wprowadziłem też osobną dla takich, które mogą zawierać i grupować inne kontrolki. Póki co jedynym obiektem tego rodzaju jest okno, ale GUI zna ich więcej: jak np. panel czy pole grupy (groupbox).