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);
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 :-).
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.
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.