C dla każdego (cz. 1.)

Wstęp

Język C to dzisiaj jeden z najpopularniejszych języków programowania, a bez wątpienia najpopularniejszy język programowania wyższego poziomu na Amidze.

Można by wymienić wiele zalet języka C, ale naszym zdaniem naprawdę ważne są tylko trzy.

* Prostota. C jest bardzo surowy, można wręcz powiedzieć -- ascetyczny. Trzon języka jest niezwykle skromny, dzięki czemu łatwo go opanować (naszym zdaniem znacznie łatwiej niż np. promowany w polskim szkolnictwie Pascal). C jest elastyczny i nakłada na programistę niewiele ograniczeń.

* Powszechność. Niemal każda publikacja na temat programowania Amigi dotyczy właśnie tego języka. W tym języku jest napisany system operacyjny naszego komputera (niecały część, tzn. fragmenty wymagające maksymalnej prędkości, została napisana w asemblerze).

* Bardzo dobre kompilatory. Jest ich wiele. Od tych działających już na A500 z 0,5 MB RAM, aż po kolubryny ledwie pracujące na A1200 z 6 MB RAM.

Kompilatory

Co do różnych kompilatorów, to od razu pojawia się pewien problem. Po prostu różne kompilatory są ze sobą nie do końca zgodne. My, podobnie jak [sts] (ach, wy tajniacy -- patrz Magazyn AMIGA 6/94, artykuł "SAS C")[ w ramach odtajnienia - Stanisław Szczygieł - jakby ktoś tego nie wiedział - przyp. mp] uważamy, że najlepszy z dostępnych na Amidze kompilatorów to SAS/C 6, więc wszystkie przykłady będą pisane z myślą o nim. Nie powinno być jednak większych problemów przy używaniu innych kompilatorów.
Co do opcji kompilatora, to sugerujemy zadbać, aby typ "char" był bezznakowy (wartości od 0 do 255, a nie od -128 do 127). W SAS/C 6 robi się to podając opcję UCHAR, w Manx Aztec C 5 opcję -PP, w GNU CC opcję -FUNSIGNED-CHAR. Należy się również upewnić, że typ "int" jest 32-bitowy (long-int), a nie 16-bitowy (short-int) -- kompilatory powinny mieć standardowo ustawioną właściwą wartość.

Program kursu

Czas napisać, czego chcielibyśmy Was nauczyć i co zakładamy, że już umiecie. Zaczynając od tego drugiego: NIE będziemy uczyć podstaw języka C. Na ten temat istnieje mnóstwo publikacji, poza tym podstawy są na każdym komputerze takie same, a więc można się uczyć nawet z książek o pececie. My polecamy zdobycie "biblii języka C" -- książki "Język ANSI C" autorstwa Briana W. Kernighana i Dennisa M. Ritchie (jest to nowa pozycja, wydana przez WNT, Warszawa 1994 -- nie polecamy pierwszego, przestarzałego już, wydania). Książka ta opisuje w dość przystępny sposób standard języka, bez żadnych pecetowych rozszerzeń (jest tylko jeden rozdział o Unixie).
Czego więc chcemy Was nauczyć? Chcemy dać Wam podstawy niezbędne do pisania zgodnych ze środowiskiem systemu operacyjnego aplikacji. Można by więc powiedzieć, że kurs ten to nie kurs języka C, ale kurs programowania zgodnego z systemem operacyjnym. Co chcemy przez to powiedzieć? To, że zdecydowaną większoć zawartych w tym kursie informacji będą mogli wykorzystywać nie tylko programujący w C, ale również programujący w asemblerze, Pascalu, E itd.
Nasz kurs języka C nie jest pierwszym w polskiej prasie komputerowej. Co więc nowego mamy zamiar wnieść w stosunku do kursów prowadzonych kiedyś przez Bohdana R. Raua w "Amigowcu" i Jarosława Chrostowskiego w "C64+4 & AMIGA"? Przede wszystkim nasz kurs będzie nowocześniejszy. System operacyjny się zmienia (no, ostatnio nieco wolniej, ale miejmy nadzieję, że to zastój chwilowy). Kurs z "Amigowca" bazował na systemie operacyjnym w wersji 1.3, my tymczasem mamy zamiar bazować na wersji 2.04 systemu, nie zapominając przy tym o systemach 2.1 i 3.0. Chcielibyśmy to jeszcze raz podkreślić: BĘDZIEMY PISALI O OS 2.04+. Większość przykładów (jeśli nie wszystkie) będzie wymagać przynajmniej tej wersji systemu operacyjnego.
Zaczniemy od tego, co z punktu widzenia użytkowników jest najbardziej widoczne -- od graficznego interfejsu użytkownika, tzn. ekranów, okien, gadżetów, menu. System operacyjny Amigi to jednak nie tylko okienka. Istnieje masa wielce przydatnych bibliotek nie związanych z GUI (Graphic User Interface -- Graficzny Interfejs Użytkownika). Opiszemy więc "exec.library", czyli System Executor -- bibliotekę zarządzającą całym systemem, oraz "dos.library" -- bibliotekę zarządzającą wysokopoziomowym I/O. Właściwie to chcielibyśmy po trochu opisać wszystkie najważniejsze elementy systemu operacyjnego, aby dać Wam dobre rozeznanie w tym, co system jest w stanie programiście zaoferować. Chcielibyśmy więc napisać również o "amigaguide.library", "commodities.library", "workbench.library", "locale.library", "iffparse.library" itd. Są to, rzecz jasna, nasze pobożne życzenia, a czy te ambitne plany uda nam się zrealizować, czas pokaże.
Jak już wcześniej wspomnieliśmy, chcemy dać Wam "podstawy". Oznacza to, że NIE będziemy dokładnie opisywać wszystkich funkcji, pól struktur czy stałych. Jest to po prostu fizycznie niemożliwe. System operacyjny jest tak obszerny, że jego pełny opis zajmuje tysiące stron (mówimy o serii książkowej "Reference Manuals"). Z konieczności więc będziemy opisywali tylko najważniejsze elementy systemu, najczęściej używane w typowych programach. W tekście artykułu znajdą się też z pewnością pewne niedomówienia oraz półprawdy -- chodzi po prostu o to, że gdybymy zbyt często zastrzegali się, że "nie do końca jest tak, jak piszemy, bo...", to niezbyt dobrze obeznani z tematem Czytelnicy po prostu utraciliby główny wątek artykułu. Zagubiliby się w tych wszystkich niuansach (albo zaczęliby sądzić, że "ktoś" usiłuje ich "zrobić w konia"); poza tym artykuł znacznie by się wtedy wydłużył.
Rzecz jasna, będziemy się starali robić jak najmniej błędów. Najprawdopodobniej wszystkie przykłady będą intensywnie testowane przy użyciu opisanych przeze mnie w Magazynie AMIGA 12/94--1/95 debuggerów. Jeżeli jednak znajdziecie w artykule bądˇ w przykładach jakie błędy, lub też macie inne uwagi/sugestie dotyczące tego kursu, to piszcie do redakcji. Nie oczekujcie jednak, że Wasze listy znajdą odzwierciedlenie już w kolejnej części kursu. Ze względu na długi cykl wydawniczy Magazynu AMIGA oraz na to, że piszemy z dwumiesięczną "zakładką", Wasze uwagi możemy uwzględnić najwcześniej 3 miesiące po ich otrzymaniu.

"Inkludy"

Chcielibyśmy skupić się na chwilę na "inkludach", czyli plikach nagłówkowych, dołączanych przez kompilator dyrektywą "#include". Na Amidze są one bardzo rozszerzone w stosunku do standardu ANSI -- zawierają wszystkie definicje i deklaracje niezbędne do pisania aplikacji korzystających z zasobów systemu operacyjnego. Istnieją różne wersje "inkludów" -- musicie mieć je przynajmniej w wersji dla OS 2.0, a w niektórych wypadkach potrzebne będą inkludy dla OS 3.0 (my używamy inkludów dla OS 3.1 i Wam też to polecamy -- można je znaleźć na dysku CD Freda Fisha bądź w Internecie: serwer "ftp.rz.uni-wuerzburg.de", katalog "pub/amiga/frozenfish/bbs/cbm"). O strukturze inkludów powinnicie wiedzieć tyle, że opisy "device'ów" znajdują się w katalogu "devices", a bibliotek w katalogu "libraries" (nie zawsze -- duże biblioteki mają osobne katalogi, np. "exec", "intuition", "dos"). W katalogu "clib" znajdują się prototypy (deklaracje) funkcji z poszczególnych bibliotek i niektórych device'ów. Twórcy kompilatorów tworzą często dodatkowy katalog, o nazwie "pragmas" bądź "inline", zawierający pewne informacje umożliwiające kompilatorom tworzenie bardziej efektywnych wywołań funkcji systemowych. Czasami tworzą też oni katalog "proto", zawierający proste pliki powodujące załadowanie jednym ruchem deklaracji i pragm, np. wykonanie w SAS/C "#include <proto/intuition.h>" powoduje dołączenie "clib/intuition_protos.h" i "pragmas/intuition_pragmas.h". Niestety, nie we wszystkich kompilatorach tak jest. Niektóre nie mają plików "proto" i trzeba "ręcznie" dołączać deklaracje i pragmy. Niektóre nie mają również pragm -- wtedy dołącza się tylko deklaracje.
Wydaje mi się, że jak na wstęp, to ten fragment zrobił się to nieco przydługi. Aby więc nie tracić czasu ani cennego miejsca, już dziś zaczynamy "regularny" kurs. "Oddaję więc klawiaturę" w ręce mego wspólnika:
"Ależ dziękuję Kamilku za okazaną mi łaskę w postaci wysłużonej dyskietki, o otrzymaniu klawiatury nie śmiałbym marzyć nawet skrycie."

Co to jest multitasking?

Multitasking jest to możliwość wykonywania jednocześnie (lub jeśli ktoś chciałby być dokładny, prawie jednocześnie) kilku programów. Taki system pracy komputera pozwala na pełniejsze wykorzystanie mocy procesora z dwóch zasadniczych powodów.
Po pierwsze komputer domowy w większości wypadków czeka na sygnał od użytkownika lub urządzeń zewnętrznych, w tym czasie inny program może efektywnie wykorzystywać system (obciążenie procesora da się łatwo sprawdzić, np. za pomocą programu Spy -- w graficzny sposób przedstawia on stopień zajęcia procesora, czas, przez jaki pracował on "na pełnych obrotach" oraz przez jaki "zbijał bąki").
Drugim równie ważnym powodem budowy takich systemów jest możliwość współpracy kilku niezależnych programów, które mogą na bieżąco przekazywać sobie wyniki swojej pracy, a w ten sposób zaoszczędzić czas potrzebny na zapis i odczyt danych z/do plików, co jest znaczące przy większych obliczeniach. Taki sposób pracy jest szalenie wygodny. W tym właśnie celu programy są wyposażane w interface ARexxa (Amiga Rexx jest językiem stworzonym w celu nadzorowania pracy programów (patrz Magazyn AMIGA 1/92, wrzesień).
Niestety, nie ma róży bez kolców: z multitaskingiem trzeba uważać. Pisząc programy należy pamiętać o tym, że podczas ich wykonywania mogą się znajdować w pamięci inne programy. To jest nasz problem -- problem programistów, którzy muszą pamiętać, że "nie są sami", że nie wolno "grzebać" w nie swojej pamięci oraz zmuszać procesora do pracy, jeli nie jest to konieczne. Zabronione jest tworzenie tak zwanych busy loopów, czyli wykonujących się bez przerwy pętli służących do oczekiwania na informację (dokładniej zajmiemy się tym przy okazji opisu funkcji Execa Wait() oraz portów).
Kolejnym problemem jest nieco wolniejsze funkcjonowanie programów, jednak twórcy borykają się z nim raczej rzadko, ponieważ zastosowanie multitaskingu spowalnia działanie programów w niewielkim stopniu (poza tym można programowi nadać wyższy priorytet, dzięki czemu będzie on miał pierwszeństwo w stosunku do pozostałych).
Jednak jeśli komuś zależałoby na wyciśnięciu z maszyny wszystkiego, może wyłączyć pozostałe zadania (taski). Do tego celu przeznaczone są funkcje Execa (Forbid(), Permit()), jednak nie będziemy się nimi zajmowali (przynajmniej na początku).
Amigowski multitasking jest dość zgrabnie zorganizowany -- nawet jeli dojdzie do katastrofy i jakiś program się "powiesi", to nie oznacza to całkowitej klęski systemu. Problem taki jest znany użytkownikom programu Windows 3.1, który ma multitasking kooperatywny, czyli, mówiąc złośliwie, multitasking bez multitaskingu -- programy zwalniają procesor, gdy tego pragną, brak tam nadzorcy (patrz PC World Komputer Luty 1994: "Dos umarł!"). System zastosowany w Amidze wygląda inaczej, powiedziałbym, że jest pod wieloma względami lepszy. Na tym komputerze stale funkcjonuje program w trybie nadzorcy (Supervisor -- tryb procesora), zajmujący się przydzielaniem czasu procesora programom znajdującym się na liście oczekujących (ten program to taki komitet kolejkowy). Istnienie "wszechmocnego" nadzorcy pozwala na wykrycie i usunięcie z listy zawieszonego programu, dzięki czemu system może działać nadal. W zasadzie mowa tu o anormalnym zachowaniu się oprogramowania, jednak takie nieszczęścia z całą pewnością spotkają Czytelników podczas pisania i testowania programów. Tyle wiedzy na temat multitaskingu powinno wystarczyć. Nareszcie możemy się zająć bibliotekami, które są wypełnione po same brzegi funkcjami.

Dlaczego biblioteki?

Stosowanie bibliotek zewnętrznych wynika z zastosowania multitaskingu. W wypadku pecetowego DOS-u funkcje znajdują się w bibliotekach, które przy linkowaniu (konsolidacji) programu są doń dołączane -- postępowanie takie znacznie wydłuża kod programu, a poza tym w wypadku funkcjonowania kilku programów jednocześnie jest wprost zabójcze dla pamięci (w wypadku Amigi również istnieją takie biblioteki, ale często ich praca ogranicza się do otwarcia zewnętrznej biblioteki i wywołania jakiejś jej funkcji). Biblioteki amigowe to wygoda i oszczędność -- spróbujmy to udowodnić. W chwili, gdy kilka programów korzysta z takiej samej funkcji, powiedzmy z file-requestera (okna wyboru plików), nie muszą one mieć takiej funkcji w swoim kodzie -- wystarczy, że skorzystają z biblioteki systemowej "asl.library", która zawiera taką funkcję. Wykorzystywanie funkcji bibliotecznych oszczędza również czas przeznaczony na pisanie i testowanie programów, poza tym dzięki zastosowaniu bibliotek programy są podobne do siebie zarówno pod względem obsługi, jak i graficznego interfejsu użytkownika (GUI). Spróbujmy sobie wyobrazić, że każde okno ma gadżet zamykania w innym miejscu, a zrozumiemy, co znaczy standard.
Częć bibliotek znajduje się w pamięci stałej komputera (od systemu 2.0 jest tego 512 KB), pozostałe biblioteki znajdują się na dysku, przeważnie w katalogu "LIBS:". Podstawową biblioteką jest "exec.library", bez jej udziału nie jest możliwe funkcjonowanie właściwie żadnego programu. Biblioteka ta jest otwierana przy inicjacji systemu i pozostaje otwarta do końca jego pracy. W jej zasobach znajdują się funkcje służące do przydzielania pamięci, otwierania innych bibliotek oraz wiele innych niezwykle pożytecznych narzędzi. Ze względu na swój specyficzny charakter biblioteka Exec musi być dostępna dla każdego i w każdej chwili. Z tej właśnie przyczyny, w przeciwieństwie do pozostałych bibliotek, nie ma potrzeby jej otwierania.

Dlaczego otwieramy biblioteki?

Biblioteka podczas otwierania jest umieszczana w pamięci, jeśli pochodzi z dysku, jeśli natomiast pochodzi z ROM-u, to pozostaje w nim, a w pamięci RAM zapisywana jest jedynie pomocnicza struktura opisująca bibliotekę. W celu otwarcia biblioteki należy posłużyć się funkcją Execa:

struct Library *OpenLibrary( UBYTE *libName, unsigned long version );

Pierwszym argumentem funkcji jest wskaźnik na nazwę biblioteki, drugim minimalna wymagana wersja biblioteki. Jeżeli funkcja odnajdzie żądaną bibliotekę w odpowiedniej wersji, to zwróci adres opisującej ją struktury "Library", w przeciwnym wypadku zwróci 0 (NULL). Dlaczego istnieje parametr ograniczający wersję biblioteki? Odpowiedź jest prosta: biblioteki rozrastają się i mają coraz więcej funkcji, programiści są i często muszą być wybredni, właśnie dlatego umiera system w wersji 1.3, a poprzednie już zostały pochowane. Funkcja OpenLibrary w tej formie pojawiła się w systemie 1.2 -- wcześniej istniała funkcja o tej samej nazwie różniąca się tym, że nie sprawdzała, jaką wersję biblioteki otwiera. Stara funkcja została zachowana dla utrzymania zgodności systemu z już napisanym oprogramowaniem -- obecnie nazywa się OldOpenLibrary(), ma jedynie pierwszy argument.
Wartość zwróconą przez OpenLibrary() należy zapamiętać w zmiennej wskaźnikowej o ściśle określonej nazwie (np. IntuitionBase dla "intuition.library", ReqToolsBase dla "reqtools.library" itd.), ponieważ zmienna ta jest wykorzystywana przy wywołaniach funkcji z danej biblioteki.

Dlaczego zamykamy biblioteki?

Kiedy otwarta przez nas biblioteka nie jest nam już dłużej potrzebna, czyli zwykle pod koniec programu, należy ją zamknąć. Służy do tego funkcja Execa:

void CloseLibrary( struct Library *library );

Jako jej parametr należy podać wartość zwróconą przez OpenLibrary(), czyli wskaźnik na strukturę "Library".
Początkujący programiści często mają wątpliwości, czy zamykanie bibliotek rzeczywiście jest potrzebne. Faktem jest, że jeżeli się biblioteki nie zamknie, to nie będzie jakiejś strasznej katastrofy (nie dojdzie do zawieszenia programu), jednak porządny program ZAWSZE powinien zostawiać po sobie porządek -- jeżeli się coś (bibliotekę, czcionkę, plik, okno itd.) otworzyło (bądź utworzyło), to należy to coś zamknąć, aby zapewnić sprawne działanie systemu, umożliwić zwrot przydzielonej przy otwieraniu pamięci.
Proponujemy przećwiczyć otwieranie i zamykanie bibliotek na przykładzie

Listing nr 1

W tym programiku otwieramy biblioteki, ich adresy przechowujemy w zmiennych o odpowiednich nazwach, po czym zamykamy biblioteki. Nie korzystamy jeszcze z żadnych funkcji zawartych w bibliotekach (nie wszystko naraz).
Tym, którzy dopiero zaczynają programować w C, należy się drobne wyjaśnienie odnośnie jednej z linii programu:

if (GfxBase=(struct GfxBase*)OpenLibrary("graphics.library", 0))

Język C jest dosyć elastyczny i pozwala na jednoczesne przypisanie wartości i sprawdzenie, czy wartość ta jest różna od zera. Z takimi językowymi idiomami będziemy się często spotykać; dla "ułatwienia" będą jeszcze wzbogacone o znak "!", czyli po prostu negację (UWAGA: niektóre kompilatory mogą wygenerować ostrzeżenie o możliwości wystąpienia błędu w wypadku takiej formy, jest ona jednak w pełni poprawna, a ostrzeżenia są po to, by odszukać literówki -- '=' zamiast '=='). Podobnie należałoby przypomnieć, co oznacza dziwoląg umieszczony poniżej, ale tym zajmiemy się za chwilę.

IntuitionBase=(struct IntuitionBase*)OpenLibrary...

Struktura opisująca bibliotekę

Jak już mówiliśmy, podczas otwierania biblioteki otrzymujemy wskaźnik na strukturę "Library". Znajomość tej struktury nie jest może niezbędna przy pisaniu programów, zawsze jednak warto wiedzieć dokładnie, "co w trawie piszczy". Jednym z jej pól jest "lib_OpenCnt", w tym polu jest zapisana liczba użytkowników korzystających z biblioteki w danej chwili. Jeli biblioteka straci wszystkich użytkowników (pole lib_OpenCnt == 0), to może zostać usunięta z pamięci i tak się stanie, ale dopiero w wypadku braków pamięci. Jeli problemy braku pamięci nie wystąpią, biblioteka będzie pozostawać w pamięci, pomimo iż nikt z niej nie korzysta (czeka na lepsze czasy). PrzejdŹmy do pól "lib_Version" i "lib_Revision". W polach tych zapisany jest numer wersji biblioteki. Właśnie po tych polach można sprawdzić, z jaką wersją systemu pracujemy.

Listing nr 2

W kolejnych programach będziemy korzystać z funkcji check_os(), której dla oszczędności miejsca nie będziemy za każdym razem przepisywać, więc Czytelnik będzie zmuszony dołączać ją (oraz definicje stałych "OS_xx") do kolejnych programów. Dwa słowa odnośnie UWORD w deklaracji funkcji check_os() -- jest to nic innego jak "unsigned short int". Analogicznie ULONG oznacza "unsigned long int", a UBYTE -- "unsigned char".
W tym krótkim programiku pokazaliśmy, jak można wykorzystać zmienną "SysBase", która jest wskaźnikiem na strukturę "ExecBase" (wartoć tej zmiennej nadaje dołączony podczas linkowania programu moduł startowy, pobierając ją z komórki pamięci pod adresem 0x00000004 -- ta informacja jest ważna raczej dla programujących w asemblerze). Zawartoć struktury "ExecBase" można poznać analizując systemowe inkludy, a ściślej mówiąc, plik "exec/execbase.h". Pierwszym polem tej struktury jest "LibNode" typu "Library", co oznacza, że struktura ta jest rozbudowaną wersją struktury "Library", dzięki czemu możemy swobodnie zastosować rzutowanie typów (ang. casting), z którym spotkalimy się już w pierwszym przykładzie podczas przypisywania wartości zmiennym "IntuitionBase" i "GfxBase". Poza polem "LibNode" struktura "ExecBase" zawiera pewne specyficzne dla niej dane, takie jak np. adres obecnie wykonywanego zadania ("ThisTask"), typ procesora ("AttnFlags") oraz wiele innych, często prywatnych, informacji (tj. takich, których aplikacje nie powinny wykorzystywać).
Do danych zawartych w polu LibNode struktury "ExecBase" możemy odwoływać się na dwa różne sposoby:

ver=SysBase->LibNode.lib_Version;
ver=((struct Library*)SysBase)->lib_Version;

Analogicznie możemy postępować z pozostałymi rozbudowanymi strukturami, opisującymi biblioteki:

opc=IntuitionBase->LibNode.lib_OpenCnt;
opc=((struct Library*)IntuitionBase)->lib_OpenCnt;

Odwołania te wskazują dokładnie na tę samą zmienną, obie formy są poprawne.
Powróćmy do struktury "ExecBase". Zapoznajmy się z kolejnym przykładem.

Listing nr 3

Pole "ThisTask", jak już wspomnieliśmy, zawiera adres struktury "Task", w której znajdują się informacje o wykonywanym w danej chwili zadaniu, a więc naszym programie. Informacji tych jest cała masa (patrz "exec/tasks.h"), my pobieramy tylko te o nazwie procesu (będzie to najprawdopodobniej "Shell Process", choć mogą się zdarzyć i inne) oraz jego priorytecie (niemal zawsze 0).
Komentarza może wymagać użyty w nim "for". Stałe symboliczne "AFB_68xxx" są zdefiniowane w pliku "exec/execbase.h". Stała AFB_68010 ma wartość 0, AFB_68020 to 1 itd. Dane zawarte w polu "AttnFlags" są zapisane w formie maski bitowej; gdy jest obecny dany procesor, to bit o numerze "AFB_68xxx" jest ustawiony, ustawione są jednak również niższe bity, tyczące się starszych procesorów -- z tego powodu zastosowaliśmy pętlę z licznikiem malejącym, zaczynającą sprawdzanie od najnowszych procesorów. Warunek "licznik==-1" będzie spełniony dla procesora MC68000, który nie jest odnotowany w "AttnFlags". Przykład ten wyświetli nieprawdziwe informacje dla procesora MC68060 (potraktuje go najprawdopodobniej jako MC68040), jako że odpowiedniej stałej brakuje w pliku "exec/execbase.h". Warto w tym momencie wspomnieć o różnicy pomiędzy stałymi "AFB_68xxx" i "AFF_68xxx": stałe "B" oznaczają NUMER bitu, a stałe "F" są gotową MASKĄ bitową (można by nieformalnie napisać: "F"=1<<"B"). Radzimy o tym pamiętać, ponieważ jest to ogólnie używana konwencja, a pomylenie jednych stałych z drugimi staje się przyczyną błędnego działania programu.
Na tym kończymy dzisiejszy odcinek i zapraszamy do lektury następnej części kursu. A za miesiąc bierzemy się do okien, zajmiemy się również informacjami pochodzącymi od okna.