C dla każdego (cz. 7.)

Malowanie po ekranie (cz. 2)

Odcinek ten zawiera dokończenie omówienia listingu 9, który zamieściliśmy w poprzednim numerze Magazynu AMIGA.

W naszym programie występuje oczywiste powiązanie pomiędzy trybem pracy programu, klawiszem powodującym jego wywołanie i opisującym go tekstem. Stworzyliśmy więc własną strukturę o nazwie "operacja", która łączy te trzy elementy: pole "name" zawiera opis trybu, a "key" literę wywołującą dany tryb. Trzecie pole -- "fun" -- może być czymś nowym dla niezbyt doświadczonych programistów. Jest to wskaźnik na funkcję pobierającą argumenty "wskaźnik na strukturę IntuiMessage" oraz "wskaźnik na strukturę Window", a nie zwracającą niczego (void). Pełniejsze wiadomości znajdziesz w książce "Język ANSI C". W programie definiowana jest tablica takich struktur: "op_tab", w pola "fun" jest wpisywany adres funkcji obsługujących dany tryb programu, a to, w jakim trybie program znajduje się w danym momencie, jest odnotowane w zmiennej "op_pos", której wartość jest po prostu indeksem tablicy "op_tab". Aby wywołać funkcję, której adres znajduje się w polu "fun", należy postąpić tak samo, jakby się wywoływało normalną funkcję. My w programie robimy po prostu:

op_tab[op_pos].fun(&msg, window);

Deklarację zmiennych mamy już za sobą, następnie otwieramy biblioteki i okna, potem przychodzi czas na pętlę "for", wykonującą się do końca programu, oraz "rutynkę", czyli oczekiwanie na informacje z portu okna, a później jeszcze jakieś "cuda": "__STDC__" i inne.

Zastosowaliśmy wyrażenie, które nie jest znane wszystkim kompilatorom, a mianowicie skopiowanie jednej struktury do drugiej za pomocą pojedynczego przypisania. Taki zapis jest zgodny z normą ANSI języka C, ale niektóre starsze kompilatory mogą mieć z nim problemy. Z tego powodu użyliśmy dyrektyw preprocesora, powodujących warunkową kompilację: jeżeli jest zdefiniowana stała "__STDC__" i jest ona różna od 0 (#if __STDC__), co oznacza, że dany kompilator jest zgodny z normą ANSI, to używamy wyżej opisanej instrukcji, w przeciwnym wypadku (#else) jawnie kopiujemy jedną strukturę do drugiej. Warto może w tym momencie wspomnieć o kilku innych standardowo zdefiniowanych stałych, np. __SASC i AMIGA dla SAS/C, AZTEC_C i MCH_AMIGA dla Aztec C, __GNUC__ i AMIGA dla GNU CC. Dzięki nim można tworzyć programy, działające na kilku platformach sprzętowych oraz możliwe do skompilowania na różnych kompilatorach.

Wprawdzie nie wszyscy będą zmuszeni do wywołania funkcji CopyMem() z biblioteki Exec, jednak wszyscy powinni ją znać:

void CopyMem( APTR source, APTR dest, unsigned long size );

Zadaniem funkcji jest skopiowanie obszaru pamięci, wskazywanego przez wskaźnik "source", do obszaru wskazywanego przez "dest", rozmiar kopiowanego "kawałka" podany w bajtach jest trzecim argumentem. UWAGA! Kolejność parametrów "source" i "dest" jest ODWROTNA niż w standardowych funkcjach kopiujących języka C, takich jak "strcpy", "memcpy" itd.

Po wykonaniu kopii przybyłej wiadomości i zwrocie oryginału "rozpakowujemy prezent" od Intuition.

Klasa wiadomości IDCMP_VANILLAKEY:

Flaga ta powoduje dostarczanie do programu informacji o użyciu klawiatury. Informacja tego typu zawiera w polu "Code" kod znaku, jaki znajdował się pod naciśniętym przyciskiem, obsługiwane jest aktualne obłożenie klawiatury, ustawione przez użytkownika za pomocą systemowego programu "Input". VANILLAKEY dostarcza tylko pojedyncze znaki, nie jest możliwe odczytanie ciągów zdefiniowanych pod przyciskami, nie można dowiedzieć się o przycisku HELP, kursorach... Możliwe jest jednak odczytanie wciśnięcia klawisza [Esc], ponieważ jest on pojedynczym znakiem o kodzie 27.

W programie, po otrzymaniu informacji o naciśnięciu przycisku, sprawdzamy, czy jest to [Esc]. Jeśli tak, to opuszczamy program, w przeciwnym wypadku przeglądamy tablicę operacji, sprawdzając, czy któraś z nich nie ma skrótu z klawiatury identycznego z naciśniętym przyciskiem. Ten przydługawy warunek w pętli "for" pozwala przejrzeć wszystkie elementy tablicy "op_tab". Ponieważ operator "sizeof" zwraca rozmiar obiektu w bajtach, należy wielkość tę podzielić przez rozmiar pojedynczego elementu. Operator "sizeof" podaje rozmiar obiektu podczas kompilacji programu, w związku z tym wyrażenie w warunku pętli "for" otrzyma stałą wartość.

Po otrzymaniu informacji IDCMP_CLOSEWINDOW wywołujemy funkcję:

BOOL DoubleClick( ULONG sSeconds, ULONG sMicros, ULONG cSeconds, ULONG cMicros);

Jej zadaniem jest sprawdzenie, czy podane dwie wartości czasu mieszczą się w tzw. double clicku. Jeśli różnica między tymi wartościami jest mniejsza od "double clicku", to zostanie zwrócona wartość TRUE, w przeciwnym wypadku FALSE. Czas do funkcji podaje się w postaci sekund i mikrosekund pierwszego i drugiego wydarzenia. Można je znaleźć w strukturze "IntuiMessage".

Dzięki funkcji DoubleClick() program opuszcza się dopiero po dwukrotnym kliknięciu na gadżecie zamykania. W wypadku pojedynczego naciśnięcia zostanie zmieniony tryb pracy programu (tak, wiemy, to nie jest intuicyjne).

W wypadku, gdy system dostarczy informacji o przyciskach myszy (IDCMP_MOUSEBUTTONS), zostanie wywołana bieżąca funkcja z tablicy "op_tab".

Wewnątrz instrukcji "switch" pojawiła się kolejna, nie omówiona dotychczas, klasa informacji IDCMP, a mianowicie IDCMP_MOUSEMOVE. Klasa ta powoduje przekazywanie do portu okna aktywnego informacji o każdym ruchu myszy (trzeba je obsługiwać szybko, bo przy nagłych ruchach myszą przybywa naprawdę sporo informacji!). Do jej funkcjonowania konieczne jest umieszczenie w polu "Flags" okna flagi WFLG_REPORTMOUSE. Program w prezentowanej wersji nie robi nic w wypadku stwierdzenia takiej klasy wiadomości. Umieściliśmy ją jednak, aby umożliwić rozbudowę programu, która bez znajomości tej klasy informacji mogłaby być kłopotliwa.

Ponieważ w funkcji "main" nie znajdziemy już nic ciekawego, omówić funkcje zawarte w tablicy "op_tab":

linia() -- zawiera funkcję z biblioteki "graphics.library":

void Draw( struct RastPort *rp, long x, long y );

Jej zadaniem jest narysowanie linii pomiędzy obecnym położeniem kursora a podanymi współrzędnymi, zmieniane jest położenie kursora graficznego na wartości x i y.

punkt() -- zawiera dwie funkcje biblioteczne:

LONG WritePixel( struct RastPort *rp, long x, long y );

Zmienia kolor punktu o współrzędnych (x, y) na kolor ustawiony jako APen. Rezultatem działania funkcji jest zero, gdy wszystko jest OK, lub -1, gdy podany punkt jest poza rastportem. Funkcja, jako jedna z niewielu, nie zmienia położenia kursora graficznego.

void Move( struct RastPort *rp, long x, long y );

Przesuwa kursor graficzny w podane miejsce, bez jakiegokolwiek efektu wizualnego.

Funkcja napis() -- odwołuje się do funkcji systemowej:

LONG Text( struct RastPort *rp, STRPTR string, unsigned long count );

Umieszcza ona tekst, wskazywany przez "string", w rastporcie. Napis jest umieszczany w bieżącym miejscu (cp_x, cp_y) i rysowany bieżącą czcionką. Pisany jest zarówno poniżej, jak i powyżej położenia kursora (dlaczego tak jest, powiemy w kolejnej części). Po zakończeniu działania funkcji kursor graficzny znajduje się na końcu tekstu. Funkcja ta wymaga podania liczby znaków do wypisania -- służy do tego ostatni argument.

drmd() -- funkcja ta zmienia tryb rysowania po naciśnięciu lewego przycisku myszy. Systemowe narzędzia, służące do tego celu, opisaliśmy wcześniej. Występująca tu przydługa i skomplikowana konstrukcja, będąca drugim argumentem funkcji SetDrMd(), powoduje, że tryb rysowania zostanie ustalony na kolejny lub odliczanie rozpocznie się od początku. Postępując zgodnie z kolejnością operatorów, najpierw zostanie wykonana konstrukcja warunkowa, zwracająca numer obecnego trybu (jej zadanie było omówione wcześniej) -- po jego zwiększeniu o jeden otrzymamy numer kolejnego trybu. Ponieważ argument funkcji SetDrMd(), będący równocześnie indeksem tablicy names[], powinien być istniejącym trybem, należy zadbać o to, aby wartość ta nie przekraczała 7, ponieważ po zsumowaniu masek bitowych wszystkich trybów rysowania otrzymamy taką właśnie wartość (patrz "graphics/rastport.h"). Do zapisania liczby z przedziału 0..7 wystarczą trzy najmłodsze bity -- właśnie w tym celu wykonujemy bitową operację I (AND) z flagą 7 (bitowo 0111). Dzięki temu przetrwają jedynie najmłodsze bity i zapisanie liczby większej od 7 nie będzie możliwe, uzyskamy więc cyklicznie zmieniający się tryb graficzny. Niektórzy z Was zapewne zastanawiają się, dlaczego nie zastosowaliśmy tu prostszej i bardziej ogólnej metody z wyrażeniem warunkowym "if" -- po prostu tak, jak jest, jest szybciej i krócej.

color() -- zmienia kolor tła po przyciśnięciu prawego przycisku oraz atramentu po przyciśnięciu lewego.

kwadrat() -- rysuje wypełniony prostokąt od położenia kursora graficznego do położenia myszy w momencie kliknięcia. Zwie się niezbyt zgodnie z prawdą, ponieważ kolega Darek nie odróżnia prostokątów od kwadratów. Funkcja ta korzysta z funkcji biblioteki graficznej:

void RectFill( struct RastPort *rp, long xMin, long yMin, long xMax, long yMax );

Jej zadaniem jest wypełnienie prostokątnego obszaru za pomocą aktualnego koloru APen. Współrzędne lewego górnego rogu prostokąta (xMin, yMin) MUSZŃ być mniejsze od współrzędnych prawego dolnego rogu (xMax, yMax) lub im równe.

elipsa() -- funkcja rysuje elipsę, długości półosi są równe współrzędnym wektora łączącego punkt (cp_x, cp_y) z punktem, nad którym "przyciśnięto" mysz. Biblioteczna funkcja do rysowania elipsy wygląda następująco:

void DrawEllipse( struct RastPort *rp, long xCenter, long yCenter, long a, long b );

xCenter i yCenter to współrzędne Środka elipsy, a i b to półosie elipsy (muszą być większe od zera). Współrzędne cp_x i cp_y nie są zmieniane.

Za miesiąc pokażemy, jak używać różnych czcionek oraz jak tworzyć górne menu.