Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: 'Złośliwiec'
Biblioteki C++

Drzewo (TreeView)

[lekcja]

Tworzenie drzewa

Jest to jedna z najprzydatniejszych kontrolek (wiele rzeczy można przedstawić za pomocą drzewiastej struktury), a zarazem jedna z najtrudniejszych w użyciu w niektórych swoich aspektach. A przynajmniej tak by wynikało z licznych pytań o nią na różnych forach internetowych. Za chwilę przekonamy się, że nie taki diabeł straszny. TreeView, czyli po prostu drzewo, wygląda mniej więcej tak:

Drzewko obrazujące strukturę folderów w Eksploratorze Windows (Windows XP)
Drzewko obrazujące strukturę folderów w Eksploratorze Windows (Windows XP)

Tworzymy je oczywiście za pomocą CreateWindowEx, podając jako nazwę klasy stałą WC_TREEVIEW albo łańcuch "SysTreeView":

C/C++
HWND hTree = CreateWindowEx( WS_EX_CLIENTEDGE, WC_TREEVIEW, "Drzefko",
WS_CHILD | WS_VISIBLE | WS_BORDER | TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT,
5, 5, 300, 450, g_hwnd,( HMENU ) 1001, hThisInstance, NULL );

Oczywiście efekt takiego zabiegu jest na razie mało imponujący, bowiem
otrzymaliśmy tylko pusty prostokąt. Aby dodać jakieś elementy do
drzewa, musimy zaznajomić się ze strukturą TVITEM, która
(jak sama nazwa wskazuje) reprezentuje pojedynczy element drzewa. Oto
ona:

C/C++
typedef struct tagTVITEM
{
   
UINT mask;
   
HTREEITEM hItem;
   
UINT state;
   
UINT stateMask;
   
LPTSTR pszText;
   
int cchTextMax;
   
int iImage;
   
int iSelectedImage;
   
int cChildren;
   
LPARAM lParam;
} TVITEM, * LPTVITEM;

Najważniejsze pole to mask - określa ono, które z pozostałych pól są prawidłowe i mogą być wykorzystane. Struktura TVITEM może być wykorzystana podczas dodawania elementu bądź do pobierania/ustawiania informacji o nim. Mogą to być dowolnie (prawie) przez nas wybrane informacje i właśnie dlatego stosuje się tutaj maskę.
 
Pole hItem przydaje się, kiedy chcemy zmodyfikować już istniejący element, np. zmienić jego tekst. Podczas dodawania nowego elementu pole to nie jest wykorzystywane.
 
Pola pszText i cchTextMask, jak same ich nazwy wskazują, pozwalają ustawić tekst elementu. Jeśli pole mask ma ustawioną flagę TVIF_TEXT, to oznacza, że zarówno pszText, jak i cchTextMask zawierają prawidłowe wartości (jednak wartość cchTextMask wykorzystywana jest tylko do pobierania informacji o elemencie, zaś gdy struktura służy do ustawiania informacji, wówczas to pole jest ignorowane).
 
Pola iImage oraz iSelectedImage pozwalają ustawić ikonki, wyświetlane po lewej stronie tekstu elementu.
 
Pole cChildren udostępnia nam informację, ile dzieci zawiera dany element (tj. ile elementów jest bezpośrednio "pod" nim).
 
Wreszcie ostatnie pole, lParam, pozostaje całkowicie do naszej dyspozycji. Typowym jego przeznaczeniem jest przechowywanie wskaźnika na jakąś strukturę, zawierającą dalsze informacje. Dzięki temu nie musimy implementować w naszym programie osobnej drzewiastej struktury, gdyż wszystkie informacje o drzewie możemy wrzucić do struktury wskazywanej przez lParam.
 
Najwyższa pora dowiedzieć się, jak się dodaje element. Na dobry początek zadowolimy się elementami tekstowymi, bez ikonek. Niestety, musimy się tu zapoznać z kolejną strukturą - krótszą niż poprzednia, ale deklarowaną w trochę bardziej zawikłany sposób:
 
C/C++
typedef struct tagTVINSERTSTRUCT
{
   
HTREEITEM hParent;
   
HTREEITEM hInsertAfter;
   
#if (_WIN32_IE >= 0x0400)
   
union
   
{ TVITEMEX itemex;
       
TVITEM item;
   
} DUMMYUNIONNAME;
   
#else
   
TVITEM item;
   
#endif
} TVINSERTSTRUCT, * LPTVINSERTSTRUCT;

Jak widzimy, podstawowe składniki struktury TVINSERTSTRUCT to uchwyty: hParent i hInsertAfter, oraz struktura, którą już znamy, zawierająca informacje o wstawianym elemencie. Pole hParent określa, jaki element będzie rodzicem elementu właśnie wstawianego, zaś hInsertAfter - jaka będzie jego pozycja względem ewentualnego "rodzeństwa" (elementów mających tego samego rodzica).
 
Widzimy również, że na systemach z zainstalowanym Internet Explorerem w wersji 4.0 i wyższej (czyli praktycznie we wszystkich obecnie spotykanych wersjach Windows :-) ) mamy możliwość, by zamiast TVITEM użyć TVITEMEX. Różnica między tymi dwiema strukturami nie jest wszakże na tyle istotna, by sobie nią tutaj zawracać głowę :-).
 
Wiemy, na czym będziemy operować, zatem do dzieła. Najpierw wypełnijmy strukturę informacji o elemencie:

C/C++
TVITEM tvi;
tvi.mask = TVIF_TEXT;
tvi.pszText = "Element Jeden";

Teraz gotową strukturę możemy wrzucić do odpowiedniego pola struktury TVINSERTSTRUCT, a także określić w tej ostatniej pozycję, gdzie wyląduje nasz nowy element. Ponieważ na razie będzie to jedyny element na drzewie, więc nie mamy zbyt wielkiego wyboru. Może on być tylko przypięty do korzenia drzewa. W tym celu zamiast "normalnego" uchwytu dajemy stałą TVI_ROOT albo po prostu NULL. Podobnie postępujemy z polem hInsertAfter:
C/C++
TVINSERTSTRUCT tvins;
tvins.item = tvi;
tvins.hParent = tvins.hInsertAfter = TVI_ROOT;

Dalszy ciąg jest już bardzo prosty - wystarczy wywołać makro TreeView_InsertItem z adresem naszej struktury tvis. Makro wyśle komunikat TVM_INSERTITEM (możemy to też zrobić bezpośrednio, tylko po co?).
C/C++
HTREEITEM hItem1 = TreeView_InsertItem( hTree, & tvins );

Element już dodany, dostaliśmy też do niego uchwyt (hItem). Żeby taki listek nie usechł z tęsknoty, możemy mu w trybie natychmiastowym wyhodować trochę rodzeństwa i potomstwa:
C/C++
tvi.pszText = "Element Dwa";
tvins.item = tvi;
tvins.hInsertAfter = TVI_LAST; // ...albo hItem1HTREEITEM
HTREEITEM hItem2 = TreeView_InsertItem( hTree, & tvins );

tvi.pszText = "Element Trzy";
tvins.item = tvi;
tvins.hInsertAfter = TVI_LAST; // ...albo hItem2HTREEITEM
HTREEITEM hItem3 = TreeView_InsertItem( hTree, & tvins );

tvi.pszText = "Dziecko Elementu Jeden";
tvins.item = tvi;
tvins.hParent = hItem1;
tvins.hInsertAfter = TVI_FIRST; // ...albo TVI_LASTHTREEITEM
HTREEITEM hItemChild = TreeView_InsertItem( hTree, & tvins );

Mieliśmy tu trochę zabawy z ustalaniem hierarchii nowo dodawanych elementów, co mniej więcej powinno dać obraz, jak się to robi. Warto wspomnieć o jeszcze jednym bajerze - pole hInsertAfter możemy również ustawić na TVI_SORT, co pozwoli nam na alfabetyczne sortowanie dodawanych elementów. Zwróć uwagę, że aby posortować całe drzewo, musimy wszystkie jego elementy dodać z hInsertAfter ustawionym na TVI_SORT.

Ikonki elementów


Przedstawienie niektórych danych w postaci drzewa z pewnością znacznie poprawia ich czytelność, ale czegoś nam tu wyraźnie brakuje. Chodzi oczywiście o ikonki. Możemy narysować własne albo użyć systemowych ikon. W obu przypadkach musimy skorzystać z usług innej kontrolki - ImageList. Nie jest ona, w odróżnieniu od większości kontrolek, elementem graficznego interfejsu i nie posiada swojej graficznej reprezentacji. Służy wyłącznie do przechowywania wielu bitmap w pamięci i ogólnie zarządzania nimi.
 
Na szczęście obsługa takich list nie należy do szczególnie trudnych zadań. Najpierw tworzymy listę za pomocą funkcji ImageList_Create, następnie poszczególne obrazki wczytujemy i dodajemy na listę za pomocą ImageList_Add, np.:
 
C/C++
HIMAGELIST himl;
HBITMAP hbmp;
SHORT nLeaf, nNodeClosed, nNodeOpen;

himl = ImageList_Create( 16, 16, FALSE, 3, 0 ) );
hbmp = LoadBitmap( g_hinst, MAKEINTRESOURCE( IDB_LEAF ) );
nLeaf = ImageList_Add( himl, hbmp,( HBITMAP ) NULL );
DeleteObject( hbmp );

hbmp = LoadBitmap( g_hinst, MAKEINTRESOURCE( IDB_NODECLOSED ) );
nNodeClosed = ImageList_Add( himl, hbmp,( HBITMAP ) NULL );
DeleteObject( hbmp );
hbmp = LoadBitmap( g_hinst, MAKEINTRESOURCE( IDB_NODEOPEN ) );
nNodeOpen = ImageList_Add( himl, hbmp,( HBITMAP ) NULL );
DeleteObject( hbmp );
TreeView_SetImageList( hTree, himl, TVSIL_NORMAL );

Pierwsze dwa parametry funkcji ImageList_Create to wymiary obrazka. Trzeci to flagi - nie używamy ich tutaj. Pozostałe dwa parametry oznaczają odpowiednio: liczbę obrazków, jakie początkowo znajdują się na liście oraz o ile lista może się rozszerzyć w niedalekiej przyszłości. Oba te parametry mają znaczenie w optymalizacji działania naszego programu w systemie.
 
Po dodaniu wszystkich trzech obrazków (tyle zazwyczaj potrzebujemy dla drzewa, choć oczywiście może ich być więcej) musimy jeszcze przypisać stworzoną właśnie listę obrazków do naszego TreeView, co czynimy w ostatniej linijce powyższego przykładu. Teraz możemy zmodyfikować elementy naszego drzewka, przypisując im odpowiednie indeksy obrazków z listy.
 
Ale skąd wziąć systemowe ikonki - takie, jakie występują np. w liście folderów Eksploratora Windows? Cóż, wygląda na to, że twórcy naszego kochanego systemu nie zatroszczyli się o jakiś łatwy sposób, a już zwłaszcza o udokumentowanie poszczególnych ikon. Nic to jednak, poradzimy sobie. Przede wszystkim musimy wywołać Shell_GetImageLists. Funkcja powinna występować we wszystkich wersjach Windowsów od 95 w górę, chociaż w dokumentacji jest napisane, że istnieje tylko pod XP. Niestety, aby odwołać się do niej z wcześniejszej wersji Windows, musimy zrobić parę sztuczek:

C/C++
typedef BOOL( WINAPI * SHGIL_PROC )( HIMAGELIST * phLarge, HIMAGELIST * phSmall );
typedef BOOL( WINAPI * FII_PROC )( BOOL fFullInit );

Te dwa typy wskaźników do funkcji będą nam potrzebne, aby "wyciągnąć" potrzebną nam funkcję z pliku shell32.dll. Przypominam, że nie musimy tego robić pod Windows XP, ale warto stosować tę metodę dla zachowania kompatybilności naszej aplikacji wstecz. Z kolei pod Oknami klasy NT występuje inna trudność - aby móc skorzystać z systemowej listy obrazków, musimy najpierw wywołać funkcję (nieudokumentowaną!) FileIconInit.
Nie ma jej w systemach 95/98/ME, więc ten fakt również musimy uwzględnić.
 
Teraz możemy pobrać adresy funkcji Shell_GetImageLists i FileIconInit. Jeśli czytałeś już odcinek o DLL, nie sprawi ci to wielkiego problemu. Jednak i tu tkwi pewien haczyk. Otóż funkcji tych nie możemy (na określonych platformach) pobrać po nazwie. Musimy znać ich numery porządkowe (ang. ''ordinals''). U nas wynoszą one, odpowiednio: 71 i 660. Jeśli interesują cię inne funkcje, które w pewnych wersjach Windows "istniały tylko jako numery", ich pełną listę znajdziesz na tej stronie

C/C++
HIMAGELIST himlLarge, himlSmall;
SHGIL_PROC Shell_GetImageLists;
FII_PROC FileIconInit;
if( g_hShell32 == 0 )
   
 g_hShell32 = LoadLibrary( "shell32.dll" );

Shell_GetImageLists =( SHGIL_PROC );
GetProcAddress( g_hShell32,( LPCSTR ) 71 );
FileIconInit =( FII_PROC );
GetProcAddress( g_hShell32,( LPCSTR ) 660 );

Zwróć uwagę, że uchwyt do biblioteki shell32.dll jest globalny. Wynika to stąd, że nie możemy zwolnić tej biblioteki przed zakończeniem naszej aplikacji - w przeciwnym razie zostaną zniszczone nasze listy z obrazkami, co oczywiście nie byłoby rzeczą pożądaną :-).
 
C/C++
HMODULE g_hShell32 = NULL;

Teraz pozostaje nam jedynie wywołać otrzymane z takim trudem funkcje w celu uzyskania dostępu do systemowych list obrazków. Jak już pewnie zauważyłeś, są aż dwie takie listy - jedna zawiera normalne ikonki(32x32), druga - ich miniatury (16x16).
 
C/C++
if( FileIconInit != 0 ) FileIconInit( TRUE ); // funkcja obecna tylko w Windows NT

Shell_GetImageLists( & himlLarge, & himlSmall );

Zgodnie z tym, co powiedzieliśmy sobie wcześniej, FileIconInit występuje tylko w serii NT, zatem jeśli nie uda się pobrać jej adresu (będzie on równy 0), to oczywiście nie wywołujemy jej.
 
Mamy wreszcie uchwyty list z obrazkami. Problem w tym, że wciąż nie wiemy, co właściwie się na tych listach znajduje. Jak już wspomniałem, Microsoft nie pofatygował się z udokumentowaniem gdziekolwiek ich zawartości. Jedynym zatem sposobem pozostaje napisanie programu, który wyświetli wszystkie ikony systemowe z listy. Na szczęście ktoś już to zrobił za nas – http://www.catch22.net/tuts/sysimg.asp.
To właśnie z tej stronki pochodzi omówiony przez nas przed chwilą sposób dobrania się do systemowych obrazków. Jest tam też do ściągnięcia przykładowy program, który właśnie wyświetla pełną listę systemowych ikon. Jak nietrudno zauważyć, oprócz tych stricte systemowych widnieją tam również ikony dodane przez rozmaite programy, które zainstalowaliśmy na naszym komputerze. Nas jednak interesuje tylko początek tej listy i ikonki: pliku (indeks 0), zamkniętego (3) i otwartego (4) foldera. Możemy teraz wreszcie ustawić te indeksy elementom drzewa. Zaczniemy oczywiście od przypisania jednej z dwóch otrzymanych list (użyjemy tylko małe ikony) drzewu:
 
C/C++
TreeView_SetImageList( hTree, himlSmall, TVSIL_NORMAL );

Teraz ustawimy indeksy. Przy okazji dowiemy się, jak modyfikować istniejące już elementy drzewa oraz jak można wykorzystać pole lParam struktury TVITEM. W tym celu zadeklarujemy sobie strukturę TEST (nie będziemy jej do niczego używać), a wskaźnik do zmiennej typu TEST zapamiętamy w polu lParam każdego elementu drzewa. Do dzieła:
 
C/C++
struct TEST
{
   
int Bla;
   
char BlaBla;
   
double BlaBlaBla;
};

TEST Tab[ 4 ];
TVITEM tvi;
tvi.mask = TVIW_HANDLE | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM;
tvi.iImage = tvi.iSelectedImage = 3;
tvi.hItem = hItem1;
tvi.lParam =( LPARAM ) & Tab[ 0 ];
TreeView_SetItem( hTree, & tvi );
tvi.hItem = hItem2;
tvi.lParam =( LPARAM ) & Tab[ 1 ];
TreeView_SetItem( hTree, & tvi );
tvi.hItem = hItem3;
tvi.lParam =( LPARAM ) & Tab[ 2 ];
TreeView_SetItem( hTree, & tvi );
tvi.hItem = hItemChild;
tvi.lParam =( LPARAM ) & Tab[ 3 ];
TreeView_SetItem( hTree, & tvi );

Nasze drzewo od razu nabrało cywilizowanego wyglądu :-).

Dynamiczne ikonki


Drzewo od razu nabrało cywilizowanego wyglądu, ale wciąż jesteśmy daleko od ideału. Ustawiliśmy mianowicie jedną i tę samą ikonkę wszystkim elementom. Moglibyśmy wprawdzie ustawić różne indeksy dla iImage oraz iSelectedImage, ale efekt niekoniecznie byłby taki, jak oczekujemy. Przyzwyczailiśmy się bowiem, że ikonka elementu zmienia się wtedy, gdy ten zostanie rozwinięty, podczas gdy iSelectedImage związana jest z zaznaczeniem elementu (a więc ikona zmieniałaby się po samym kliknięciu na element).
 
Rozwiązaniem jest dynamiczne przypisywanie indeksu ikonce. Aby skorzystać z tej cechy, zamiast indeksu obrazka zostawiamy w polach iImage oraz iSelectedImage wartość I_IMAGECALLBACK.Od tej pory kontrolka TreeView wysyła nam powiadomienia TVN_GETDISPINFO, kiedy tylko potrzebuje pobrać aktualny indeks obrazka dla elementu. Powiadomienia te wysyłane są w formie WM_NOTIFY, a obsłużyć je możemy następująco:
 
C/C++
case WM_NOTIFY:
{
   
if((( LPNMHDR ) lParam )->code == TVN_GETDISPINFO )
   
{
       
LPNMTVDISPINFO lptvdi =( LPNMTVDISPINFO ) lParam;
       
TVITEM item = lptvdi->item;
       
TVITEM tvi;
       
tvi.mask = TVIF_HANDLE | TVIF_STATE | TVIF_CHILDREN;
       
tvi.hItem = item.hItem;
       
tvi.stateMask = TVIS_EXPANDED;
       
TreeView_GetItem( hTree, & tvi );
       
int nImageIndex;
       
       
if( tvi.cChildren == 0 )
           
 nImageIndex = 1;
       
else
       
{
           
if( tvi.state & TVIS_EXPANDED )
               
 nImageIndex = 4;
           
else
               
 nImageIndex = 3;
           
       
}
       
       
lptvdi->item.iImage = nImageIndex;
       
lptvdi->item.iSelectedImage = nImageIndex;
   
}
}
break;

Struktura typu NMTVDISPINFO, do której wskaźnik dostajemy w lParam, zawiera (poza standardowym nagłówkiem powiadomieniowym) pole item, które jest zmienną znanego nam już doskonale typu TVITEM. Pole item.mask zawierać może w tym przypadku jedną lub kilka spośród flag: TVIF_CHILDREN, TVIF_TEXT, TVIF_IMAGE, TVIF_SELECTEDIMAGE. Nas interesują dwie ostatnie. Nie obchodzi nas przy tym, która z nich jest ustawiona, w obu przypadkach zwrócimy ten sam indeks (dlatego nie sprawdzamy, co zawiera lptvdi->mask). Wynik będzie zależał od czego innego, mianowicie od liczby ewentualnych dzieci danego elementu oraz od jego stanu (rozwinięty lub zwinięty). Informacje te zwykle zawarte są w strukturze TVITEM, ale tutaj akurat nie możemy skorzystać z tej, do której dotarliśmy przez lParam, ponieważ nie
zawiera ona potrzebnej nam informacji o ewentualnych dzieciach. Dlatego wywołujemy jeszcze TreeView_GetItem.
 
Efekt jest już całkiem przyzwoity: elementy, które nie posiadają dzieci mają jedną ikonkę, elementy-rodzice drugą, a jeśli rozwiniemy takiego rodzica (np. klikając przycisk '+' przy nim), to jego ikonka zmieni się na otwarty folder.

Reakcja na zdarzenia


Oprócz wyświetlania w drzewie różnych rzeczy prawdopodobnie chcielibyśmy również np. wykonać jakąś akcję, gdy użytkownik kliknie na jakimś elemencie. Możemy wykryć takie zdarzenie poprzez obsługę powiadomień. Drzewo wysyła zarówno standardowe powiadomienia (np. NM_CLICK, NM_RCLICK), jak i specyficzne dla siebie (np. TVN_SELCHANGED, oznaczające, że użytkownik zaznaczył właśnie jakiś element). Tak więc możemy dodać do naszej obsługi WM_NOTIFY kolejne przypadki:
 
C/C++
case WM_NOTIFY:
{
   
switch((( LPNMHDR ) lParam )->tt )
   
{
   
case TVN_GETDISPINFO:
       
{
           
... // nie będziemy się powtarzać ;-)
       
}
       
break;
       
   
case TVN_SELCHANGED:
       
{
           
HTREEITEM hCurrentItem = TreeView_GetSelection( hTree );
       
}
       
break;
       
   
}
}
break;

W tym przypadku pobieramy tylko indeks aktualnie zaznaczonego elementu; co z nim zrobimy, to już mało istotne :-). Obsługa takiego powiadomienia może się nam przydać np. do wyświetlenia dodatkowych informacji o elemencie zaznaczonym prez użytkownika. Tutaj z kolei może nam się przydać struktura, do której wskaźnik mamy w TVITEM::lParam.
 
Często zdarza się, że chcemy, by po kliknięciu prawym przyciskiem na elemencie pojawiało się menu kontekstowe. Możemy w tym celu obsłużyć NM_RCLICK. Powiadomienie to nie dostarcza nam niestety informacji o dokładnym miejscu kliknięcia, więc aby dowiedzieć się, na jakim elemencie użytkownik kliknął, korzystamy z makra TreeView_HitTest, podając mu jako parametr adres struktury TVHITTESTINFO. Struktura ta zawiera trzy pola: pt, flags, hItem. Do pola pt wpisujemy współrzędne punktu, o którym chcemy się dowiedzieć, jaką część drzewa stanowi - w naszym przypadku chodzi o współrzędne kursora myszy, które możemy pobrać za pomocą GetCursorPos(). Gdy komunikat wysłany przez makro wróci, pole flags będzie zawierało informację o tym, nad jaką częścią drzewa znalazł się kursor, a hItem będzie uchwytem do elementu pod kursorem (albo będzie równe NULL, jeśli mysz nie znajduje się nad żadnym elementem).

Dynamiczny kolor i czcionka


Przydatną cechą byłaby zmiana kolorów elementów tudzież niektórych atrybutów ich czcionki w pewnych sytuacjach. Na przykład moglibyśmy chcieć, aby dało się "wyłączać" wybrane elementy (wtedy byłyby wyświetlane na szaro), albo wypisywać pogrubioną czcionką te elementy, które zawierają w nazwie podane przez użytkownika słowo. Na szczęście nie musimy się uciekać do subclassingu, aby tego dokonać.
 
Istnieje bowiem specjalne powiadomienie – NM_CUSTOMDRAW. Parametr lParam tego powiadomienia zawiera wskaźnik na pewną strukturę. Jeśli mamy pewność, że powiadomienie przyszło od naszego drzewka, to będzie to struktura typu NMTVCUSTOMDRAW. Zawiera ona sporo ciekawych rzeczy, z których szczególnie ciekawe jest dla nas w tym momencie pole nmcd.dwDrawStage. Mówi ono, w którym stadium rysowania elementu się obecie znajdujemy. Albo innymi słowy, jakich informacji żąda od nas kontrolka, która chce się narysować (drzewo).
 
I tak jeśli pole to jest równe CDDS_PREPAINT, to oznacza, że drzewo pyta nas, czy chcemy od niego dostawać dalsze szczegółowe komunikaty "z frontu", czyli na temat rysowania poszczególnych elementów, a jeśli tak, to jakie. Interesuje nas informacja o odrysowywaniu elementu, więc odpowiadamy wartością CDRF_NOTIFYITEMDRAW. Od tej pory drzewo usłużnie powiadamia nas, co i kiedy chce sobie narysować. Tuż przed narysowaniem tego czegoś dostaniemy powiadomienie, dla którego dwDrawStage będzie równe CDDS_ITEMPREPAINT. To jest właściwy moment, by podpowiedzieć drzewku, jakiej czcionki i koloru powinno użyć do rysowania elementu. Struktura NMTVCUSTOMDRAW zawiera gdzieś między innymi uchwyt do elementu, więc możemy go wykorzystać do pobrania informacji o tym elemencie i wybrania właściwej czcionki/koloru. Konkretnie uchwyt ten jest upychany w polu nmcd.dwItemSpec (oczywiście po odpowiedniej konwersji).
 
Dość gadania, zmieńmy ten kolor. Pójdziemy tym razem na łatwiznę i nie wykorzystamy wspomnianego uchwytu. Zamiast tego pobawimy się polem iLevel struktury NMTVCUSTOMDRAW (pamiętając, że jest ono dostępne w Common Controls w wersji 4.71 lub nowszej). Oznacza ono poziom zagnieżdżenia elementu (0 - korzeń, 1 - dziecko korzenia itd.). Naszym celem niech będzie pomalowanie na niebiesko elementów o poziomie większym lub równym 1.

C/C++
case WM_NOTIFY:
{
   
switch((( LPNMHDR ) lParam )->code )
   
{
   
case TVN_GETDISPINFO:
       
{
           
... //nie będziemy się powtarzać ;-)
       
}
       
break;
   
case TVN_SELCHANGED:
       
{
           
... //nie będziemy się powtarzać ;-)
       
}
       
break;
   
case NM_RCLICK:
       
{
        }
       
break;
   
case NM_CUSTOMDRAW:
       
{
           
LPNMTVCUSTOMDRAW lpNMCustomDraw =( LPNMTVCUSTOMDRAW ) lParam;
           
switch( lpNMCustomDraw->nmcd.dwDrawStage )
           
{
           
case CDDS_PREPAINT:
               
return CDRF_NOTIFYITEMDRAW;
           
case CDDS_ITEMPREPAINT:
               
{
                   
if( lpNMCustomDraw->iLevel == 1 )
                       
 lpNMCustomDraw->clrText = RGB( 0, 0, 255 );
                   
else
                       
 return CDRF_DODEFAULT;
                   
                   
return CDRF_NEWFONT;
               
}
            }
        }
       
break;
   
}
}
break;


Zmiany koloru tekstu (w podobny sposób możemy również zmienić kolor tła) dokonujemy przez wpisanie żądanej wartości w pole clrText omawianej struktury. Gdybyśmy chcieli do tego zmienić czcionkę, możemy przypisać ją do hdc "wyciągniętego" ze struktury nmcd. Bez względu na to, co zmieniamy w wyświetlanym tekście, musimy zwrócić CDRF_NEWFONT. Jeśli nie zmieniamy nic (tak, jak w powyższym przykładzie dla elementów poziomu różnego od 1), to zwracamy CDRF_DODEFAULT.

Drzewo z niebieskimi liściami... Sen wariata? Niekoniecznie. (Windows XP)
Drzewo z niebieskimi liściami... Sen wariata? Niekoniecznie. (Windows XP)

Zwrócenie odpowiedniej wartości jest tutaj sprawą życia i śmierci niemalże, więc pojawi się problem, gdy nasze drzewo umieszczone jest w oknie dialogowym posiadającym własną procedurę okienkową. Jak wiemy, procedura taka może zwracać jedynie TRUE lub FALSE. Dlatego warto poznać sposób, który służy do obejścia tego ograniczenia. Istnieje pewien atrybut okna dialogowego, który możemy ustawić za pomocą SetWindowLong (a właściwie SetWindowLongPtr, gdybyśmy chcieli zachować zgodność z najnowszymi trendami ;-P). Wartość, którą chcemy zwrócić, umieszczamy właśnie w tym atrybucie:

C/C++
SetWindowLongPtr( hwndDialog, DWLP_MSGRESULT,( LONG ) CDRF_NEWFONT );

...po czym spokojnie zwracamy TRUE. System po otrzymaniu naszego TRUE sprawdzi najpierw wartość, przechowywaną w atrybucie DWLP_MSGRESULT. Jeśli komunikat, na który odpowiadamy, wymagał zwrócenia jakiejś wartości, to zostanie użyta ta z atrybutu DWLP_MSGRESULT. W ten sposób mamy pełną obsługę NM_CUSTOMDRAW także i w dialogu.

Lista CheckBox-ów

Kontrolka TreeView posiada jeszcze jedną ciekawą cechę. Możemy wyświetlać CheckBox-y obok elementów (dokładniej: między ikonką, a tekstem). Wystarczy dołączyć styl TVS_CHECKBOXES przy tworzeniu drzewa:
 
C/C++
HWND hTree = CreateWindowEx( WS_EX_CLIENTEDGE, WC_TREEVIEW, "Drzefko", WS_CHILD | WS_VISIBLE |
WS_BORDER | TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | TVS_CHECKBOXES,
5, 5, 300, 450, g_hwnd,( HMENU ) 1001, hThisInstance, NULL );

Takie rzeczy też mogą wyrosnąć na drzewie (Windows XP)
Takie rzeczy też mogą wyrosnąć na drzewie (Windows XP)

Oczywiście nic nie stoi na przeszkodzie, aby schować ikonki, linie i przyciski "+", "-". Wszystko wedle indywidualnych potrzeb. Bez względu na to musimy jednak mieć jakiś sposób na ustawienie i odczytanie stanu każdego CheckBox-a (zaznaczony lub nie). Dokumentacja do Platform SDK zaleca napisanie sobie takiej oto funkcji:

C/C++
BOOL TreeView_SetCheckState( HWND hwndTreeView, HTREEITEM hItem, BOOL fCheck )
{
   
TVITEM tvItem;
   
tvItem.mask = TVIF_HANDLE | TVIF_STATE;
   
tvItem.hItem = hItem;
   
tvItem.stateMask = TVIS_STATEIMAGEMASK;
   
tvItem.state = INDEXTOSTATEIMAGEMASK(( fCheck ? 2: 1 ) );
   
return TreeView_SetItem( hwndTreeView, & tvItem );
}

Służy ona oczywiście do zmiany stanu zaznaczenia podanego elementu. Aby zaś sprawdzić, w jakim aktualnie jest on stanie, możemy napisać funkcję:
 
C/C++
BOOL TreeView_GetCheckState( HWND hwndTreeView, HTREEITEM hItem )
{
   
TVITEM tvItem;
   
tvItem.mask = TVIF_HANDLE | TVIF_STATE;
   
tvItem.hItem = hItem;
   
tvItem.stateMask = TVIS_STATEIMAGEMASK;
   
TreeView_GetItem( hwndTreeView, & tvItem );
   
return(( BOOL )( tvItem.state >> 12 ) - 1 );
}

Teraz obsługa CheckBox-ów na drzewie będzie dokładnie tak prosta, jak być powinna. Nawiasem mówiąc miło, że panowie z MS podpowiadają nam, jak stworzyć te funkcje, jednak z pewnością jeszcze milej by było, gdyby je po prostu dołączyli do API. Widocznie nie można mieć wszystkiego.
 
Żeby nie zakończyć odcinka narzekaniem, podsumujmy nasze osiągnięcia :-). Dowiedzieliśmy się, jak stworzyć kontrolkę TreeView i dodawać do niego elementy o dowolnej hierarchii. Nauczyliśmy się ustawiać ikonki elementów i wyciągać z systemu standardowe ikony. Następnie sprawiliśmy, że drzewo zaczęło reagować na poczynania użytkownika. Upiększyliśmy je trochę, dorzucając trochę kolorów do standardowej, pogrzebowej czerni, a wreszcie wzbogaciliśmy całość o przydatne CheckBox-y. Oczywiście jest to tylko część funkcjonalności, którą oferuje nam drzewo. Można by jeszcze pogadać o edycji etykiet, wyświetlaniu podpowiedzi, hot-trackingu i paru innych bajerach, ale coby wtedy zostało dla czytelników do samodzielnych eksperymentów? ;-)
Poprzedni dokument Następny dokument
StatusBar ListView