• Kurs MUI - część 4

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

Zasady tworzenia programu pod MUI

W tym odcinku zajmę się zasadami tworzenia programów korzystających z MUI. System ten nakłada na nas pewne dodatkowe reguły, których należy przestrzegać, chcąc by program był funkcjonalny i dobrze współpracował z systemem. Jako że MUI jest oparte o programowanie obiektowe, traktuje cały nasz program jako obiekt. Dokładnie polega to na tym, że podstawowym obiektem jaki musimy stworzyć w programie jest obiekt klasy MUIC_Application. W poprzedniej części wspomniałem, że obiekt ten odpowiada za współpracę programu z systemem. Jest on również obiektem, do którego należą wszystkie inne stworzone przez nas obiekty, w szczególności wszystkie okna wraz z zawartością. Dzięki temu upraszcza się zarządzanie pamięcią. Wszelka pamięć (i ewentualnie inne zasoby, takie jak np. pliki na dysku) należy do obiektów, wobec tego usunięcie obiektu aplikacji spowoduje likwidację wszystkich obiektów programu, a co za tym idzie całkowite zwolnienie zajętych przez nasz program zasobów.

Budowanie i niszczenie drzewa obiektów

MUI Ponieważ każdy obiekt w programie ma (a przynajmniej powinien mieć) swój obiekt nadrzędny do którego należy, obiekty są uformowane w drzewo, którego przykład pokazany jest na rysunku. Nie należy mylić tego drzewa z drzewem klas, o którym wspomniałem wcześniej. Te dwa drzewa są wewnątrz połączone inną relacją. W przypadku drzewa klas jest to relacja bycia. Tak więc gadżet tekstowy (MUIC_String) jest obszarem (MUIC_Area) i jest obiektem klasy MUIC_Notify. W drzewie obiektów natomiast obowiązuje relacja przynależności (lub patrząc z góry na dół - posiadania), a więc gadżet należy do grupy, grupa należy do okna, a okno należy do aplikacji. Jedynym obiektem, który do niczego nie należy jest sama aplikacja, będąca korzeniem drzewa.

Kolejność tworzenia obiektów w drzewie jest wstępująca, co oznacza tworzenie drzewa od jego najdrobniejszych "gałązek", grupowanie ich w większe "konary" i sukcesywne posuwanie się w kierunku korzenia. Obiekt aplikacji tworzony jest na końcu. Tego rodzaju taktyka pozwala na bardzo łatwą obsługę błędów. Konstruktor każdej klasy jest napisany w taki sposób, że jeżeli konstrukcja się nie powiedzie z jakiegoś powodu (np. zabraknie pamięci), to zwraca on zero jako wskaźnik obiektu. Klasy grupujące z kolei jeżeli dostaną zero, jako wskaźnik jednego z posiadanych obiektów, najpierw niszczą wszystkie inne posiadane obiekty (te które zostały stworzone do tej pory) i same również zwracają zero. W ten sposób program automatycznie obsłuży każdy błąd jaki wystąpi przy tworzeniu aplikacji. Jeżeli jakikolwiek obiekt gdziekolwiek w drzewie nie zostanie stworzony, to wywoła on "falę niszczenia", która poprzez obiekty klas grupujących rozejdzie się po całym drzewie i dojdzie do obiektu aplikacji. On również nie zostanie stworzony (bo dostanie zero jako np. jedno ze swoich okien). Cała obsługa błędów, jaką musimy wykonać, sprowadza się do sprawdzenia, czy adres obiektu aplikacji jest różny od zera, jeżeli tak, to możemy kontynuować program, jeżeli nie, wszystkie zasoby są już zwolnione. Jedyne co nam pozostało to uraczyć użytkownika odpowiednim komunikatem i wyjść z programu. Na rysunku za pomocą czerwonych i niebieskich strzałek wyjaśniłem przebieg niszczenia obiektów przy błędzie tworzenia jednego z nich, w tym przypadku gadżetu X. Konstruktor tego gadżetu sygnalizuje błąd przez zgłoszenie zerowego wskaźnika gadżetu. Grupa A po odebraniu takiego wskaźnika niszczy grupę C, która z kolei niszczy gadżety Y i Z. Następnie wysyła zerowy wskaźnik do okna 1. Okno 1 likwiduje grupę A i wysyła zerowy wskaźnik do aplikacji, która to niszczy pozostałe obiekty, a na końcu zwraca swój zerowy wskaźnik sygnalizując nam błąd (a jednocześnie fakt zwolnienia wszystkich zajętych zasobów). Niszczenie obiektów odbywa się poprzez wysłanie do nich metody OM_DISPOSE. Falę niszczenia możemy zapoczątkować sami, robimy to przy wyjściu z programu niszcząc obiekt aplikacji funkcją MUI_DisposeObject() a dzięki fali niszczenia usuwając w ten sposób wszystkie obiekty.

Teraz, gdy wiemy już jak zniszczyć to, co w trudzie i znoju będziemy budować, możemy zająć się tym właśnie budowaniem. Oto przykład tworzenia drzewa obiektów z rysunku:

Object *app, *okno1, *okno2, *grupaA, *grupaB, *grupaC, *gadU, *gadW, *gadX,
 *gadY, *gadZ;
 
gadZ = MUI_NewObject (MUIC_String, /* ... */, TAG_END);
gadY = MUI_NewObject (MUIC_Text, /* ... */, TAG_END);
grupaC = MUI_NewObject (MUIC_Group,
 MUIA_Group_Child, gadZ,
 MUIA_Group_Child, gadY,
 TAG_END);
gadX = MUI_NewObject (MUIC_Radio, /* ... */, TAG_END);
grupaA = MUI_NewObject (MUIC_Group,
 MUIA_Group_Child, grupaC,
 MUIA_Group_Child, gadX,
 TAG_END);
okno1 = MUI_NewObject (MUIC_Window,
 MUIA_Window_RootObject, grupaA,
 TAG_END);
gadU = MUI_NewObject (MUIC_Slider, /* ... */, TAG_END);
gadW = MUI_NewObject (MUIC_Image, /* ... */, TAG_END);
grupaB = MUI_NewObject (MUIC_Group,
 MUIA_Group_Child, gadU,
 MUIA_Group_Child, gadW,
 TAG_END);
okno2 = MUI_NewObject (MUIC_Window,
 MUIA_Window_RootObject, grupaB,
 TAG_END);
menu = MUI_NewObject (MUIC_Menustrip, /* ... */, TAG_END);
app = MUI_NewObject (MUIC_Application,
 /* ... */,
 MUIA_Application_Window, okno1,
 MUIA_Application_Window, okno2,
 MUIA_Application_Menustrip, menu,
 TAG_END);


Każdy, kto trochę już próbował programowania z MUI zauważy, że ten styl programowania jest nieelegancki i nieprzejrzysty. Oczywiście tak jest, są dwa sposoby na uproszczenie powyższego kodu. Pierwszy z nich to wykorzystanie możliwości podania wyniku jednej funkcji jako argumentu następnej funkcji. W ten sposób pozbędziemy się wszystkich zmiennych zadeklarowanych na początku (oprócz wskaźnika na obiekt aplikacji), bo adresy wszystkich obiektów będą trzymane przejściowo na stosie. Ten sam kod będzie wyglądał następująco:

app = MUI_NewObject (MUIC_Application,                    /* aplikacja */ 
  MUIA_Application_MenuStrip, MUI_NewObject (MUIC_Menu,   /* menu */
    /* ... */, TAG_END),
  MUIA_Application_Window, MUI_NewObject (MUIC_Window,   /* okno 1 */
    MUIA_Window_RootObject, MUI_NewObject (MUIC_Group,  /* grupa A */
      MUIA_Group_Child, MUI_NewObject (MUIC_Group,     /* grupa C */
        MUIA_Group_Child, MUI_NewObject (MUIC_String,   /* gadżet Z */
          /* ... */, TAG_END),
        MUIA_Group_Child, MUI_NewObject (MUIC_Text,   /* gadżet Y */
          /* ... */, TAG_END),
      MUIA_Group_Child, MUI_NewObject, (MUIC_Radio,   /* gażdet X */
        /* ... */, TAG_END),
      TAG_END),
    TAG_END),
  MUIA_Application_Window, MUI_NewObject (MUIC_Window,   /* okno 2 */
    MUIA_Window_RootObject, MUI_NewObject (MUIC_Group,   /* grupa B */
      MUIA_Group_Child, MUI_NewObject (MUIC_Slider,      /* gadżet U */
        /* ... */, TAG_END),
      MUIA_Group_Child, MUI_NewObject (MUIC_Image,       /* gadżet W */
        /* ... */, TAG_END),
      TAG_END),
    TAG_END),
  TAG_END);


Znakami komentarza (/*... */) zaznaczyłem nieistotne w tym momencie atrybuty obiektów. Cały powyższy kod to jedna długa funkcja. Wbrew pozorom kolejność tworzenia obiektów jest taka sama jak w pierwszym przykładzie - kompilator zaczyna wykonywanie takiej funkcji od najbardziej zagnieżdżonych wywołań, tak więc obiekt aplikacji zostanie, podobnie jak poprzednio, stworzony na końcu. Jak jednak wspomniałem istnieje drugi sposób uproszczenia kodu, stosowany najczęściej razem z pierwszym. Są to makra (zdefiniowanie w pliku "mui.h") zmniejszające znacznie czas klepania w klawisze przy programowaniu. Oto jak miło wygląda nasz przykład po wykorzystaniu makr:

app = ApplicationObject, /* ... */,                    /* aplikacja */
  MUIA_Application_Menu, MenustripObject, /* ... */,   /* menu */
    End,
  SubWindow,                                           /* okno 1 */
    WindowContents, VGroup,                            /* grupa C */
      Child, VGroup                                    /* grupa A */
        Child, StringObject, /* ... */,                /* gadżet Y */
          End,
        Child, TextObject, /* ... */,                  /* gadżet Z */
          End,
        End,
      Child, RadioObject, /* ... */,                   /* gadżet X */
        End,
      End,
    End,
  SubWindow,                                           /* okno 2 */
    WindowContents, HGroup                             /* grupa B */
      Child, SliderObject, /* ... */,                  /* gadżet U */
        End,
      Child, ImageObject, /* ... */,                   /* gadżet W */
        End,
      End,
    End,
  End;


Makra te są bardzo proste (w większości nie mają parametrów) i ich analizę sobie tu darujemy, bo preprocesor po prostu zamiast nazwy makra wstawia to, co stanowi resztę linii w definicji. Nic nie stoi na przeszkodzie, aby dopisać sobie trochę własnych makr korzystając jak zwykle z komendy preprocesora #define (np. do często używanych, a nie zdefiniowanych w standardowych makrach obiektów). Piszącym w E przypominam jeszcze raz o OPT PREPROCESS na początku kodu.

Zasady grupowania

Struktura drzewa obiektów jest rezultatem stosowania klas grupujących. Po przyjrzeniu się przykładom powyżej łatwo zauważyć, że każda klasa grupująca używa do grupowania innych atrybutów. Na przykład grupa ma atrybut MUIA_Group_Child, okno - MUIA_Window_Root_Contents, aplikacja z kolei ma między innymi MUIA_Application_Window. Stanie się to oczywiste, gdy zauważymy, że każda z tych relacji posiadania nakłada na grupę posiadającą inne obowiązki. Na przykład obowiązkiem grupy jest odpowiednie wyznaczenie miejsc wszystkim jej gadżetom, obowiązkiem okna jest, między innymi, przekazywanie zdarzeń generowanych przez użytkownika (mysz, klawiatura) i tak dalej. Warto więc wiedzieć jakie obiekty mogą należeć do różnych obiektów klas grupujących. Zaczniemy od najbardziej ogólnej z nich, czyli MUIC_Family. Do grupowania używamy atrybutu MUIA_Family_Child. Tu nie mamy ograniczeń na klasę grupowanych obiektów, możemy grupować co nam się tylko spodoba. Często używa się tej klasy do grupowania obiektów niewidocznych na ekranie, czyli nie będących elementami GUI. Mogą to być np. obiekty reprezentujące dane, na których operuje nasz program. Ilość grupowanych obiektów jest ograniczona wyłącznie dostępną pamięcią. W tej klasie możemy grupować kaskadowo, czyli elementem "rodziny" może być inna "rodzina".

Podklasami MUIC_Family są klasy obsługujące systemowe menu. Tu na ilość grupowanych obiektów i ilość poziomów zagnieżdżania ograniczenia nakłada intuition.library. Możemy więc mieć maksymalnie 31 pozycji menu, 63 elementy menu w każdej pozycji, jeden poziom pod-menu i po 31 elementów w każdym pod- -menu. Do grupowania używamy również MUIA_Family_Child. Nadrzędny obiekt musi być klasy MUIC_Menustrip, do niego należą obiekty klasy MUIC_Menu posiadające z kolei obiekty klasy MUIC_Menuitem.

Kolejną klasą grupującą jest MUIC_Group. Służy do grupowania obiektów z klasy MUIC_Area i pochodnych, a więc generalnie rzecz biorąc gadżetów. Do grupowania służy atrybut MUIA_Group_Child.

Specyficzną klasą grupującą jest MUIC_Window. Ta odmienność polega na tym, że do okna może należeć tylko jeden obiekt i obiekt ten musi być grupą (co widać na rysunku). Ze względów estetycznych grupa ta nie powinna mieć ramek. Jak nieciekawie to wygląda można - niestety - zobaczyć w okienku ściągania poczty w YAM-ie (również p7). Grupę główną dołączamy do okna atrybutem MUIA_Window_RootObject.

Ostatnią z serii klas grupujących jest MUIC_Application. Klasa ta ma kilka atrybutów grupowania, ponieważ należy do niej wiele różnych obiektów. Przede wszystkim aplikacja grupuje okna (atrybut MUIM_Application_Window). Ilość okien jest ograniczona wyłącznie dostępną pamięcią, Aplikacja może również nie mieć żadnego okna. Poza tym do aplikacji należy menu (MUIC_Menustrip), które może być tylko jedno, albo może go nie być. Menu jest dołączane atrybutem MUIA_Application_Menustrip.

Struktura programu

Po opanowaniu sposobu budowy drzewa obiektów możemy zająć się strukturą całego programu działającego z MUI. Program taki można podzielić na cztery bloki:

  • kod startowy
  • stworzenie drzewa obiektów
  • główna pętla
  • kod kończący

Kodem startowym nazywam procedury otwierające potrzebne biblioteki i odczytujące parametry np. z linii wywołania, czy tooltypów ikony. Oprócz tego możemy tu utworzyć niezbędne klasy prywatne. Tworzenie drzewa obiektów jest już nam znane. Przejdźmy więc do pętli głównej. Jest ona dość nietypowa, bo bardzo krótka. Założeniem MUI jest bowiem, że obsługa wszelkich akcji użytkownika odbywa się poza główną pętlą, poprzez notyfikacje. Główna pętla oczekuje jedynie na dwa zdarzenia: odebranie sygnału CTRL-C (przerwanie procesu) i otrzymanie przez obiekt aplikacji metody MUIM_Application_ReturnID z parametrem MUIV_Application_ReturnID_Quit, co oznacza zakończenie programu. Oto przykład poprawnej pętli głównej, zaczerpnięty z przykładowych programów z pakietu MUI dla programistów:

ULONG signals = 0;

while (DoMethod (app, MUIM_Application_NewInput, &signals) != 
 MUIV_Application_ReturnID_Quit)
  {
    if (signals)
      {
	signals = Wait (signals | SIGBREAKF_CTRL_C);
        if (signals & SIGBREAKF_CTRL_C) break;
      }
    }


Pętli tej warto przyjrzeć się dokładnie. Wiele programów pod MUI jest źle napisanych z powodu niewykorzystania idei leżącej u podstaw tego systemu. Wielu początkujących programistów korzysta z faktu, że przy pomocy metody MUIM_Application_ReturnID można do obiektu aplikacji przesłać dowolną wartość i obsługuje zdarzenia wysyłając różne wartości. Następnie wartości te sprawdzają w dużej instrukcji switch w głównej pętli. Takie postępowanie, choć z pozoru "ułatwia" pisanie programu, jest błędem i powinno się go unikać. Aby zrozumieć tego przyczyny przeanalizujmy co się tak naprawdę w tej pętli dzieje. Zmienna 'signals' przechowuje wszystkie sygnały, na jakie MUI w danej chwili czeka. Są to głównie sygnały z portów IDCMP otwartych okien. W czasie wykonywania metody MUIM_Application_NewInput sygnały te są określone i wstawione pod adres zmiennej 'signals'. Oprócz tego w metodzie tej odbywa się odświeżenie gadżetów. Wynikiem tej metody są ewentualne identyfikatory wysłane skądkolwiek do obiektu aplikacji przez MUIM_Application_ReturnID. Jeżeli odbierzemy identyfikator końca programu wychodzimy z pętli. W przeciwnym wypadku czekamy na określone wcześniej sygnały funkcją Wait() z exec.library. Oprócz tego czekamy na sygnał CTRL-C. Jest on używany przez system operacyjny do przerywania procesów. Możemy go wysłać np. z shella z którego uruchomiliśmy program wciskając klawisze CTRL+C (stąd nazwa), albo z jakiegoś monitora procesów (np. Scout). Odebranie tego sygnału również oznacza wyjście z pętli. Wszelkie inne sygnały oznaczają jakieś zdarzenia wywołane przez użytkownika i po nawrocie pętli zostaną zinterpretowane w następnym wywołaniu MUIM_Application_NewInput. Dlatego szczególnie ważne jest, aby między funkcją Wait() a końcem pętli nie umieszczać żadnych dodatkowych instrukcji. Takie instrukcje powodują opóźnienie między akcją użytkownika (np. kliknięciem na gadżet) a reakcją GUI (wciśnięciem się gadżetu), co jest bardzo denerwujące przy pracy z programem i bywa podnoszone jako wada MUI przez jego przeciwników. A wynika to wyłącznie z niewiedzy programisty. Również wypychanie kodem pętli głównej przed instrukcją Wait() powoduje zakłócenia w pracy programu objawiające się czasowym brakiem jego reakcji na akcje użytkownika. Dlatego należy tego unikać.

Po wyjściu z pętli głównej wykonywany jest kod kończący, który sprowadza się do likwidacji obiektu aplikacji funkcją MUI_DisposeObject(), usunięcia ewentualnych klas prywatnych i zamknięcia bibliotek.

Na zakończenie przykład typowej notyfikacji powodującej wyjście z programu po zamknięciu. Notyfikację tą, tak jak i wszelkie inne, umieszcza się między blokiem tworzenia drzewa obiektów, a pętlą główną:

Object *app, *win;

DoMethod (win, MUIM_Notify, MUIA_Window_CloseRequest, MUIV_EveryTime, app,
 2, MUIM_Application_Return_ID, MUIV_Application_ReturnID_Quit);


Przykład ten pozostawię bez komentarza, a notyfikacjami zajmę się szerzej w jednym z następnych odcinków.

 głosów: 1   
dodaj komentarz
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