• Kurs MUI - część 1

20.02.2005 14:34, autor artykułu: Grzegorz Kraszewski
odsłon: 5363, powiększ obrazki, wersja do wydruku,

MUI - krok w krainę obiektów

Jak wiadomo wykonanie ładnego interfejsu użytkownika do własnego programu nie jest zadaniem łatwym. Dlatego powstało wiele bibliotek mających na celu ułatwienie programiście tej pracy. Niewątpliwie najlepszą z nich jest Magic User Interface. Wiele już napisano jakie korzyści daje MUI użytkownikowi. Jeden i ten sam program korzystający z MUI może wyglądać zupełnie inaczej na dwóch Amigach. Nieważne czy masz Workbench 640x256 w czterech kolorach, czy 1024x768 TrueColor. Dzięki MUI na obu tych ekranach program będzie wyglądał doskonale. Porównajmy to sobie z nudną identycznością wyglądu Windowsów... O tym pisano już wiele. Jednak sposoby pisania programów z wykorzystaniem MUI są owiane mgiełką tajemnicy i atmosferą "wyższej czarnej magii". Pisząc o tym mam zamiar tę tajemnicę rozwiać.

Programowanie "pod MUI" nie jest bowiem wcale trudniejsze niż bez niego, a po nabraniu pewnej wprawy jest o wiele prostsze. Zasadnicza trudność polega na zmianie podejścia do programowania. Do tej pory przyzwyczailiśmy się, że cała sztuka pisania programu polega na napisaniu procedur lub, jak kto woli, funkcji. Z funkcji składamy do kupy nasz program dodając po drodze jakieś potrzebne struktury danych. Jest to tzw. proceduralny styl programowania. Inny sposób to zaczęcie od danych, a potem dopisanie do nich potrzebnych funkcji. Naukowcy nazwali to stylem abstrakcji danych. Niezależnie jednak którą drogę wybierzemy, jeżeli tylko problem będzie odpowiednio złożony, dostaniemy w rezultacie niezły bałagan. Zarówno bowiem nasze dane jak i procedury będą tworzyć wielką masę w której znalezienie czegokolwiek, nie mówiąc już o wprowadzeniu zmian, stanie się niewykonalne. Jedynym wyjściem pozostaje programowanie obiektowe.

Cóż to takiego jest? Wbrew pozorom sprawa jest bardzo prosta. Załóżmy, że mamy napisać program obsługujący bibliotekę (taką zwykłą, z książkami). Od czego zacząć? Co będzie podstawowym obiektem dla takiego programu? Oczywiście książka! Ale to jeszcze nie wszystko. Musimy wiedzieć z czego składa się obiekt. Podstawowe jego elementy to atrybuty i metody. Atrybuty są to po prostu cechy obiektu. Atrybutem książki jest więc tytuł, autor, wydawnictwo, rok wydania, nazwisko wypożyczającego (tak, to też jest cecha książki z biblioteki) i tak dalej. Natomiast metody to czynności które można wykonać na obiekcie. Co bibliotekarka może zrobić z książką? Może ją wypożyczyć, może przyjąć zwrot, może wreszcie kupić nową książkę, albo starą usunąć z biblioteki. I to są właśnie metody obiektu. Zauważmy jedną ciekawą rzecz. Budowa naszego programu jest odzwierciedleniem rzeczywistości, nasz program będzie jak gdyby modelem świata rzeczywistego. Dzięki temu łatwiej jest taki program pisać, poprawiać i ulepszać. Po prostu lepiej go rozumiemy. I to jest właśnie idea programowania obiektowego.

Obiekty mogą być różne. Zupełnie czymś innym jest obiekt "książka" z programu obsługującego bibliotekę, a czym innym obiekt "cel naziemny" z symulatora lotu. Fachowo to nazywając możemy powiedzieć, że obiekty te są różnych klas. Różne klasy mogą występować w tym samym programie. W bibliotece mamy przecież również czasopisma, które są jednak czymś innym niż książki. I to właśnie klasa decyduje o tym czym jest obiekt. W klasie opisane są jego atrybuty i metody. Pisanie programu metodą obiektową zaczynamy od ustalenia klas, a następnie opisania ich atrybutów i metod. Gdy już mamy klasę możemy stworzyć dowolną ilość obiektów tej klasy. Są również takie klasy, które mają tylko jeden obiekt. W naszym programie bibliotekarskim możemy mieć klasę "biblioteka" i będziemy mieli tylko jeden obiekt tej klasy. Przy czym nie dlatego, że nie da się tego zaprogramować, ale dlatego, że w rzeczywistości mamy do czynienia z jedną biblioteką. Kolejny raz widzimy tu cechę programowania obiektowego: program odzwierciedla rzeczywistość.

Jeszcze jedną zaletą programowania obiektowego jest "ukrycie" danych obiektu przed funkcjami z zewnątrz. Pisząc program tradycyjnie często przez pomyłkę wywołujemy procedurę z błędnymi parametrami, przez co działa ona na "nie swoich" danych. Do danych obiektu mamy dostęp jedynie przez atrybuty i metody jego klasy. Jeżeli więc przez pomyłkę spróbujemy zadziałać metodami z innej klasy to nie będzie żadnego efektu, po prostu nie stanie się nic. Program nie zadziała jak należy, ale nie powiesi nam komputera. Przy testowaniu to wielkie ułatwienie. Takie ukrycie danych pomaga też zachować porządek w programie i ułatwia jego rozwijanie. Możemy na przykład wymienić kod jednej z klas na nowy. Jeżeli tylko zachowamy te same metody i atrybuty w innych klasach nie musimy nic zmieniać.

Kolejnym ważnym zagadnieniem jest rzecz mądrze zwana dziedziczeniem. Żeby nie zanudzać przykładami bibliotekarskimi tym razem coś z motoryzacji. Załóżmy, że piszemy program symulujący ruch drogowy w mieście. Oczywiście niezbędny jest obiekt "pojazd", w końcu ruch robią pojazdy. Ale to jeszcze mało. Zupełnie czym innym jest jadący ulicą maluch, a czym innym zasuwający pasem TIR z naczepą. Jeszcze inaczej wpłynie na ruch jadąca na sygnale karetka pogotowia. Wychodziłoby więc na to, że do każdego rodzaju pojazdu trzeba napisać oddzielną klasę. Trochę szkoda naszej pracy, bo jednak wiele jest atrybutów i cech wspólnych pojazdów. Każdy charakteryzuje się jakąś szybkością maksymalną, przyspieszeniem i innymi cechami. Najwygodniej byłoby, gdyby te atrybuty i metody, które są wspólne można było napisać raz i wszystkie obiekty klasy "pojazd" z nich by korzystały, a te metody które są różne możnaby napisać oddzielnie dla każdego rodzaju pojazdów. Czy to jest możliwe? Tak, i bardzo szeroko wykorzystywane. Wszystkie klasy opisujące różne rodzaje pojazdów tworzymy jako klasy podrzędne (inaczej zwane klasami potomnymi) klasy "pojazd". Dzięki temu wszystkie metody i atrybuty klasy "pojazd" są przejmowane, czyli dziedziczone przez klasę "TIR", czy "karetka pogotowia". Do klasy potomnej możemy dodać nowe metody i atrybuty. W ogólności pojazdy nie mogą przejeżdżać przez skrzyżowanie na czerwonym świetle, karetki jednak mogą. Nie ma sprawy, dopisujemy nową metodę i po kłopocie. Jeżeli mamy dwie różne klasy, które mają cechy wspólne, zawsze warto stworzyć klasę nadrzędną zawierającą wspólne atrybuty i metody, a te dwie uczynić jej klasami potomnymi. Program będzie krótszy i łatwiejszy w uruchamianiu.

W porządku: klasy, obiekty, wszystko pięknie. Tylko że trzeba mieć obiektowy język programowania albo tą całą obiektowość zaprogramować ręcznie, wtedy program zamiast być krótszy będzie dłuższy. Na szczęście twórcy systemu operacyjnego Amigi myśleli przyszłościowo i już osiem lat temu tworząc wersję 2.0 AmigaOS umieścili tam cały system obsługi obiektów. System ten oznaczony jest skrótem BOOPSI (Basic Object Oriented Programming System for Intuition), czyli w tłumaczeniu na swojski podstawowy system programowania obiektowego dla Intuition (chodzi o bibliotekę intuition.library). Nazwa trochę myląca, bo system ten można wykorzystać do klas dowolnego typu, niekoniecznie związanych z gadżetami i okienkami. Dzięki BOOPSI możemy programować obiektowo w każdym języku np. asemblerze, a nawet Amosie (!) - tego ostatniego jednak bym nie polecał.

BOOPSI załatwia wszystkie problemy związane z programowaniem obiektowym: tworzenie klas, obiektów, obsługę dziedziczenia, ukrywanie danych i rezerwowanie na nie pamięci. Dostarcza także klasę podstawową, z której możemy sobie wyprowadzić wszystkie potrzebne nam w programie klasy. Oczywiście niekoniecznie bezpośrednio, możemy z klasy podstawowej wyprowadzić jakąś jedną, z tej z kolei następną i tak dalej. Powstanie wtedy tak zwane drzewo klas. Jego korzeniem będzie klasa podstawowa, logiczne więc, że nazywa się "rootclass". Jest ona maksymalnie uniwersalna, nie posiada żadnych atrybutów i tylko kilka metod. Posuwając się od korzenia drzewa w stronę gałęzi napotkamy klasy coraz bardziej wyspecjalizowane i konkretne. Klasa "rootclass" posiada jedynie kilka metod. Są to:

OM_NEW - jest to metoda, która służy do stworzenia nowego obiektu,
OM_DISPOSE - ta metoda likwiduje obiekt,
OM_GET - tą metodą odczytujemy wartość dowolnego atrybutu,
OM_SET - a tą metodą możemy atrybutowi nadać wartość.

Podstawową funkcją do wykonywania metod na obiektach jest DoMethodA (). Niestety funkcja ta (i kilka innych) nie znajduje się w ROM-ie Amigi, ale w bibliotece amiga.lib. Jest to tak zwana biblioteka linkowana, to znaczy że funkcje w niej zawarte są dołączane do kodu programu przez kompilator. Na szczęście DoMethodA () jest bardzo krótka. Bibliotekę amiga.lib posiada większość najbardziej rozpowszechnionych kompilatorów. Parametry funkcji DoMethodA () to adres obiektu na którym metoda ma zostać wykonana i tajemnicza struktura Msg (a dokładnie jej adres). W tej strukturze zawarty jest identyfikator metody i jej parametry. Każda metoda może posiadać inne parametry, więc struktura Msg nie jest ściśle zdefiniowana. Ważne jest to, że pierwszym polem tej struktury musi być identyfikator metody. Jak wszystkie funkcje z końcówką "A" także i DoMethodA () posiada swój odpowiednik w którym strukturę Msg można zbudować na stosie podając kolejne jej pola jako parametry funkcji (tylko w językach wysokopoziomowych, w asemblerze mamy jedynie DoMethodA ()). Oto krótki przykład:

struct ParametryMojejMetody
 {
  ULONG MethodID;	/* to pole jest obowiązkowe */
  long parametr1
  STRPTR parametr2;	/* i tak dalej */
 }
struct ParametryMojejMetody param = {MOJA_METODA,737282,"Tekst"};
DoMethodA (jakiś_obiekt,¶m);


Cały ten kod źródłowy możemy zastąpić jedną linijką:

DoMethod (jakiś_obiekt,MOJA_METODA,737282,"Tekst");


Również cztery podstawowe metody każdej klasy, opisane wyżej (ma je klasa "rootclass", a więc każda inna dziedziczy po niej te metody) można wywołać w ten sposób. Jednak ze względu na ich powszechność użycia istnieją dla nich specjalne funkcje.

Metodę OM_NEW wykonujemy funkcją NewObjectA (). Ponieważ obiekt na którym wykonujemy tą metodę jest dopiero tworzony, więc to zrozumiałe, że nie mamy jeszcze jego adresu. Za to funkcja NewObjectA () pozwala na podanie klasy do jakiej ma należeć obiekt. Są tutaj dwie możliwości. Jeżeli klasa jest publiczna i posiada nazwę (np. "rootclass") to podajemy nazwę, a adres klasy podajemy pusty (NULL). Jeżeli klasa jest prywatna, to ma adres a nie ma nazwy, wtedy NULL podajemy zamiast nazwy. Następnie podajemy parametry metody OM_NEW. Parametrem jest adres tablicy tagów zawierających atrybuty tworzonego obiektu. Wynikiem funkcji jest adres tego obiektu. Jeżeli funkcja zwróci zero, to znaczy, że stworzenie obiektu nie udało się. Oto przykłady:

Class *JakaśKlasa;
Object *obiekt1;
struct TagItem atrybuty[] = {ATR_Waga,200,ATR_Kolor,"zielony",TAG_END,0};
/* klasa prywatna */
obiekt1 = NewObjectA (JakaśKlasa,NULL,(struct TagItem*)&atrybuty);
/* klasa publiczna */
obiekt1 = NewObjectA (NULL,"image.mui",(struct TagItem*)&atrybuty);


Jak dla wszystkich funkcji z "A" na końcu możemy użyć wygodnej w językach wysokiego poziomu funkcji bez "A":

obiekt1 = NewObject (JakaśKlasa,NULL,ATR_Waga,200,ATR_Kolor,"zielony",TAG_END);


do usunięcia obiektu, czyli wykonania na nim metody OM_DISPOSE służy funkcja DisposeObject (). Jej jedynym parametrem jest adres usuwanego obiektu:

DisposeObject (obiekt1);


Atrybuty obiektu można ustawiać i zmieniać funkcją SetAttrsA (). Wykonuje ona na obiekcie metodę OM_SET:

struct TagItem nowe_atrybuty[] = {ATR_Waga,100,ATR_Kolor,"czerwony",TAG_END,0};
SetAttrsA (obiekt1,(struct TagItem*)&nowe_atrybuty);


Lub w wersji z budowaniem listy atrybutów na stosie (bez "A"):

SetAttrs (obiekt1,ATR_Waga,100,ATR_Kolor,"czerwony",TAG_END);


Przy NewObject () i SetAttrs () nie wolno zapomnieć o TAG_END kończącym listę atrybutów. Jego pominięcie spowoduje, że atrybuty będą czytane aż do napotkania tagu zerowego (czyli właśnie TAG_END). Jeżeli będziemy mieli pecha to może to trwać dość długo, a nawet zawiesić komputer. Poza tym możemy akurat trafić na identyfikator któregoś z atrybutów i funkcja ustawi nam w obiekcie jakieś bzdurne wartości.

Ostatnia z czterech funkcji to GetAttr (). Służy ona do pobrania wartości atrybutu z obiektu, a więc wykonuje metodę OM_GET. Trochę nietypowy jest sposób jej działania. Wartość atrybutu nie jest zwracana jako wynik funkcji, a zapisywana w podanym przez nas miejscu pamięci. Jako to miejsce najwygodniej podać adres jakiejś zmiennej. Popatrzmy na przykład:

ULONG waga;
STRPTR kolor;
GetAttr (ATR_Waga,obiekt1,&waga);
GetAttr (ATR_Kolor,obiekt1,(ULONG*)&kolor);
printf ("Waga obiektu 1 to %ld kg, kolor %sn",waga,kolor);


Pozostało nam jeszcze omówienie sposobu tworzenia własnych klas. Jest to dość proste. Przed utworzeniem klasy musimy zdefiniować dwa jej składniki. Pierwszym jest struktura danych obiektu. Każdy obiekt klasy będzie posiadał taką strukturę. Pamięć na nią jest przydzielana i zwalniana automatycznie przez BOOPSI. W strukturze tej trzyma się atrybuty i inne dane (mogą być niedostępne z zewnątrz) dotyczące obiektu. Oto przykład takiej struktury:

struct DaneObiektu
 {
  ULONG waga;
  STRPTR kolor;
  APTR bufor_pamięci;
 }


Aby uzyskać dostęp do tej struktury wewnątrz metod klasy używamy makra

INST_DATA zdefiniowanego w :
struct DaneObiektu *dane = INST_DATA(cl,obj);


Dzięki temu dostęp do danych obiektu jest możliwy tylko poprzez metody. W ten sposób realizowane jest ukrywanie danych, o którym wspomniałem wcześniej. Drugim niezbędnym elementem klasy jest tak zwany dispatcher. Jest to po prostu tablica skoków dla metod naszej funkcji. W zależności od wartości pola msg->MethodID w strukturze Msg skaczemy do odpowiedniej metody. Tak wygląda typowy dispatcher:

__asm ULONG JakiśDispatcher (register __a0 Class *cl,register __a2 Object
            *obj,register __a1 Msg msg)
 {
  switch (msg->MethodID)
   {
    case OM_NEW:	return (MetodaNew (cl,obj,msg));
    case OM_SET:	return (MetodaSet (cl,obj,msg));
    case MOJA_METODA:	return (MojaMetoda (cl,obj,msg));
    /* i tak dalej... */
    default:		return (DoSuperMethodA (cl,obj,msg));
   }
 }


Jak widać parametry do dispatchera są przekazywane przez rejestry. Jeżeli dany kompilator nie obsługuje tego, trzeba będzie sie posłużyć małą wstawką asemblerową. Jednak większość dobrych języków sobie z tym radzi. Warto zauważyć, że dispatcher jest szczególnym przypadkiem hooka (to była uwaga dla zorientowanych). Jeżeli klasa zawiera mało metod i są one krótkie, ich kod można umieścić bezpośrednio w dispatcherze. W przypadku, gdy nasza klasa nie ma danej metody, wtedy jest ona przekazywana do klasy nadrzędnej przy pomocy funkcji DoSuperMethodA () z amiga.lib. W ten sposób realizowane jest dziedziczenie metod. Po przygotowaniu struktury danych i dispatchera możemy już stworzyć klasę funkcją MakeClass ():

Class *MojaKlasa;
MojaKlasa = MakeClass (NULL,"rootclass",NULL,sizeof(struct DaneObiektu),0);


Pierwszy parametr to identyfikator klasy, który w przypadku klasy prywatnej nas nie interesuje. Drugi to identyfikator pubilcznej klasy nadrzędnej, trzeci to adres prywatnej klasy nadrzędnej. Używamy ich zamiennie podobnie jak w NewObject (). Następnie podajemy rozmiar struktury danych obiektu, niezbędny do automatycznego rezerwowania pamięci i na koniec flagi, w obecnych wersjach systemu zawsze zero. Następną czynnością (po sprawdzeniu czy czasem nie dostaliśmy adresu zero, co oznacza, że wystąpił błąd) jest podłączenie dispatchera do klasy:

MojaKlasa->cl_Dispatcher.h_Entry = HOOKFUNC (JakiśDispatcher);


Makro HOOKFUNC zdefiniowane w zajmuje się bezproblemowym rzutowaniem typu funkcji, z czym w języku C bywają czasem małe problemy. Teraz nasza klasa jest już gotowa do użytku. Przy zakończeniu programu i (uwaga!) zlikwidowaniu funkcją DisposeObject () wszystkich obiektów klasy, można ją również zlikwidować funkcją FreeClass:

FreeClass (MojaKlasa);


MUI To byłaby podstawowa dawka wiedzy na temat BOOPSI i programowania obiektowego. Jednak jak łatwo zauważyć artykuł miał być o MUI, a tymczasem nie było jeszcze o nim ani słowa. Czym właściwie jest MUI dla programisty? Gdy piszemy program potrzebne są nam nie tylko klasy reprezentujące problem rozwiązywany przez nasz program. Potrzebujemy też klas do stworzenia graficznego interfejsu użytkownika, klas obsługujących okienka, gadżety, przyciski, suwaki i tak dalej. Gdyby takie klasy trzeba było pisać samemu to zysk z użycia programowania obiektowego byłby (zwłaszcza przy niewielkich programach) żaden. Ale właśnie w MUI znajdziemy zestaw gotowych do użycia, bardzo dobrze napisanych i wszechstronnych klas związanych z interfejsem użytkownika. Mamy więc okna, przyciski, suwaki, inne ciekawe elementy na przykład skale procentowe, wskaźniki wychyłowe, grupy wirtualne i wiele innych niespotykanych nawet w "profesjonalnych" systemach rzeczy. O tym wszystkim będę pisał w kolejnych odcinkach tego cyklu. A w odcinku następnym spróbujemy napisać jakiś prosty program "pod MUI". Klikając tutaj znajdziecie program z kodem źródłowym w C pokazujący wszystkie opisane w tym odcinku zagadnienia.

 głosów: 1   
komentarzy: 3ostatni: 14.10.2009 12:21
Na stronie www.PPA.pl, podobnie jak na wielu innych stronach internetowych, wykorzystywane są tzw. cookies (ciasteczka). Służą ona m.in. do tego, aby zalogować się na swoje konto, czy brać udział w ankietach. Ze względu na nowe regulacje prawne jesteśmy zobowiązani do poinformowania Cię o tym w wyraźniejszy niż dotychczas sposób. Dalsze korzystanie z naszej strony bez zmiany ustawień przeglądarki internetowej będzie oznaczać, że zgadzasz się na ich wykorzystywanie.
OK, rozumiem