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.