C dla każdego (cz. 2.)

Okno na szary świat

Najwyższy czas zabrać się do konkretnej pracy. Dziś zajmiemy się oknami, obracać się więc będziemy w kręgu biblioteki Intuition, która zarządza GUI.
Zacznijmy od, dostępnych dopiero po otwarciu biblioteki Intuition, funkcji do otwierania okien. Dawniej istniała tylko jedna taka funkcja:

struct Window *OpenWindow( struct NewWindow *newWindow );

Obecnie (od systemu 2.0) istnieje też druga:

struct Window *OpenWindowTagList( struct NewWindow *newWindow, struct TagItem *tagList );
struct Window *OpenWindowTags( struct NewWindow *newWindow, unsigned long tag1Type, ... );

Pewnie cisną się Wam na usta dwa pytania: co robią powyżej dwa prototypy funkcji, skoro miał być tylko jeden, i co oznaczają parametry tych funkcji. Pierwszy parametr to wskaźnik na zainicjowaną wcześniej strukturę informacyjną, w której umieszczało się informacje o tym, jaki miał być rozmiar okna, jakie miało mieć ono gadżety, tytuł itp. Piszemy w czasie przeszłym, ponieważ parametru tego w dzisiejszych czasach już się właściwie nie używa (jako pierwszy parametr podaje się po prostu NULL, czyli 0). Struktura NewWindow została wyparta przez tagi.

Dlaczego tagi?

System się rozwija. Od czasu pierwszej Amigi dużo zostało zrobione. Niestety, nawet pomimo dużej liczby "furtek", pozostawionych przez twórców systemu, niektóre struktury stały się w końcu zbyt ciasne -- klasycznym przykładem jest właśnie struktura NewWindow. Zastępowanie ich w każdym systemie nowymi nie ma sensu. Właśnie dlatego pojawiły się tagi. Za ich pomocą można przekazywać do funkcji nieograniczone ilości informacji, bez posługiwania się sztywnymi strukturami, tagi umożliwiają też bezproblemowe dodawanie nowych elementów do systemu operacyjnego. Argumenty podaje się parami (strukturami TagItem) -- pierwszy element pary to właśnie TAG, który jest po prostu stałą informującą, co oznacza dana przekazana w drugim elemencie pary, może to być np. szerokość okna, jego tytuł (wskaźnik na nazwę) itd. Po pierwszej parze może nastąpić druga, trzecia i następne.
Funkcje korzystające z tagów mogą być wywołane na dwa sposoby. Istnieją dwie deklaracje tej samej funkcji bibliotecznej (różniące się zwykle końcówką nazwy -- tak jak w wypadku OpenWindowTagList() i OpenWindowTags()). Pierwszym ze sposobów wywołania takiej funkcji jest podanie jej wskaźnika na tablicę tagów w charakterze argumentu (w naszym wypadku chodzi o ostatni argument funkcji OpenWindowTagList()). Tablica taka jest wypełniona strukturami TagItem. W drugim sposobie (OpenWindowTags()) zamiast wskaźnika na zdefiniowaną wcześniej tablicę podaje się po prostu pary jako kolejne parametry do funkcji o zmiennej liczbie argumentów (na podobnej zasadzie, jak w "printf()"). Oba sposoby są równoważne, w rzeczywistości funkcja przyjmuje argumenty w tej pierwszej postaci, a druga jest po prostu "tłumaczona", przez kompilator, na pierwszą, a istnieje dlatego, że jest wygodniejsza w użyciu.
Warto wspomnieć o tagu, który MUSI się pojawić na końcu, w tablicy struktur TagItem, lub jako ostatni argument funkcji. Jest nim TAG_END, nazywany również TAG_DONE (równy 0). Właśnie ten tag informuje funkcję, że nie ma już więcej danych. Jeśli go zabraknie, to nieszczęsna funkcja nadal będzie czytać dane z przypadkowego obszaru pamięci i może się naczytać bzdur. Dlatego uczulamy Was: PAMIĘTAJCIE o stawianiu tagu TAG_END. Wykrycie takiego błędu nie zawsze jest proste, gdyż program może czasami "chodzić" całkiem normalnie (bo akurat znajdzie w pamięci zero).
Obie opisane powyżej funkcje do otwierania okien zwracają adres struktury opisującej otwarte okno (struktura Window) lub NULL, w wypadku gdy spotka je niepowodzenie.
Zastosowanie tych funkcji można zobaczyć tutaj:

Listing nr 4

Przypominamy, że listingi nie są kompletne! Należy dołączyć funkcję check_os() z poprzedniej części kursu oraz definicje stałych "OS_xx", deklarację zmiennej "SysBase" i pliki nagłówkowe "exec/execbase.h" oraz "stdlib.h".
Warto wytłumaczyć, co oznaczają użyte w tym przykładzie tagi:

WA_Left -- współrzędna lewej krawędzi okna,
WA_Top -- współrzędna górnej krawędzi okna,
WA_Width -- rozmiar okna w poziomie,
WA_Height -- rozmiar okna w pionie.

W wypadku Amigi współrzędne są podawane względem lewego górnego rogu, który ma współrzędne (0, 0), ekran za jest położony niejako w czwartej ćwiartce układu współrzędnych. Nie należy się sugerować programami graficznymi, w wypadku których wyświetlane współrzędne są przeliczane w taki sposób, że ekran jest przez użytkownika widziany jako pierwsza ćwiartka kartezjańskiego układu współrzędnych. Ta sama zasada obowiązuje w wypadku okien. Współrzędne w oknie oblicza się względem lewego górnego rogu okna.

WA_Title -- tytuł okna (wyświetlany na jego listwie),
WA_Flags -- flagi dotyczące głównie wyglądu okna:
WFLG_SIZEGADGET -- okno ma mieć gadżet do zmieniania wielkości,
WFLG_DRAGBAR -- okno ma mieć listwę do przesuwania za pomocą myszy,
WFLG_DEPTHGADGET -- okno ma mieć gadżet przód/tył,
WFLG_CLOSEGADGET -- okno ma mieć gadżet zamykania,
WFLG_ACTIVATE -- okno ma zostać automatycznie uaktywnione po otwarciu,
WFLG_RMBTRAP -- naciskanie prawego przycisku menu nie ma powodować rysowania listwy menu.

Niektóre flagi mają swoje odpowiedniki wśród tagów. Można je wywoływać bezpośrednio, podając po nich wartość TRUE. Przykładami takich flag mogą być: WA_SizeGadget, WA_DepthGadget, WA_CloseGadget. Dokładny ich wykaz, wraz z definicjami stałych, znajduje się w pliku "intuition/intuition.h".

WA_ScreenTitle -- napis wyświetlany na górnej listwie ekranu, gdy okno jest aktywne.

W programie została użyta pewna funkcja z biblioteki "dos.library". Jej składnia jest bardzo prosta:

void Delay( long timeout );

Za pomocą tej funkcji można chwileczkę odczekać. Długość chwileczki jest podawana w jednostkach nazywanych ticks, 50 takich jednostek to jedna sekunda. Uważny Czytelnik zapewne się zastanawia, jakim prawem wołamy funkcję z biblioteki dos.library bez jej otwarcia. Możemy pozwolić sobie na tę "zbrodnię", ponieważ bibliotekę tę otwiera dla nas moduł startowy, dołączany przez linkera, i podstawia pod zmienną "DOSBase" (więcej o module startowym powiemy w przyszłości).
Można więc chyba uznać, że otwarcie okna mamy za sobą. Teraz należy się nauczyć z nim komunikować. Jest to rozwiązane za pomocą tak zwanych portów (struktur MsgPort), będących czymś w rodzaju skrzynek kontaktowych. Jeden program umieszcza w porcie wiadomość (strukturę Message), a drugi po chwili ją odbiera -- daje się tu wyraźnie odczuć multitasking.
Program będący właścicielem okna sam decyduje, jakie wiadomoci chce za jego pomocą uzyskiwać. Może go np. zupełnie nie interesować, czy użytkownik nacisnął jakiś klawisz na klawiaturze albo czy wyjął dyskietkę ze stacji. To, jakie wiadomości program ma otrzymywać, ustala się podczas otwierania okna, za pomocą tagu "WA_IDCMP" (ta "wpadająca w pamięć" nazwana jest skrótem od Intuition Direct Communication Message Port). Jako dane dla tego tagu podaje się stałe symboliczne, oznaczające poszczególne klasy wiadomoci, jakie program ma otrzymywać. Jest ich mnóstwo. Będziemy je podawać stopniowo (żeby Was nie dobić). W tej części użyjemy w przykładach następujących klas:

IDCMP_CLOSEWINDOW -- kliknięcie na gadżet zamykania okna.
IDCMP_DISKINSERTED -- włożenie dysku.
IDCMP_DISKREMOVED - wyjęcie dysku.
IDCMP_MOUSEBUTTONS -- naciśnięcie bądź zwolnienie któregoś z przycisków myszy (tylko przy aktywnym oknie).

W wypadku okien adres portu znajduje się w strukturze Window, w polu o nazwie "UserPort". Program pragnący otrzymać wiadomość nie musi co chwilę sprawdzać, czy użytkownik wykonał jaką operację (np. kliknął myszą). Wystarczy, że poprosi system o poinformowanie go, kiedy coś takiego nastąpi. W programach bardzo często zachodzi potrzeba oczekiwania na wydarzenie, do tego celu służy funkcja Execa:

struct Message *WaitPort( struct MsgPort *port );

Funkcja ta oczekuje na pojawienie się jakiejkolwiek wiadomości w podanym porcie, jest ona wykonywana aż do czasu pojawienia się wiadomości, wówczas przekazuje sterowanie z powrotem do programu. Funkcja ta zwraca wskaźnik na pierwszą wiadomość umieszczoną w porcie, choć może być ich w porcie więcej. Należy pamiętać, że funkcja ta czeka "do upadłego". Jeśli żadna wiadomość do portu nie dotrze, to program dalej "nie pójdzie". Nierzadkim błędem jest oczekiwanie na wiadomoci od okna, które żadnych wiadomości przysłać nie może (ma pole IDCMPFlags==0). Na szczęście trudno taki błąd przegapić.
Kiedy już stwierdzimy, że w porcie jest jakaś wiadomość, to należy ją odebrać. Posłużymy się w tym celu funkcją Execa:

struct Message *GetMsg( struct MsgPort *port );

Funkcja ta usuwa z portu pierwszą ze znajdujących się w nim wiadomoci i jej adres zwraca naszemu programowi. W wypadku, gdy port jest pusty, funkcja zwraca NULL.
W otrzymanej w ten sposób wiadomoci znajdziemy wiele cennych informacji. Omówimy je za chwilę. W wypadku operacji na portach ważny jest pośpiech, tak więc istotne dla nas informacje należy sobie w pomocniczych zmiennych zapamiętać, a wiadomoć jak najszybciej zwrócić nadawcy. Należy pamiętać, aby niczego w otrzymanej wiadomoci nie zmieniać, bo mogą wystąpić problemy. Wiadomoć odsyłamy za pomocą funkcji Execa:

void ReplyMsg( struct Message *message );

Funkcja ta odsyła wiadomość do "macierzystego" portu. Po odesłaniu wiadomości NIE WOLNO już z niej korzystać. System może zrobić z nią, co zechce, np. zwolnić zajmowaną przez nią pamięć, przez co dane by po prostu znikły i naczytalibyśmy się bzdur.
W wypadku wiadomości pochodzących od okien mamy do czynienia z rozszerzonymi strukturami Message o nazwie IntuiMessage. Postępowanie jest takie samo, jak w wypadku rozszerzonych struktur Library. I tym razem pierwszym polem struktury IntuiMessage jest struktura Message, można więc swobodnie zastosować rzutowanie typów. Zawartością struktury Message zajmiemy się póˇniej, przy dokładnym omawianiu portów (są to zresztą systemowe zawiłości, bez których można się obejść). Teraz omówmy pozostałe pola struktury IntuiMessage. Zajrzyjmy do niej -- znajduje się ona w pliku "intuition/intuition.h". Najważniejsze pola to:

ULONG Class -- Informuje o klasie (rodzaju) wiadomości, która dotarła do portu. Są to te same stałe, których używa się wcześniej w tagu WA_IDCMP (IDCMP_CLOSEWINDOW itd.).

UWORD Code -- Zawiera dodatkowe informacje, zależne od rodzaju wiadomości, np. dla wiadomości IDCMP_MOUSEBUTTONS pole to zawiera informacje o tym, który przycisk myszy został wciśnięty lub zwolniony (dokładniej o tym za chwilę). Niektóre rodzaje wiadomości w ogóle nie używają tego pola, np. IDCMP_CLOSEWINDOW.

UWORD Qualifier -- To pole zawiera nie przetworzone informacje, dostarczone przez program systemowy o nazwie input.device, zajmujący się obsługą wejścia/wyjścia. Jest ono rzadko wykorzystywane. Można tu znaleźć np. informacje o tym, czy gdy wystąpiło zdarzenie, był wciśnięty lewy klawisz [Shift] (IEQUALIFIER_LSHIFT), albo czy dana cyfra została wpisana na klawiaturze numerycznej (IEQUALIFIER_NUMERICPAD) itp. -- odpowiednie stałe są zdefiniowane w pliku "devices/inputevent.h".

APTR IAddress -- Zawiera adres obiektu, będącego przyczyną wysłania wiadomoci, jest zależne od jej rodzaju, np. w wypadku wiadomoci o gadżetach zawiera adres gadżetu.

WORD MouseX, MouseY -- Zawierają położenie myszy w chwili wystąpienia zdarzenia, współrzędne są podawane względem lewego górnego rogu okna. Jeśli w polu IDCMPFlags okna był ustawiony znacznik IDCMP_DELTAMOVE, współrzędne będą podawane względem poprzednio meldowanego położenia (wektor przesunięcia).

ULONG Seconds, Micros -- Zawierają czas systemowy, w którym zdarzenie nastąpiło (bardzo przydatne, gdy chcemy sprawdzić, czy miał miejsce tzw. double-click, czyli szybkie, dwukrotne, kliknięcie myszą).

Jak już wcześniej pisaliśmy, wiadomości otrzymane od Intuition należy jak najszybciej zwrócić. W związku z tym najwygodniej jest skopiować całą strukturę lub tylko te pola, które nas interesują. Przetrzymywanie informacji od Intuition może wpłynąć na spowolnienie działania systemu. Po wykonaniu kopii i zwróceniu wiadomości możemy się swobodnie i bez pośpiechu zastanawiać, co począć dalej. Nieocenioną pomocą jest instrukcja "switch". Na początku należy rozpoznać rodzaj wiadomości, czyli zerknąć do pola "Class", a następnie, w zależności od jego zawartości, analizować pozostałe pola.
W wypadku gdy otrzymamy informację typu IDCMP_CLOSEWINDOW, wiemy, co sygnalizuje użytkownik -- pozostałe pola nie zawierają istotnych informacji. Podobnie jest w wypadku IDCMP_DISKINSERTED i IDCMP_DISKREMOVED -- nie ma sensu patrzeć na zawartość pozostałych pól (jeżeli ktoś nie pisze programu sprawdzającego, jak szybko można zmienić dyski, wówczas potrzebne byłyby pola Seconds i Micros, ale sadyzmu nie popieramy).
Zupełnie innaczej wygląda sytuacja z wiadomościami o "przyciśnięciu myszy" (IDCMP_MOUSEBUTTONS). W tym wypadku pole "Code" zawiera użyteczne informacje:

SELECTDOWN -- wciśnięto lewy przycisk myszy,
SELECTUP -- zwolniono lewy przycisk myszy,
MENUDOWN -- wciśnięto prawy przycisk myszy,
MENUUP -- zwolniono prawy przycisk myszy,
MIDDLEDOWN -- wciśnięto środkowy przycisk myszy,
MIDDLEUP -- zwolniono środkowy przycisk myszy.

Wiadomości tej klasy docierają tylko do jednego okna -- do tego, które jest aktywne.
Amiga obsługuje trzeci przycisk myszy, choć często go nie ma, natomiast pecety mają go prawie zawsze, ale i tak ogromna większość programów używa tylko jednego przycisku -- lewego (dla zainteresowanych dodam, że w Amidze trzeci przycisk myszy należy podłączyć do wyprowadzenia numer 5 oraz do masy, czyli wyprowadzenia numer 8).
Za miesiąc przedstawię wykorzystanie portu okna.