• Kurs MUI - część 9

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

Listy i listviewy 2

Dziś zajmę się częściej używanymi dodatkowymi możliwościami list i listviewów. Jednocześnie przypominam że ten cykl artykułów nie zastąpi lektury dokumentacji MUI, on ją tylko uzupełnia. Zainteresowanym szczegółami przypominam więc o istnieniu tej dokumentacji. To tyle tytułem wstępu, przejdę teraz do rzeczy.

Tytuły list

MUI Bardzo często chcemy, aby każda kolumna listy była w jakiś sposób opisana. Narzucającym się rozwiązaniem są tytuły kolumn umieszczone na górze, zupełnie tak samo jak w zwykłej tabeli. Niestety umieszczenie tychże tytułów po prostu w pierwszej linii listy nie spełni naszych oczekiwań - w momencie przesunięcia zawartości listy suwakiem nasze tytuły zwyczajnie znikną nad górną krawędzią listy. Potrzebny jest sposób dzięki któremu tytuły będą umieszczone stale na górze listy niezależnie od tego w którym miejscu listy jesteśmy. Taki mechanizm istnieje w klasie List i jest związany z atrybutem MUIA_List_Title. W najprostszym przypadku listy jednokolumnowej złożonej z napisów po prostu wystarczy podać tutaj tytuł listy

MUIA_List_Title, "Spis z natury",


Takie zastosowanie tytułu listy pokazuje przykład 10a. Sprawa się nieco komplikuje w wypadku listy wielokolumnowej. Możemy oczywiście pozostać przy jednym tytule, ale skoro podzieliliśmy już listę na kolumny, to przeważnie to samo chcemy zrobić z tytułem. W tym celu traktujemy atrybut MUIA_List_Title nie jako wskaźnik na napis, ale jako wartość logiczną

MUIA_List_Title, TRUE,


Same tytuły kolumn umieścimy w DisplayHooku (patrz poprzedni odcinek). Jak pamiętacie zadaniem DisplayHooka była zamiana elementu listy na odpowiednie napisy umieszczane w jej kolumnach. Parametrem hooka był adres elementu listy. W momencie gdy MUI chce wyświetlić tytuły kolumn (zamiast elementu listy) podaje jako adres elementu zero. Nasz DisplayHook musi być na to zero przygotowany - gdy wykryjemy zero, w tablicy z tekstami umieszczamy tytuły kolumn. Oto przykładowy DisplayHook, jest on modyfikacją hooka z przykładu 9c.

long TowarDisplayer (struct Towar *towar reg(a1), char **teksty reg(a2))
 {
  static char textl[64], ilosc[12];

  /* tytuły kolumn, jeśli adres elementu zerowy */

  if (!towar)
   {
    teksty[0] = "X33cX33bX0331Nazwa towaru";
    teksty[1] = "X33cX33bX0331Ilość";
    return 0;
   }

  /* normalne wypisanie elementu w przeciwnym wypadku */
  
  if (towar->ile_na_skladzie > 0)
   {
    teksty[0] = towar->nazwa;
    sprintf (ilosc, "%ld", towar->ile_na_skladzie);
   }
  else
   {
    sprintf (textl, "X33b%sX33n", towar->nazwa);
    teksty[0] = textl;
    sprintf (ilosc, "X33b%ldX33n", towar->ile_na_skladzie);
   }
  teksty[1] = ilosc;
  return 0;
 }


UWAGA! W miejsce znaku X należy wstawić znak "slasha".

Efekty pokazuje przykład 10b, który jest niczym innym jak przerobionym przykładem 9c - zmienił się tylko hook, dodałem też oczywiście atrybut MUIA_List_Title dla listy.

MUI

Sortowanie drag&drop

Ciekawą właściwością MUI-owej listy jest możliwość jej sortowania metodą drag&drop, czy też "przeciągnij i upuść" jak chcą poloniści. Jak to działa wie chyba każdy użytkownik MUI - łapiemy wskaźnikiem myszki za element listy i przeciągamy w wybrane miejsce. Listę sortowalną zrobić jest bardzo łatwo, dlatego tym razem obędzie się bez przykładu. Wszystko co musimy zrobić, to ustawić dwa atrybuty. Dla obiektu List ustawiamy

MUIA_List_DragSortable, TRUE,


Dla nadrzędnego obiektu ListView zaś

MUIA_Listview_DragType, MUIV_Listview_DragType_Immediate,


Przypominam o tym, że w czasie tworzenia obiektu nie wolno podawać atrybutów ListViewa dla obiektu List ani odwrotnie!

Szybkie wstawianie

W normalnych warunkach wstawienie każdego elementu do listy (lub usunięcie z niej elementu) powoduje jej odświeżenie na ekranie. Jeżeli mamy zamiar wstawić do listy np. 100 elementów operacje graficzne niepotrzebnie zajmują czas procesora i spowalniają cały proces. Dlatego mamy do dyspozycji specjalny atrybut MUIA_List_Quiet. Jego ustawienie na wartość TRUE blokuje wszelkie operacje odświeżania widoku. Lista funkcjonuje normalnie, możemy wstawiać i usuwać elementy, ale nie znajduje to swojego odzwierciedlenia w okienku programu. Gdy przywrócimy atrybutowi MUIA_List_Quiet wartość FALSE, lista zostanie automatycznie odrysowana, uwidoczniając tym samym wprowadzone zmiany. Warto stosować ten sposób np. przy ładowaniu zawartości listy z pliku.

Listy z wyborem wielu elementów


Takie listy (ang. multiselect lists) pozwalają na zaznaczenie wielu elementów jednocześnie. Element na takiej liście może znajdować się w czterech stanach:

  • nieaktywny, nie zaznaczony,
  • nieaktywny, zaznaczony,
  • aktywny, nie zaznaczony,
  • aktywny, zaznaczony.

Dla każdego stanu (oprócz pierwszego rzecz jasna) istnieje odrębne ustawienie koloru kursora listy w preferencjach. Lista staje się listą wielokrotnego wyboru po podaniu jej atrybutu MUIA_Listview_Multiselect. Odpowiednie wartości tego atrybutu pozwalają na wielokrotny wybór w sposób kontrolowany przez użytkownika w preferencjach (polecam używanie tej wartości), wielokrotnego wyboru tylko z klawiszem SHIFT, lub wyboru aktywnego zawsze przy "ciągnięciu" myszą po elementach.

Odrębnym zagadnieniem jest odczytanie z listy, które obiekty są wybrane. Jeżeli chcemy reagować na każdą zmianę selekcji, notyfikację ustawiamy na atrybut MUIA_Listview_SelectChange z wartością TRUE. Jeżeli natomiast interesują nas tylko elementy wybrane w momencie podwójnego kliknięcia (co zwyczajowo oznacza ostateczne wybranie elementów), wtedy notyfikację należy ustawić na MUIA_Listview_DoubleClick, TRUE. Niezależnie od rodzaju notyfikacji elementy zaznaczone możemy zidentyfikować przy pomocy metody MUIM_List_NextSelected. Identyfikacja odbywa się w pętli, oto jej przykład:

long pozycja;          /* pozycja kolejnego elementu */

for (pozycja = MUIV_List_NextSelected_Start;
     pozycja != MUIV_List_NextSelected_Stop; 
     DoMethod (list, MUIM_List_NextSelected, &pozycja)
 {
  /* zmienna 'pozycja' zawiera numer kolejnego wybranego elementu */
  /* tu robimy coś z tym elementem */
 }


Zmienna 'pozycja' przechowuje naszą pozycję na liście w trakcie przeszukiwania. Jest ona inicjowana wartością początkową oznaczającą, że MUI ma zacząć poszukiwania zaznaczonych elementów od początku listy. Kolejne wywołania metody przesuwają nas do kolejnego zaznaczonego elementu. Koniec zaznaczonych elementów MUI sygnalizuje wstawiając specjalną wartość do zmiennej 'pozycja' i wtedy opuszczamy pętlę. Jeżeli MUI nie wykryje żadnego zaznaczonego elementu na liście, opisana pętla da nam numer elementu aktywnego (nawet jeżeli nie jest zaznaczony).

W niektórych przypadkach może być potrzebna lista która w ogólności jest listą wielokrotnego wyboru, ale niektóre elementy nie mogą być wybrane łącznie z innymi. Klasa List pozwala na dołączenie do obiektu List specjalnego hooka "filtrującego" wielokrotny wybór. Hook ten podaje się liście używając atrybutu MUIA_List_MultiTestHook. Jego rola jest prosta - jest on wywoływany przy każdej próbie zaznaczenia więcej niż jednego elementu. Parametrem jaki dostajemy jest wskaźnik na element listy (na strukturę jaką dołączyliśmy). Teraz możemy podjąć decyzję - jeżeli w hooku zwrócimy TRUE, element będzie mógł być wybrany razem z innymi, jeżeli FALSE - wybranie elementu zostanie zablokowane.

Obsługa dużych plików

Elastyczność list MUI pozwala na robienie z nimi niemożliwych na pierwszy rzut oka rzeczy. Wyobraźmy sobie, że nasz spis towarów umieszczony jest w pliku i zawiera 50 tysięcy pozycji, zajmując na dysku 2 MB. Chcemy go pokazać w listviewie i program oczywiście ma działać nawet na "gołej" A1200 z dwoma megabajtami pamięci, z których przeważnie po uruchomieniu systemu pozostaje półtora... Niemożliwe? Nic bardziej błędnego.

MUI Możliwość obsługi dużych plików pokazuje, nieco bardziej rozbudowany niż poprzednie, przykład 10c. Jest to prosta przeglądarka do tekstów pokazująca je w obiekcie Listview w taki sposób, że każda linia jest jedną pozycją listy. Wydawałoby się więc, że jest to zwykła lista ze stringami jako elementami. Wtedy jednak treść wszystkich linii tekstu musiałaby być załadowana do pamięci, a więc nie moglibyśmy przeglądać tekstów większych niż ilość wolnej pamięci. Zauważmy jednak, że w danym momencie potrzebujemy jedynie treści tych linii, które są widoczne w listviewie. Dlatego właśnie odczytywanie treści odbywa się w hooku wyświetlającym (!), a konstruktor dodaje do listy jedynie tzw. offsety poszczególnych linii, czyli numery bajtów w pliku, od których zaczynają się linie. W ten sposób każda linia zabiera nam stałą ilość pamięci (używaną przez MUI do połączenia obiektów w listę) niezależnie od jej długości. Przyjrzyjmy się bliżej newralgicznym fragmentom kodu:

SetAttrs (App, MUIA_Application_Sleep, TRUE, TAG_END);
SetAttrs (*listview, MUIA_List_Quiet, TRUE, TAG_END);

while (1)
 {
  pozycja = Seek (Plik, 0, OFFSET_CURRENT);
  odczyt = FGets (Plik, linia, 1024);
  if (!odczyt) break;
  DoMethod (*listview, MUIM_List_InsertSingle, pozycja, MUIV_List_Insert_Bottom);
 }

SetAttrs (*listview, MUIA_List_Quiet, FALSE, TAG_END);
SetAttrs (App, MUIA_Application_Sleep, FALSE, TAG_END);


Poniższy fragment pochodzi z funkcji CzytajPlik() wywoływanej jako hook w momencie wybrania pliku do obejrzenia. Zadaniem pętli jest odszukanie wszystkich początków linii w pliku i dopisanie tych początków jako elementów listy. Pozycję kolejnych linii poznajemy funkcją Seek () z dos.library, czytanie liniami zapewnia funkcja FGets () z tej samej biblioteki. Pozycje kolejnych linii są wstawiane do listy metodą MUIM_InsertSingle. Zwracam uwagę na posłużenie się omówionym wcześniej atrybutem MUIA_List_Quiet, dzięki wyłączeniu odświeżania listy na czas wstawiania linii uzyskujemy tu znaczne przyśpieszenie czytania pliku, dociekliwym proponuję skasowanie odpowiednich linii w kodzie źródłowym i porównanie szybkości czytania jakiegoś kilkusetkilobajtowego tekstu. Ustawiając atrybut MUIA_Application_Sleep blokuję okno programu, jednocześnie wskaźnik myszy przyjmuje kształt "oczekiwania" sygnalizując użytkownikowi, że program wykonuje czasochłonną operację. Teraz przyjrzyjmy się hookowi konstrukcyjnemu:

long LiniaConstructor (long linia reg(a1), APTR mempool reg(a2))
 {
  long *element;

  if (element = AllocPooled (mempool, sizeof (long)))
   {
    *element = linia;
    return (long)element;
   }
  return NULL;
 }


Jest on bardzo prosty - pozycja linii w pliku jest kopiowana do świeżo przydzielonej pamięci. Skorzystanie z "memory pools" znacznie przyspiesza czytanie pliku, przy większych plikach konstruktor może być wywołany kilkadziesiąt tysięcy razy i jego maksymalna optymalizacja jest bardzo wskazana. Ostatnim interesującym miejscem jest hook wyświetlający - w nim odbywa się odczyt rzeczywistej zawartości linii z dysku:

long LiniaDisplayer (long *linia reg(a1), char **teksty reg(a2))
 {
  static char text[1024];

  Seek (Plik, *linia, OFFSET_BEGINNING);
  FGets (Plik, text, 1024);
  *teksty = text;
  return 0;
 }


W zasadzie kod jest oczywisty - funkcją Seek() ustawiam się na początek linii w pliku, następnie czytam linię do bufora i adres bufora umieszczam w tablicy tekstów do wypełnienia, podobnie jak we wszystkich poprzednich przykładach. I również podobnie jak poprzednio - tablica w której umieszczany jest tekst jest zmienną statyczną, ponieważ jest wykorzystywana poza funkcją przez MUI. Można się zastanawiać, czy odczyt z dysku będzie odpowiednio szybki, wszak odczyty będą następowały przy każdej manipulacji listą. Funkcja FGets() jest jednak buforowana, co znacznie redukuje obciążenie operacjami dyskowymi. W praktyce przy czytaniu pliku z dysku twardego w systemie FFS nie odczuwa się różnicy w porównaniu z listą przechowującą zawartość w pamięci. Problemy mogą jedynie wystąpić, jeżeli plik jest na dyskietce. Z drugiej jednak strony sposobu, który przedstawiłem używa się do plików o wielkościach megabajtowych, a takie na dyskietce się nie mieszczą... Jednak ważną sprawą jest buforowanie, w przypadku użycia niebuforowanej funkcji Read() spowolnienie działania listy może być już widoczne, dlatego należy zastąpić ją buforowaną funkcją FRead(). Szczególnie dobrze sprawdza się ta funkcja w przypadku elementów listy o stałej długości. Omówiona tu metoda pozwala na wczytanie pliku kilka razy większego od ilości wolnej pamięci. Zysk zależy od wielkości elementów listy, im są większe tym większy jest nasz zysk z zastąpienia elementów ich pozycjami w pliku. Istnieją sposoby na jeszcze większą oszczędność pamięci, pozwalające na obróbkę plików o wielkościach powyżej 1 GB, niemniej ich omówienie wykraczałoby tematycznie poza ten cykl artykułów, gdyż metody te nie wykorzystują specyficznych cech MUI.

Notyfikacje w listach

Listy posiadają kilka atrybutów, które zmieniają się pod wpływem poczynań użytkownika i na atrybuty te możemy ustawiać notyfikacje. Przede wszystkim przydatny będzie atrybut MUIA_List_Active. Wskazuje on na numer elementu listy na którym znajduje się kursor. Możliwe wartości tego atrybutu mieszczą się w zakresie od 0 do ilości elementów pomniejszonej o 1 (ilość elementów listy można odczytać poprzez atrybut MUIA_List_Entries). Oprócz tego możemy otrzymać wartość MUIV_List_ActiveOff, która oznacza, że żaden element listy nie jest wybrany. "Zapomnienie" o tej możliwości jest częstym błędem. Pozostałe wyczulone na użytkownika atrybuty znajdują się w klasie Listview, co zresztą nie dziwi - klasa ta odpowiada za obsługę myszy i klawiatury w listach. Zobaczmy więc, co my tu mamy. Przede wszystkim MUIA_Listview_DoubleClick. Reaguje za każdym razem, gdy użytkownik podwójnie kliknie na elemencie listy. Ponieważ teki element stanie się elementem aktywnym, jego numer można odczytać wspomnianym wcześniej atrybutem MUIA_List_Active. Atrybut MUIA_Listview_ClickColumn przydaje się przy listach wielokolumnowych i podaje numer kolumny listy w którą kliknął użytkownik (kliknął jednokrotnie). Wreszcie ostatni interesujący atrybut to MUIA_Listview_SelectChange. Szczególnie przydatny będzie przy listach z wielokrotnym wyborem (multiselect), czyli takich, w których można zaznaczyć jednocześnie kilka elementów. Notyfikacja na ten atrybut zostanie wykonana przy każdej zmianie zaznaczenia elementów. Sposób czytania zaznaczonych elementów z listy opisałem wyżej.

Pobieranie informacji z listy i jej modyfikacja

Jak zauważyliście wszystkie opisane dotąd atrybuty i metody pozwalające odczytać wybór użytkownika podają numery wybranych elementów. Jednak oczywistym jest, że interesuje nas zawartość tych elementów. Dostęp do zawartości daje metoda MUIM_List_GetEntry. Oto przykład dostępu do dziesiątego elementu listy:

struct Towar *towar;

DoMethod (list, MUIM_List_GetEntry, 10, towar);


W zmiennej 'towar' zostaje umieszczony adres struktury dziesiątego elementu. Jako numer elementu można też podać wartość MUIA_List_GetEntry_Active, co jak łatwo zgadnąć, da nam dostęp do aktualnie aktywnego elementu. Jeżeli podany numer elementu nie istnieje (albo zażądamy podania elementu aktywnego, a żaden element nie jest w tym stanie) w zmiennej zostanie umieszczone zero - warto wziąć to pod uwagę przy pisaniu kodu. Po uzyskaniu wskaźnika do struktury elementu możemy dokonać na nim modyfikacji bezpośrednio lub na elemencie odłączonym od listy. Modyfikacja bezpośrednia jest prosta do wykonania, ale nieco ryzykowna. Oto jak może wyglądać (załóżmy, że modyfikujemy 10 element otrzymany w kodzie powyżej):

towar->ile_na_skladzie = 17;
DoMethod (list, MUIM_List_Redraw, 10);


Metoda MUIM_List_Redraw powoduje uwidocznienie wprowadzonych zmian. Jednak bezpośrednia modyfikacja niesie ze sobą wiele ukrytych niebezpieczeństw. Wyobraźmy sobie, że w podobny sposób zmienimy pole 'nazwa' w strukturze Towar. W normalnym przypadku hook konstrukcyjny wykonywał kopię tekstu, a więc wskazany tekst mógł być nawet zmienną lokalną. Jeżeli zmienną lokalną użyjemy do modyfikacji bezpośredniej, przy pierwszym odświeżaniu listy zobaczymy tak zwane "krzaki"... Kolejne problemy przyniesie destruktor - będzie on próbował zwolnić pamięć, w której znajduje się tekst - co będzie jeżeli tekst znajduje się w kodzie programu, albo w danych jakiegoś innego obiektu (np. obiektu String)? Kolejny problem sprawią nam listy z automatycznym sortowaniem - jeżeli modyfikacja bezpośrednia może spowodować zmianę porządku elementów, trzeba ręcznie wywołać metodę MUIM_List_Sort.

Wiele z tych niebezpieczeństw może być łatwo przeoczonych, dlatego zalecam (i sam stosuję w swoich programach) modyfikację elementu po odłączeniu go od listy. Oto przykład modyfikacji elementu 'Towar':

char nazwa[128];         /* zmienna lokalna */
long ile_na_skladzie;
struct Towar *towar, nowy_towar;

DoMethod (list, MUIM_List_GetEntry, 10, &towar);
sprintf (nazwa, "%s NG", towar->nazwa);               /* modyfikuję */
ile_na_skladzie = towar->ile_na_skladzie + 10;        /* modyfikuję */
DoMethod (list, MUIM_List_Remove, 10);

nowy_towar->nazwa = nazwa;
nowy_towar->ile_na_skladzie = ile_na_skladzie;
DoMethod (list, MUIM_List_InsertSingle, &nowy_towar, 10);


Jak widać wyciągam ze struktury Towar niezbędne elementy i modyfikuję je. Następnie (nie przedtem, inaczej będę odczytywał pola struktury z już zwolnionej pamięci!) usuwam element z listy, a z zachowanych (i zmodyfikowanych) składowych buduję nowy element i dołączam go do listy. Dzięki temu zarówno konstruktor jak i destruktor będzie mógł poprawnie wykonać operacje na pamięci przeznaczonej na kopię elementu. Jeżeli lista jest automatycznie sortowana, wystarczy przy wstawianiu podać MUIV_List_Insert_Sorted zamiast konkretnej pozycji. Również odświeżenie listy na ekranie zostanie wykonane automatycznie. Jak widać ta z pozoru bardziej skomplikowana metoda modyfikacji elementów jest dużo prostsza w użyciu i pozwala uniknąć wielu problemów, które mogą pogorszyć stabilność programu.

Te dziewięć części cyklu zamknęło pierwszy jego etap, który można nazwać oswajaniem się z MUI i jego wewnętrzną strukturą. Nie omówiłem oczywiście wszystkich klas wbudowanych, ale nie jest moim celem zastąpienie dokumentacji MUI, starałem się pokazać typowe sposoby używania obiektów i zapoznać Was z filozofią tej biblioteki. Teraz nadchodzi pora na bardziej zaawansowaną wiedzę - od przyszłego miesiąca zajmiemy się tworzeniem własnych klas MUI. Dodatek - tutaj.

    
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