W poprzednim, pierwszym odcinku cyklu na temat MUI opisałem podstawy BOOPSI. Ponieważ MUI jest oparte na BOOPSI i jest jego rozszerzeniem, było to niezbędne dla zrozumienia tego, o czym piszę w tym i następnych odcinkach.
MUI to hierarchiczna struktura klas przeznaczonych do tworzenia graficznego interfejsu użytkownika. Wszystkie klasy są wyprowadzone ze wspomnianej w poprzednim odcinku klasy bazowej "rootclass". Klasy MUI nie są jednak zwykłymi klasami BOOPSI, są one nieco bardziej rozbudowane. Co z tego wynika dla nas? Klasy i obiekty MUI tworzymy nie funkcjami z intuition.library (takimi jak MakeClass(), czy NewObjectA()), ale specjalnie do tego celu przeznaczonymi funkcjami z biblioteki muimaster.library. Do stworzenia własnej klasy MUI służy funkcja:
MUI_CreateCustomClass (baza, nazwa_klasy, adres_klasy, rozmiar_danych, dispatcher)
Funkcja jest podobna do MakeClass() niemniej występują tu pewne różnice. Parametr "baza" jest używany jedynie w wypadku, gdy piszemy klasę zewnętrzną. Takie klasy są bibliotekami umieszczonymi w katalogu MUI:Libs/MUI i posiadają rozszerzenie .mcc. Na razie jednak nie będziemy się tym zajmować i przy zwykłych klasach wstawiamy tam po prostu zero. Nazwa i adres klasy w parametrach funkcji pełnią tę samą zamienną rolę co w MakeClass(). Jeżeli tworzymy klasę z klasy publicznej, np. którejś z wbudowanych klas MUI - podajemy nazwę, jeżeli z klasy prywatnej - podajemy adres. Nieużywany w danej chwili parametr ustawiamy na zero. Rozmiar danych to oczywiście rozmiar struktury danych tworzonej automatycznie dla każdego obiektu klasy. Wreszcie parametr "dispatcher" to adres funkcji dispatchera. Tym razem nie musimy go podłączać ręcznie, MUI_CreateCustomClass() zrobi to za nas. Wynikiem funkcji jest adres klasy, nie jest to jednak adres struktury IClass, klasy MUI posiadają własną strukturę MUI_CustomClass. Jednym z jej elementów jest wskaźnik na IClass. Do usunięcia klasy MUI służy funkcja:
MUI_DeleteCustomClass (klasa)
Jak łatwo się domyślić jako parametr podajemy adres klasy zwrócony przez funkcję MUI_CreateCustomClass().
Do tworzenia obiektów MUI używamy dwóch funkcji. Jeżeli obiekt jest klasy wbudowanej (np. MUIC_Window) posługujemy się funkcją z muimaster.library:
MUI_NewObjectA (nazwa_klasy, lista atrybutów)
Lista atrybutów to wskaźnik na tablicę struktur TagItem, czyli par identyfikator - wartość. Oczywiście istnieje wersja MUI_NewObject(), gdzie wszystkie tagi podajemy jako kolejne parametry, nie zapominając o TAG_END na końcu. Jeżeli natomiast tworzymy obiekt klasy prywatnej (czyli stworzonej przez nas) posługujemy się normalnie funkcją NewObjectA() z intuition.
NewObjectA (adres_klasy->mcc_Class, NULL, lista_atrybutów)
NewObjectA() oczekuje jako adresu klasy wskaźnika na strukturę IClass, pobieramy go więc ze struktury MUI_CustomClass.
Podobna dwoistość funkcji występuje przy likwidowaniu obiektów. Obiekty klas wbudowanych unicestwiamy funkcją MUI_DisposeObject() z muimaster.library, a obiekty klas prywatnych tradycyjnie przy pomocy DisposeObject() z intuition. Jedynym parametrem obu tych funkcji jest adres obiektu do usunięcia. Komunikacja z obiektami natomiast, a więc funkcje SetAttrsA(), GetAttr() i DoMethodA() pozostają niezmienione w stosunku do BOOPSI.
Skoro wyjaśnienie tych drobnych różnic w zarządzaniu obiektami mamy już za sobą możemy zająć się hierarchią klas wbudowanych MUI.
W ostatnich wersjach MUI klas wbudowanych mamy sporo. Nie będę tu opisywał wszystkich, skoncentruję się jedynie na tych, które tworzą szkielet każdego programu. W pliku nagłówkowym "mui.h" znajdującym się w pakiecie MUI dla programistów, pokazane jest drzewko hierarchii klas. Jest to bardzo ważny dla nas rysunek, ponieważ pokazuje w jaki sposób kolejne klasy są stworzone z nadrzędnych. Rysunek ten jest wskazówką, w jaki sposób możemy skorzystać z dziedziczenia metod i atrybutów. Prosty przykład - chcemy ustawić tło dla gadżetu tekstowego. Niestety przeszukanie dokumentacji do klasy MUIC_String ujawni, że nie ma takiego atrybutu. Cóż więc robimy? Jak widać w drzewku MUIC_String jest klasą potomną klasy MUIC_Area, która posiada atrybut MUIA_Background. Ustawiamy, kompilujemy, odpalamy - pełny sukces.
Na szczycie drzewa znajduje się klasa MUIC_Notify. Jej głównym zadaniem jest automatyczna komunikacja między obiektami. Jest to klasa nadrzędna dla wszystkich innych, co oznacza, że komunikować się mogą między sobą wszystkie obiekty. Komunikacja ta, zwana notyfikacją, polega na tym, że określona zmiana jakiegoś atrybutu w obiekcie "nadajniku" powoduje wykonanie jakiejś metody na obiekcie "odbiorniku". Na przykład wciśnięcie przycisku powoduje otwarcie okna, a wpisanie czegoś do gadżetu tekstowego powoduje np. dołączenie tego tekstu do listy w sąsiednim gadżecie. Idealny program w MUI wszelkie reakcje na działania użytkownika wykonuje właśnie poprzez notyfikacje ustawione na początku, a później jedynie oczekuje w pętli na wyjście z programu.
Bardzo ważna klasa (choć często niedoceniana) to MUIC_Family. Jest to klasa grupująca, a więc służy do grupowania obiektów. Jest ona oparta na liście dwukierunkowej znanej z exec.library i metody tej klasy są podobne do funkcji obsługujących listy. Jak widać z drzewa klas, jedynymi klasami wbudowanymi opartymi na MUIC_Family są klasy obsługujące systemowe menu. Jednak po pierwsze MUIC_Family nie jest jedyną klasą grupującą w MUI, a po drugie może być przydatna do grupowania obiektów własnych klas, np. w bazie danych. Grupowanie jest w MUI bardzo szeroko wykorzystywane. Wykonanie metody na obiekcie klasy grupującej powoduje wykonanie jej na wszystkich obiektach do niej należących. Na przykład aby usunąć wszystkie obiekty przy wychodzeniu z programu wystarczy usunąć główny obiekt - aplikację klasy MUIC_Application (o której niżej). Wszystkie obiekty podrzędne zostaną usunięte automatycznie, zwalniając zajętą przez siebie pamięć i inne zasoby. Oczywiście grupowanie może być wielopoziomowe, czyli możemy mieć grupę obiektów należącą do grupy, która należy do innej grupy i tak dalej, jedyne ograniczenia to ilość dostępnej pamięci i poczucie zdrowego rozsądku.
Kolejna bardzo ważna klasa to MUIC_Application. Opisuje ona cały nasz program, a więc zwykle tworzymy tylko jeden obiekt tej klasy. MUIC_Application odpowiada za współpracę naszego programu z systemem, obsługuje więc commodity, AppIcon i AppWindow, port Arexxa, system pomocy, komunikację z oknami przez porty IDCMP, systemowe menu i tym podobne sprawy. Dzięki tej klasie automatycznie mamy rozwiązane wszystkie powyższe problemy. Obiekt klasy MUIC_Application jest obiektem grupującym, to znaczy, że posiada obiekty podrzędne (nie mylić z klasami podrzędnymi). Do aplikacji należą (bezpośrednio lub pośrednio) wszystkie obiekty w programie, w szczególności menu systemowe (obiekt klasy MUIC_Menustrip) i dowolna ilość okien (obiektów klasy MUIC_Window). Oczywiście można sobie wyobrazić program nie posiadający żadnego okna.
Skoro doszliśmy już do okien, to naturalnie są to także obiekty MUI, klasy MUIC_Window. Klasa ta opisuje zwykłe systemowe okna rozszerzając jednak ich funkcje, co widać choćby po ilości gadżetów na belce. Okna są jedynym miejscem, gdzie można umieszczać gadżety. Klasa MUIC_Window jest grupująca, a więc do okna należą wszystkie umieszczone w nim gadżety. Do każdego okna możemy też mieć oddzielne menu.
Wszystkie gadżety są oparte na klasie MUIC_Area. Obiekt tej klasy to prostokątny obszar w oknie, z dowolną zawartością. Na poziomie MUIC_Area określamy podstawowe cechy gadżetu, a więc ramkę, tło, czcionkę do napisów (jeżeli jakieś są), sposób reagowania na mysz i wiele innych. Dopiero podklasy wyprowadzone z MUIC_Area różnicują nam gadżety. Z reguły rzadko tworzymy w programie obiekty tej klasy, ale za to biorąc pod uwagę dziedziczenie bardzo często korzystamy z jej atrybutów i metod.
Przy pomocy klasy MUIC_Group gadżety są łączone w grupy. Klasa MUIC_Group odpowiada za wzajemne położenie gadżetów, a więc odpowiednio grupując gadżety (i wszystkie inne obiekty) w grupy i określając atrybuty tych grup projektujemy wygląd naszego programu. Tematowi projektowania wyglądu GUI poświęcę cały następny odcinek cyklu. Łatwo jest bowiem dzięki MUI wypełnić okno gadżetami, ale takie zaprojektowanie GUI aby było czytelne i dobrze wyglądało na dowolnej konfiguracji, jest już sztuką. Co oczywiście nie znaczy, że sztuki tej nie można opanować.
Na zakończenie tej części poświęcę kilka słów współpracy MUI z różnymi kompilatorami. W tej chwili najpopularniejszymi językami do pisania programów współdziałających z systemem Amigi są C oraz E. Małe programy bywają pisane w asemblerze, ale zdecydowanie łatwiej jest pisać w języku wysokopoziomowym, w razie potrzeby jedynie podpierając się wstawkami asemblerowymi. Samo MUI zostało napisane w C, więc naturalnym jest, że z tym językiem najlepiej współpracuje. Jedynym w zasadzie problemem na jaki możemy się natknąć, są dispatchery klas i hooki. Od zwykłych funkcji C różnią się one tym, że są wywoływane bezpośrednio przez system operacyjny, który umieszcza ich parametry w ściśle określonych rejestrach procesora (konkretnie w A0, A1 i A2). Zwykle kompilator C przekazuje parametry przez stos, jednak większość dobrych kompilatorów pozwala na napisanie funkcji otrzymującej parametry w rejestrach procesora. Oto przykładowa definicja dispatchera w SAS/C:
__asm __saveds JakiśDispatcher (register __a0 struct IClass *cl, register __a2 Object *obj, register __a1 Msg msg);
Słowo kluczowe __asm oznacza, że funkcja otrzymuje parametry w rejestrach procesora, a nie na stosie. Słowo __saveds zapewnia przechowanie rejestru A4 używanego jako wskaźnik do danych lokalnych. Dispatcher jest bowiem w pewnym sensie częścią systemu operacyjnego i część jego kodu znajduje się de facto w ROM-ie (dokładnie ta wykonywana w czasie DoSuperMethodA()). Nie możemy więc być pewni, że rejestr A4 nie zostanie naruszony.
Nieco więcej problemów napotkają programiści piszący w języku E. Pierwsze różnice wynikają z wymagań jakie język E narzuca na nazwy funkcji i stałych. Jak wiadomo w nazwach funkcji z bibliotek systemowych pierwsza litera musi być duża a druga mała. Dlatego wszystkie funkcje z muimaster.library zaczynają się od "Mui_". Podobnie wszystkie struktury i ich składowe pisane są małymi literami. Nazwy stałych nie zmieniają się. Druga sprawa, to funkcje budujące listę argumentów na stosie, takie jak np. NewObject(). Amiga E nie posiada takich funkcji, ale dość prosto można je zastąpić korzystając z list. Jeżeli na przykład w programie w C napisalibyśmy:
obj = MUI_NewObject (MUIC_Text, MUIA_Text_Contents, "tekst", MUIA_Frame, MUIV_Frame_Text, TAG_END);
W E możemy równie dobrze napisać:
obj := Mui_NewObjectA (MUIC_Text, [MUIA_Text_Contents, 'tekst', MUIA_Frame, MUIV_Frame_Text, 0, 0])
Korzystając z tego, że listy w E mogą zawierać elementy obliczane dynamicznie, możemy stosować wszelkie chwyty związane z budową argumentu funkcji na stosie stosowane w programach w C. Możemy również swobodnie korzystać z makrodefinicji służących do tworzenia obiektów, wszystkie one są zdefiniowane w module 'libraries/mui.e'. Co więcej kod w E będzie nieco szybszy, gdyż adres listy jest już znany w momencie kompilacji i nie trzeba rezerwować pamięci na stosie. Kolejnym naszym problemem będą - podobnie jak w C - dispatchery i hooki. Niestety tu sprawa jest poważniejsza. Język E nie umożliwia przekazywania parametrów do funkcji przez rejestry. Nasuwającym się rozwiązaniem jest zastosowanie wstawek asemblerowych, co jest tym prostsze, że instrukcje asemblera można wstawiać bezpośrednio w kod E. Powstał jednak specjalny pakiet dla programistów piszących pod MUI w E. Jego autorem jest Jan Hendrik Schultz, a najłatwiej znaleźć go na Aminecie. W pakiecie tym znajduje się moduł 'muicustomclass', który zawiera zastępczą funkcję eMui_CreateCustomClass (). Funkcja ta w prawidłowy sposób instaluje dispatchera w klasie, dbając o zachowanie rejestru A4. Do instalacji zwykłych hooków (nie będących dispatcherami klas) najwygodniej skorzystać z funkcji installhook () zdefiniowanej w module 'tools/installhook' dostępnym w standardowej dystrybucji Amiga E. Funkcja ta instaluje zwykłą funkcję E jako hooka zapewniając prawidłowe przekazanie parametrów i obsługę rejestru A4. Jeszcze jedną ciekawostką jest sprawa identyfikatorów TRUE i FALSE. Zarówno w C jak i w E FALSE oznacza 0. Natomiast TRUE w C to 1, a TRUE w E to -1 ($FFFFFFFF). Mogą z tego niekiedy wynikać kłopoty. Dlatego najlepiej podając wartość logiczną TRUE dla MUI używać zdefiniowanej stałej MUI_TRUE równej 1. Kolejną potencjalną przyczyną kłopotów są makra. Aby zostały prawidłowo zinterpretowane, należy włączyć preprocesor E dyrektywą OPT PREPROCESS na początku programu. Ostatnim problemem mogącym czyhać na E-owców jest sprawa związana znowu z budowaniem argumentu funkcji na stosie i z funkcją SetAttrs (). Podczas zmiany atrybutów MUI wykrywa próby ustawienia danego atrybutu ponownie na tą samą wartość i blokuje je przez zamaskowanie odpowiadającego mu taga wartością TAG_IGNORE. W C operacja ta wykonywana jest na kopii danych stworzonej na stosie, a więc oryginał pozostaje nie zmieniony. Przy próbie użycia w E funkcji SetAttrsA () z listą argumentów, MUI wpisuje TAG_IGNORE bezpośrednio do listy, więc przy następnym wywołaniu funkcji atrybut zostanie zwyczajnie pominięty. Receptą na to jest zdefiniowanie funkcji wykonującej kopię atrybutów. Najprościej jest zrobić to dla jednego atrybutu:
PROC set(obj,attr,value) IS SetAttrsA(obj,[attr,value,0])
Funkcja ta jest już zdefiniowana w module 'libraries/mui'. Nieco trudniej jest gdy chcemy podać naraz więcej atrybutów. Można wywołać set() odpowiednią ilość razy, zdefiniować sobie funkcje na 2, 3, itd. atrybuty, albo napisać funkcję tworzącą kopię listy korzystając z List(), ListLen() i ListCopy(). W tym ostatnim przypadku musimy jednak się liczyć z tym, że może zabraknąć pamięci na stworzenie kopii listy.
Na zakończenie kilka słów o programie przykładowym zamieszczonym w dodatku do tego odcinka. Aby pokazać różnicę między klasami MUI i BOOPSI jest to ten sam program co w poprzednim odcinku, ale wszystkie klasy są już klasami MUI. Program nie posiada jeszcze pełnego interfejsu graficznego - tym zajmiemy się w następnej części. Program napisałem w dwóch wersjach - w C i w E, można więc zobaczyć w jaki sposób rozwiązuje się wspomniane wyżej problemy "językowe". Dodatek - tutaj.