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

ScrollBar

[lekcja] Rozdział 30. Omówienie suwaka poziomego i pionowego od podstaw: tworzenie, inicjalizacja oraz jego obsługa czyli przesuwanie.
Ucząc się programowania, zaczynamy tworzyć coraz to bardziej złożone programy, które posiadają coraz to więcej przycisków i innych dobrodziejstw interfejsu użytkownika. Jednak prędzej czy później okaże się, że nasze okno programu jest zbyt małe by pomieścić to wszystko. Oczywiście możemy powiększyć okno, ale nasz monitor też prędzej czy później się cały zapełni, i co wtedy? Na szczęście z pomocą przychodzą nam paski przesuwu, czyli ScrollBary:
To są właśnie ScrollBary (Windows 98)
To są właśnie ScrollBary (Windows 98)
ScrollBar jest kontrolką umożliwiającą przesuwanie w poziomie i w pionie. Oto jak jest zbudowany ScrollBar:
Anatomia scrollbara
Anatomia scrollbara

Tworzenie ScrollBara

Samo tworzenie wygląda tak samo, jak w przypadku innych kontrolek. Nazwa klasy to SCROLLBAR. Domyślnie tworzony jest pasek poziomy, natomiast do stworzenia paska pionowego trzeba użyć stylu SBS_VERT. Zaczniemy od stworzenia globalnego uchwytu:
C/C++
HWND g_hScrollBar = NULL;
I teraz możemy już stworzyć ScrollBar:
C/C++
g_hScrollBar = CreateWindowEx( 0, "SCROLLBAR", NULL, WS_CHILD | WS_VISIBLE, 50, 20, 220, 21, hwnd, NULL, hInstance, NULL );
Powyższy kod utworzy nam samodzielny pasek przesuwu. Możemy też dodać wbudowany ScrollBar do głównego okna naszego programu. Wystarczy przy tworzeniu okna dodać style WS_HSCROLL lub WS_VSCROLL w zależności od tego jaki pasek chcemy otrzymać.

Oczywiście jak to w WinAPI nie obędzie się bez problemów. Mianowicie jeśli skorzystamy z pierwszego sposobu, będziemy musieli potem ręcznie ustawić pasek przesuwu we właściwe miejsce, i z każdą zmianą rozmiaru okna na nowo go ustawiać. Oto jak to zrobić:
C/C++
RECT okno;
GetClientRect( hwnd, & okno );
int iVThumb = GetSystemMetrics( SM_CYVTHUMB );

g_hScrollBar = CreateWindowEx( 0, "SCROLLBAR", NULL, WS_CHILD | WS_VISIBLE | SBS_VERT,
okno.right - 16, okno.top, 16, okno.bottom - iVThumb,
hwnd, NULL, hInstance, NULL );
Jeśli w naszym programie zezwalamy na zmianę rozmiaru okna, to musimy wrzucić powyższy kod do obsługi komunikatu WM_SIZE.

Inicjalizacja ScrollBara

Zanim zaczniemy korzystać ze ScrollBara, musimy w nim ustawić parę rzeczy, między innymi pozycję maxymalną i minimalną, itd. W tym celu posłużymy się funkcją SetScrollInfo, która jako jeden z argumentów przyjmuje strukturę SCROLLINFO z wieloma mniej lub bardziej przydatnymi polami. Oto jak mniej więcej jest ona zadeklarowana:
C/C++
typedef struct tagSCROLLINFO {
   
UINT cbSize;
   
UINT fMask;
   
int nMin;
   
int nMax;
   
UINT nPage;
   
int nPos;
   
int nTrackPos;
} SCROLLINFO, * LPCSCROLLINFO;
Oczywiście cbSize to rozmiar struktury, który jak zawsze w WinAPI trzeba podawać. Pole fMask oznacza które pola struktury zawierają poprawne informacje, czyli te które chcemy odczytać/zapisać. Teraz nMin, nMax i nPage - cóż to takiego? nMin to odległość początku przewijanego obszaru od góry (w przypadku całego okna 0), nMax to wysokość całego okna (włącznie z niewidoczną częścią, do której trzeba będzie przewijać), natomiast nPage to wysokość obszaru, który jest widoczny (w przypadku całego okna to wysokość obszaru klienta). Oczywiście to są informacje dotyczące pionowego paska, dla poziomego będzie to długość, nie wysokość. Pole nPos to pozycja ScrollBara, natomiast pole nTrackPos jest tylko do odczytu, więc go nie ruszamy.

Skoro już wiemy (prawie) wszystko o tej strukturze, to możemy ją wypełnić:
C/C++
RECT okno;
GetClientRect( hwnd, & okno );

SCROLLINFO si;
ZeroMemory( & si, sizeof( si ) );

si.cbSize = sizeof( SCROLLINFO );
si.fMask = SIF_RANGE | SIF_PAGE | SIF_POS;
si.nMin = 0;
si.nMax = 1000;
si.nPage = okno.bottom;
si.nPos = 0;
Na początku oczywiście zerujemy strukturę i wypełniamy pole cbSize rozmiarem struktury. Potem ustawiamy pola nMin, nMax i nPos. W naszym przykładzie zakładamy, że ScrollBar będzie przewijał cały obszar klienta w pionie. Pozycję początkową ustawiamy na 0 czyli na górę ;-). Teraz przejdźmy do funkcji SetScrollInfo:

Składnia: SetScrollInfo (hwnd, fnBar, lpsi, fRedraw)

Argument Znaczenie
hwnd Uchwyt do ScrollBara lub okna aplikacji ze ScrollBarem
fnBar Jedna z trzech flag. Jeśli użyjemy SB_CTL to argument hwnd oznacza ScrollBara, natomiast przy SB_HORZ i SB_VERT hwnd oznacza okno ze ScrollBarem.
lpsi Wskaźnik na strukturę SCROLLINFO.
fRedraw Czy ScrollBar ma być odrysowany po wywołaniu funkcji.

Tak więc dla ScrollBara jako samodzielnej kontrolki wywołamy funckję SetScrollInfo następująco:
C/C++
SetScrollInfo( g_hScrollBar, SB_CTL, & si, TRUE );
A dla ScrollBara będącego częścią okna (styl WS_VSCROLL):
C/C++
SetScrollInfo( hwnd, SB_VERT, & si, TRUE );

Przesuwanie

No właśnie, tyle się natrudziliśmy, a nasz ScrollBar wciąż nie działa! Nie dość, że nie przesuwa kontrolek w oknie, to w dodatku gdy go przesuniemy, sam wraca na stare miejsce! Dlaczego tak się dzieje? Otoż nie obsłużyliśmy odpowiednio komunikatów odpowiedzialnych za przesuwanie. Są tą WM_VSCROLL i WM_HSCROLL. W niższym słowie parametru wParam znajdziemy stałą oznaczającą to, co użytkownik zrobił ze ScrollBarem. Stałe te zostały zebrane w tabelce (Uwaga: dla ScrollBara pionowego):
Stała Znaczenie
SB_TOP Użytkownik przewinął na samą górę.
SB_BOTTOM Użytkownik przewinął ScrollBar na sam dół.
SB_LINEDOWN Użytkownik kliknął strzałkę w dół.
SB_LINEUP Użytkownik kliknął strzałkę w górę.
SB_PAGEDOWN Użytkownik przewinął o stronę w dół.
SB_PAGEUP Użytkownik przewinął o stronę w górę.
SB_THUMBPOSITION Użytkownik przesunął suwak.
SB_THUMBTRACK Użytkownik jest w trakcie przesuwania suwaka (zmienił jego położenie, ale jeszcze nie puścił lewego przycisku myszki).

Ponieważ nie wszyscy mogą kojarzyć o które części ScrollBara dokładnie chodzi, zamieszczam rysunek:
Zdarzenia ScrollBara
Zdarzenia ScrollBara
Na powyższym rysunku zaznaczone są też nazwy stałych dla ScrollBara poziomego, dlatego oddzielnej tabelki nie będzie. Dodam tylko że SB_LEFT i SB_RIGHT których nie ma na rysunku to odpowiednie końce poziomego paska przesuwu. Teraz twardszy orzech do zgryzienia - jak odczytać aktualną pozycję ScrollBara? Można ją znaleźć w wyższym słowie wParam, ale:
  • Wartość odczytana w ten sposób jest 16-bitowa czyli maksymalna odczytwalna wartość to 65535, pomimo że kontrolka obsługuje wartości 32-bitowe.
  • Pozycję ScrollBara możemy odczytać tak tylko dla komunikatów SB_THUMBPOSITION i SB_THUMBTRACK.
Łatwo można dojść do wniosku, że ten sposób się nie nadaje i to prawda ;-) Instnieje funkcja GetScrollPos do odczytywania pozycji, ale jako rzecze dokumentacja, nie wolno jej używać bo jest przestarzała. W zamian trzeba używać GetScrollInfo! Funkcja ta odczytuje dane do struktury SCROLLINFO, tej samej, którą używaliśmy przy inicjalizacji ScrollBara. Jest zbudowana tak samo jak SetScrollInfo, tylko że nie ma 4 argumentu. A oto jak odczytać pozycję suwaka:
C/C++
SCROLLINFO si;
ZeroMemory( & si, sizeof( si ) );

si.cbSize = sizeof( SCROLLINFO );
si.fMask = SIF_POS;

SetScrollInfo( hwnd, SB_VERT, & si );

int pozycja = si.nPos;
Dobra, mamy już pozycję, teraz jak sprawić, by okno i suwak w rzeczywistości się przesunęły? Zaczniemy od suwaka, bo to prostsze ;-) Możemy się w tym celu posłużyć funkcją SetScrollPos która ustawi nam nową pozycję suwaka. Ale znowu MSDN surowo zabrania jej używania, a w zamian daje SetScrollInfo. Tą funkcję już znamy, korzystaliśmy już z niej wcześniej. Tym razem zmienimy tylko pozycję. No właśnie, na co zmienimy? To dobre pytanie. Wszystko zależy od tego co użytkownik zrobił ze ScrollBarem. Oto przykładowa obsługa przesunięcia ScrollBara:
C/C++
case WM_VSCROLL: {
   
   
SCROLLINFO si;
   
ZeroMemory( & si, sizeof( si ) );
   
si.cbSize = sizeof( SCROLLINFO );
   
si.fMask = SIF_POS | SIF_PAGE | SIF_TRACKPOS;
   
GetScrollInfo( hwnd, SB_VERT, & si );
   
   
int pozycja = si.nPos;
   
   
switch( LOWORD( wParam ) ) {
   
case SB_TOP:
       
pozycja = 0;
       
break;
   
case SB_BOTTOM:
       
pozycja = 1000;
       
break;
   
case SB_LINEUP:
       
if( pozycja > 0 ) {
           
pozycja--;
       
}
       
break;
   
case SB_LINEDOWN:
       
if( pozycja < 1000 ) {
           
pozycja++;
       
}
       
break;
   
case SB_PAGEUP:
       
pozycja -= si.nPage;
       
if( pozycja < 0 ) {
           
pozycja = 0;
       
}
       
break;
   
case SB_PAGEDOWN:
       
pozycja += si.nPage;
       
if( pozycja > 1000 ) {
           
pozycja = 1000;
       
}
       
break;
   
case SB_THUMBPOSITION:
       
pozycja = si.nTrackPos;
       
break;
   
case SB_THUMBTRACK:
       
pozycja = si.nTrackPos;
       
break;
   
}
   
   
ZeroMemory( & si, sizeof( si ) );
   
si.cbSize = sizeof( SCROLLINFO );
   
si.fMask = SIF_POS;
   
si.nPos = pozycja;
   
   
SetScrollInfo( hwnd, SB_VERT, & si, TRUE );
}
break;
Jak widać poza pozycją odczytaliśmy także wielkość strony i parametr nTrackPos którego na początku mieliśmy nie ruszać. Teraz jest on bardzo przydatny, gdyż właśnie w nim znajdziemy nowe wartości ScrollBara dla komunikatów SB_THUMBPOSITION i SB_THUMBTRACK. Obsługa pozostałych komunikatów jest chyba jasna.

Teraz zabierzmy się za przesuwania zawartości okna. Domyślasz się pewnie, że jest do tego jakaś funkcja np. ScrollWindow. Owszem, taka funkcja istnieje, lecz jako rzecze MSDN ta funkcja jest zła i trzeba używać ScrollWindowEx, która ma o wiele więcej parametrów ;-(. No cóż, takie jest życie. Wystarczy narzekania, przyjrzyjmy się bliżej tej funkcji:

Składnia: ScrollWindowEx(hwnd, dx, dy, prcScroll, prcClip, hrgnUpdate, prcUpdate, flags)

Argument Znaczenie
hwnd Uchwyt do okna, które chcemy przewijać.
dx Ile chcemy przewinąć w prawo. Jak chcemy w lewo to używamy ujemnej wartości.
dy Ile chcemy przewinąć w dół. Jak chcemy w górę to używamy ujemnej wartości.
prcScroll Wskaźnik na RECT oznaczający którą część obszaru klienta przewinąć. Jak damy NULL, to wszystko zostanie przewiniętę.
prcClip Wskaźnik na RECT oznaczający przycinanie. Bity przewijane z zewnątrz do tego prostokąta są rysowane. Bity przewijane na zewnątrz tego prostokąta nie są rysowane. Może być NULL.
hrgnUpade Niech się wypcha NULLem!
prcUpdate Niech się wypcha NULLem!
flags Różne mniej przydatne flagi. Nas interesuje SW_SCROLLCHILDREN, bo to ta flaga oznacza przewijanie ;)

Teraz nadszedł czas zastanowić się co podać jako argumenty dx i dy by się przewijało tak jak Pan Bóg przykazał? Można to zrobić np. w ten sposób:
C/C++
int dy = -( pozycja - si.nPos );
Jak pamiętamy ze wcześniejszych fragmentów kodu pozycja to nowa pozycja, natomiast si.nPos wciąż przechowuje starą pozycję. Dodatkowo musimy zamienić znaki w wyniku (operator -), inaczej ScrollBar będzie przewijał na odwrót ;-) Ten kod musimy umieścić przez kodem ustawiającym nową pozycję ScrollBara (a konkretniej, przed ZeroMemory) bo inaczej wartość si.nPos zostanie skasowana.

Teraz przejdźmy do ScrollWindowEx. Jako argument dx dajemy 0, gdyż w naszym przykładzie mamy tylko pionowy pasek przesuwu. A oto kod:
C/C++
ScrollWindowEx( hwnd, 0, dy,( CONST RECT * ) NULL,( CONST RECT * ) NULL,( HRGN ) NULL,( LPRECT ) NULL, SW_SCROLLCHILDREN );
UpdateWindow( hwnd );
Hura! Nasze kontrolki w oknie się przesuwają i zostawiają za sobą piękny ślad... No właśnie, coś tu jest nie tak, nie? W profesjonalnych aplikacjach nie ma takiego śladu! Na szczęście jego usunięcie nie jest zbyt kłopotliwe, wystarczy dorzucić flagi SW_INVALIDATE i SW_ERASE do ostatniego argumentu funkcji przewijającej. A oto kompletny kod obsługi komunikatu WM_VSCROLL:
C/C++
case WM_VSCROLL: {
   
   
SCROLLINFO si;
   
ZeroMemory( & si, sizeof( si ) );
   
si.cbSize = sizeof( SCROLLINFO );
   
si.fMask = SIF_POS | SIF_PAGE | SIF_TRACKPOS;
   
GetScrollInfo( hwnd, SB_VERT, & si );
   
   
int pozycja = si.nPos;
   
   
switch( LOWORD( wParam ) ) {
   
case SB_TOP:
       
pozycja = 0;
       
break;
   
case SB_BOTTOM:
       
pozycja = 1000;
       
break;
   
case SB_LINEUP:
       
if( pozycja > 0 ) {
           
pozycja--;
       
}
       
break;
   
case SB_LINEDOWN:
       
if( pozycja < 1000 ) {
           
pozycja++;
       
}
       
break;
   
case SB_PAGEUP:
       
pozycja -= si.nPage;
       
if( pozycja < 0 ) {
           
pozycja = 0;
       
}
       
break;
   
case SB_PAGEDOWN:
       
pozycja += si.nPage;
       
if( pozycja > 1000 ) {
           
pozycja = 1000;
       
}
       
break;
   
case SB_THUMBPOSITION:
       
pozycja = si.nTrackPos;
       
break;
   
case SB_THUMBTRACK:
       
pozycja = si.nTrackPos;
       
break;
   
}
   
   
int dy = -( pozycja - si.nPos );
   
ScrollWindowEx( hwnd, 0, dy,( CONST RECT * ) NULL,( CONST RECT * ) NULL,( HRGN ) NULL,( LPRECT ) NULL, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE );
   
UpdateWindow( hwnd );
   
   
ZeroMemory( & si, sizeof( si ) );
   
si.cbSize = sizeof( SCROLLINFO );
   
si.fMask = SIF_POS;
   
si.nPos = pozycja;
   
   
SetScrollInfo( hwnd, SB_VERT, & si, TRUE );
}
break;
W powyższym przykładzie został pokazany ScrollBar pionowy. Obsługa poziomego odbywa się analogicznie, z tym że w funkcji ScrollWindowEx uzupełniamy drugi argument, a nie trzeci jak w przykładzie powyżej. Komunikat do obsłużenia to WM_HSCROLL.

Podsumowanie

Na koniec tego artykułu podsumujemy nasze osiągnięcia ;-) Nauczyliśmy się jak utworzyć ScrollBar i ustawić jego parametry. Dowiedzieliśmy się także co zrobić, żeby naprawdę przewijał. Jak to w WinAPI, nie było to zbyt proste zadanie, ale daliśmy sobie z nim radę bez problemu. Tak więc jakieś podstawy ScrollBarów już znamy, choć jest jeszcze wiele rzeczy, które można by omówić, np. stworzenie poziomego ScrollBara, jednak informacje zawarte w tym artykule powinny ci wystarczyć, by stworzyć taki pasek samemu.
Poprzedni dokument Następny dokument
Toolbar, cz. 3 StatusBar