• Kurs MUI - część 14

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

Dynamiczne okna

MUI Każdy większy program z reguły wymaga więcej niż jednego okienka. Spójrzmy na YAM-a, AmIRC-a, czy Voyagera - okienek mamy tam co najmniej kilkanaście. Oczywiście nigdy nie są otwarte wszystkie na raz. Jak tworzyć pojawiające się na zawołanie i znikające okna we własnych programach? Najprostszym sposobem jest stworzenie wszystkich okien przy budowaniu aplikacji, a potem tylko pokazywanie ich lub zamykanie atrybutem MUIA_Window_Open. Najprostszym, ale - jak to zwykle bywa - niekoniecznie najlepszym. Wady są dwie: po pierwsze wszystkie w danej chwili niepotrzebne okna zajmują pamięć. Po drugie zaś są takie okna, których nie wiadomo ile zechce użytkownik otworzyć. Np. okna kanałów i prywatnych rozmów w AmIRC-u. Jeżeli ktoś ma kartę graficzną i duży monitor może ich mieć kilkanaście. Stworzenie np. piętnastu obiektów Window przy starcie programu pochłonie zupełnie niepotrzebnie cenne kilobajty pamięci, a poza tym kto zabroni użytkownikowi wejść na jeszcze jeden kanał? Rozwiązaniem tych problemów jest dynamiczne tworzenie i usuwanie okien.

Na czym to polega? Tworzymy okno wraz z zawartością w momencie gdy jest potrzebne, dołączamy je do aplikacji, pozwalamy użytkownikowi z niego skorzystać, a następnie odłączamy je od aplikacji i likwidujemy. Sekwencja wygląda następująco:

okno = MUI_NewObject (MUIC_Window, /* ... */ );
DoMethod (app, OM_ADDMEMBER, okno);
SetAttrs (okno, MUIA_Window_Open, TRUE, TAG_END);

/* działamy w oknie */

SetAttrs (okno, MUIA_Window_Open, FALSE, TAG_END);
DoMethod (app, OM_REMMEMBER, okno);
MUI_DisposeObject (okno);


MUI Proste prawda? Niestety nie do końca. Problem tkwi w usuwaniu okna. Załóżmy, że po wciśnięciu przycisku w głównym oknie otwiera się okno drugie. To drugie okno zamykamy jego gadżetem zamykania. Aby utrwalić sobie pisanie własnych klas zróbmy to obiektowo. Utworzymy własną podklasę z klasy Application z dwiema nowymi metodami: APPM_AddSubWindow i APPM_RemSubWindow. W pierwszej z nich znajdą się trzy pierwsze linijki kodu z wcześniej pokazanego fragmentu, w drugiej zaś trzy ostatnie. Notyfikacja wywołująca pierwszą metodę będzie wyglądała następująco:

DoMethod (przycisk, MUIM_Notify, MUIA_Pressed, FALSE, app, 1,
 APPM_AddSubWindow);


Notyfikacja wywołująca drugą metodę również wydaje się oczywista:

DoMethod (okno, MUIM_Notify, MUIA_Window_CloseRequest, TRUE, app, 1,
APPM_RemSubWindow);


A więc wpisujemy (patrz "przyklad1.c"), kompilujemy, odpalamy, teraz przycisk, okno się otwiera, zamykamy je i... zwiecha! Przypadek? Błąd w MUI? Nie - ten program ma obowiązek się powiesić! Dlatego proszę nie przysyłać do redakcji listów, że "ten lamer Krashan pisze wieszające się przykłady" - pierwszy program ma zastosowanie wyłącznie edukacyjne :-).

MUI

No dobrze, ale dlaczego tak się dzieje? Przyczyna jest prosta - obiekt próbuje sam się usunąć. Zwróćcie uwagę na to, że notyfikacja usuwająca obiekt jest wywoływana z niego samego właśnie. Notyfikacje obiektu przechowywane są w liście, po wykonaniu danej notyfikacji MUI zechce sprawdzić następną pozycję listy, a tu... listy już nie ma, bo nasza notyfikacja skasowała obiekt i mamy zawieszenie się programu. Wniosek z tego jest prosty - usunięcie obiektu nie może być spowodowane notyfikacją wywoływaną z niego samego (ani z obiektów należących do niego). Pokazuje to przykład 2 - tutaj okno zamykamy po wciśnięciu gadżetu "Zamknij" w głównym oknie i program działa prawidłowo. Niestety nie można okna zamknąć klikając na jego gadżet zamykania. Nie można też okna zamknąć z jakiegokolwiek innego gadżetu umieszczonego w jego wnętrzu, więc wszelkie przyciski typu "OK" czy "Zapisz" również nie będą działać. Jest to więc rozwiązanie niewygodne dla użytkownika programu.

Jak wybrnąć z tej sytuacji? Podstawowa wskazówka jest jasna - usuwanie obiektu nie może odbywać się w notyfikacji. Najprostszym rozwiązaniem jest skorzystanie z MUIM_Application_ReturnID i zmiennej globalnej do przekazania adresu okna. Nie jest to może szczyt programistycznej elegancji, ale pozwala nam skutecznie uporać się z problemem. Tym razem w notyfikacji zostaje jedynie ustawiona wartość ReturnID, ale sprawdzenie tej wartości i usunięcie okna wykonywane jest w głównej pętli. Ma to miejsce po wykonaniu wszystkich notyfikacji wyzwolonych sygnałami, na które program czekał w funkcji Wait() ostatnim razem. Tu jednak musimy bardzo uważać w przypadku większej ilości okien. Jeżeli kombinacja sygnałów w Wait() spowoduje nam wywołanie notyfikacji, które jednocześnie usuną więcej niż jedno okno - jesteśmy ugotowani na miękko. Usunięte zostanie tylko okno "zanotyfikowane" jako ostatnie.

W przypadku większej ilości okien pojawi się jeszcze problem zapanowania nad nimi. W naszym programie adres okna jest pamiętany w obiekcie aplikacji (data->subwindow). Oczywiście jest tam miejsce tylko na jedno okno. Unikam stworzenia większej ich ilości przez zablokowanie przycisku po stworzeniu pierwszego. W przypadku, gdy liczba tworzonych okien jest nieograniczona najlepiej zmodyfikować metodę APPM_RemSubWindow w taki sposób, żeby okno podawało jej swój adres jako parametr. Takie rozwiązanie pokazałem w przykładzie 4. Trochę zaskakująca może być notyfikacja na zamknięcie pod-okna:

DoMethod (subwin, MUIM_Notify, MUIA_Window_CloseRequest, TRUE, obj, 2,
 APPM_RemSubWindow, subwin);


Sekret polega na tym, że przy każdym wywołaniu (a więc przy każdym tworzonym oknie) zmienna "subwin" zawiera jego adres i ten adres zostanie zapamiętany w notyfikacji. Podstawienie tam konkretnej wartości odbywa się w momencie zakładania notyfikacji, a nie w momencie jej wywołania. Dzięki temu każda notyfikacja wywoła metodę APPM_RemSubWindow z adresem swojego okna. Efekty widać na rysunku - ilość okien jest zupełnie dowolna i możemy zaszaleć. Oczywiście każde okno zostanie poprawnie zamknięte, również w przypadku zamknięcia całego programu (okna głównego) MUI zrobi porządek z wszystkimi obiektami. Efektu zamknięcia kilku okien jedną notyfikacją nie musimy się obawiać - niech użytkownik spróbuje wcisnąć kilka gadżetów jednocześnie... Ale nawet jeżeli zaszłaby potrzeba usuwania okien po kilka, jest na to sposób. Pokazałem go w przykładzie 5. Tu za każdym razem otwieramy dwa okna - okno z "postępami ładowania" i okno informacyjne. Zamknięcie dowolnego okna z pary powoduje również zamknięcie drugiego. Oczywiście mógłbym pójść po najmniejszej linii oporu i dodać drugą zmienną globalną. Jednak chcę pokazać uniwersalne rozwiązanie działające nawet wtedy, gdy liczba zamykanych jednocześnie okien może się zmieniać. Sztuczka jest prosta - okna po odłączeniu od aplikacji w metodzie APPM_RemSubWindow pakuję do obiektu klasy Family (jest to obiekt będący listą obiektów - pisałem o tym w jednym z pierwszych odcinków) i adres tego właśnie obiektu podaję do zniszczenia w zmiennej globalnej. Usunięcie obiektu klasy Family powoduje usunięcie wszystkich jego "dzieci", których ilość może być dowolna.

MUI

Inny sposób na obsługę wielu okien zaproponował sam twórca MUI, Stefan Stuntz, w pliku "MUIDev.guide" dołączonym do pakietu dla programistów. Zgodnie z jego metodą ominięcie problemów z samodestrukcyjną notyfikacją polega na dopisaniu dodatkowej pętli głównej (choć w tym wypadku trudno ją nazwać główną) do procedury, w której otwieramy i zamykamy okno. Kod wygląda mniej więcej tak:

okno = MUI_NewObject (MUIC_Window, /* ... */);

if (okno)
 {
  LONG running = TRUE, signals = 0;

  DoMethod (okno, MUIM_Notify, MUIA_Window_CloseRequest, TRUE, app, 2,
   MUIM_Application_ReturnID, ZAMKNIJ_OKNO);
  DoMethod (app, OM_ADDMEMBER, okno);
  SetAttrs (okno, MUIA_Window_Open, TRUE);

  while (running)
   {
    switch (DoMethod (app, MUIM_NewInput, &signals))
     {
      case MUIV_Application_ReturnID_Quit:
        DoMethod (app, MUIM_Application_ReturnID,
         MUIV_Application_ReturnID_Quit);
        running = FALSE;
       break;

      case ZAMKNIJ_OKNO:
        running = FALSE;
       break;
     }

    if (signals)
     {
      signals = Wait (signals | SIGBREAKF_CTRL_C);
      if (signals & SIGBREAKF_CTRL_C)
       {
        DoMethod (app, MUIM_Application_ReturnID,
         MUIV_Application_ReturnID_Quit);
        running = FALSE;
       }
     }
   }
  SetAttrs (okno, MUIA_Window_Open, FALSE, TAG_END);
  DoMethod (app, OM_REMMEMBER, okno);
  MUI_DisposeObject (okno);
 }


W momencie otworzenia drugiego okna program zaczyna "chodzić" w tej dodatkowej pętli zamiast w głównej. Opuszczenie pętli następuje w momencie zamknięcia okna, odebrania identyfikatora MUIV_Application_ReturnID_Quit lub sygnału CTRL-C. Pętla musi rozróżniać te sytuacje i w wypadku dwóch ostatnich powtórzyć wysłanie identyfikatora, aby mógł zostać odebrany przez główną pętlę po wyskoczeniu z tej dodatkowej. Jak widać jest to sposób dość zagmatwany i nieefektywny - powtarzamy ten sam kod w kilku miejscach programu. Co ciekawsze ta metoda nie jest stosowana w przykładach dołączonych do pakietu MUI. W jedynym jak się wydaje przykładzie który dynamicznie obsługuje okna - a jest nim WBMan napisany przez Klausa Melchiora - okna obsługiwane są metodą opisaną wyżej, z MUIM_Application_ReturnID i zmienną globalną... Przy okazji, WBMan jest świetnym przykładem na to jak NIE powinno się pisać programów pod MUI. Stada zmiennych globalnych i gromada różnych ReturnID w pętli głównej - po prostu horror.

W następnym odcinku pisanie programów wielowątkowych. Przy okazji pokażę też jedną z bardziej eleganckich metod obsługi wielu okienek z wykorzystaniem MessagePortu. Dodatek - tutaj.

 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