Das Programm des letzten Tutorials hatte mehrere Nachteile und wirkte nicht so modern. Das wollen wir in diesem Tutorial ändern. Erstens war die Scrollleiste immer gleich Groß, was bei neueren Programm nicht mehr der Fall ist. Die Größe passt sich nun immer der Größ des zu scrollenden Bereiches an. Zweitens konnte man im letzten Programm nicht sehen, wie weit man schon gescrollt hatte, denn der Bildschirm wurde erst nach dem Beenden der Scrollaktion neu gezeichnet. Wenn man mit der Tastatur scrollen wollte, so funktionierte dies auch nicht. Und manch einer wird vielleicht gedacht haben: Warum muss ich diese Rechnerei machen, wenn das doch eigentlich auch Windows für mich machen könnte, da es immer das gleiche ist.
All dieses wollen wir in diesem Tutorial beheben. Und dafür benutzen wir nicht mehr die
alten Funktionen SetScrollPos
und SetScrollRange
, sondern die
in Windows 95 eingeführten Funktionen SetScrollInfo
und
GetScrollInfo
. Der Einfachheit halber benutzen wir den gleichen Text im
Anwendungsbereich des Fensters.
#define STRICT #include <windows.h> LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); const char szAppName[] = "Tutorial 16: Moderne Scrollbars und die Tastatur"; const char szText[] = "Gallia est omnis divisa in partes tres, quarum unam incolunt " "Belgae, aliam Aquitani, tertiam, qui ipsorum lingua Celtae, " "nostra Galli appellantur. Hi omnes lingua, institutis, " "legibus inter se differunt. Gallos ab Aquitanis Garunna " "flumen, a Belgis Matrona et Sequana divit.\nHorum omnium " "foritissimi sunt Belgae, propterea quod a cultu atque " "humanitate provinciae longissime absunt minimeque ad eos " "mercatores saepe commeant atque ea, quae ad effeminandos " "animos pertinent, important proximique sunt Germanis, qui " "trans Thenum incolunt, quibuscum continenter bellum " "gerunt.\n\nApud Helvetios longe noblilissimus fuit et " "ditissimus Orgetorix. Is M. Messala M. Pisone consulibus " "regni cupiditate inductus coniurationem nobilitatis fecit " "et civitati persuasit, ut de finibus suis cum omnibus " "copiis exirent: perfacile esse, cum virtute omnibus " "praestarent, totius Galliae imperio potiri.\nId hoc " "gacilius iis persuasit, quod undique loci natura Helvetii " "continentur: una ex parte flumine Rheno latissimo atque " "altissimo, qui agrum Helvetium a Germanis dividit, altera " "ex parte monte Iura altissimo, qui est inter Sequanos " "et Helvetios, tertia lacu Lemanno et flumine Thodano, " "qui provinciam nostram ab Helvetiis dividit.\n" "\nG. J. Caesar"; int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { const int iWindowWidth = 400; const int iWindowHeight = 300; MSG msg; HWND hWnd; WNDCLASS wc; wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInstance; wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wc.lpszMenuName = 0; wc.lpszClassName = szAppName; RegisterClass(&wc);
In der CreateWindow
setzten wir als dwStyle
auch wieder den
WS_VSCROLL
Stil.
hWnd = CreateWindow( szAppName, szAppName, WS_OVERLAPPEDWINDOW | WS_VSCROLL, CW_USEDEFAULT, CW_USEDEFAULT, iWindowWidth, iWindowHeight, NULL, NULL, hInstance, NULL); ShowWindow(hWnd, iCmdShow); UpdateWindow(hWnd); while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
Dieses mal brauchen wir nur die Konstanten für den Fensterrand und die Zeichenhöhe,
welche normalerweise keine Konstante sein sollte und erst über
GetTextMetrics
von Windows geholt werden sollte, da dieser Wert unter
umständen auf anderen Windows Systemen einen anderen Wert haben könnte.
Dann brauchen wir wieder die rect
Struktur, in der wir, genau wie im letzten
Tutorial, den Zeichenbereich speichern. Die Variablen iScrollRange
und
iScrollPos
brauchen wir diesmal nicht.
static const int iRand = 20; static const int iZeichenhoehe = 16; static RECT rect; switch (message) { case WM_SIZE: {
In der WM_SIZE
Nachricht bestimmen wir wieder die Länge unseres Textes und
stellen dabei die Scrollleiste richtig ein. Die Breite des Textes berechnet für uns wieder
die DrawText
Funktion, weswegen wir uns auch wieder einen DeviceContext
besorgen müssen. TextRect
speichert wieder die Position des Texts. Diesmal
brauchen wir auch die SCROLLINFO
Struktur (im
MSDN: SCROLLINFO
),
welche die Daten zur Scrollleiste speichert und zur Übergabe an die
SetScrollInfo
Funktion gebraucht wird.
HDC hDC; RECT TextRect; SCROLLINFO siScrollInfo;
Nun belegen wir, genau wie im letzten Tutorial, die RECT
Struktur mit Werten
vor und lassen dann die Größe des Textes errechnen.
rect.left = iRand; rect.top = 0; rect.right = LOWORD(lParam) - iRand; rect.bottom = HIWORD(lParam); TextRect = rect; hDC = GetDC(hWnd); { DrawText(hDC, szText, lstrlen(szText), &TextRect, DT_WORDBREAK | DT_CALCRECT); } ReleaseDC(hWnd, hDC);
Nun belegen wir die SCROLLINFO
Struktur mit Werten. Das erste Element
cbSize
muss die Größe der Struktur erhalten. Dies hat zwei
Gründe. Erstens kann so bei der Übergabe von falschen Zeigern dies erkannt
und so das Überschreiben von anderen Variablen vermieden werden. Denn wenn
cbSize
nicht den richtigen Wert hat, verweigert die Funktion die Mitarbeit.
Und zweitens ist die Funktion nun aufwärtskompatibel, denn die Funktion kann immer
erkennen mit welcher SCROLLINFO
Stuktur es zu tun hat.
Mit dem Element fMask
kann man die Bearbeitung der Funktion einschränken.
Wenn man zum Beispiel nur die Position setzen möchte, muss man sich nicht erst die
Werte für die anderen Felder holen, sondern kann einfach in fMask
SIF_POS
einsetzen und nur das nPos
Element mit Werten füllen.
Möchte man zwei Werte übergeben, dann verknüpft man diese durch die
Oder-Verknüpfung (|
). Wenn man alle Werte neu setzen möchte, kann man
alternativ auch SIF_ALL
benutzen. SIF_RANGE
umfasst zwei
Elemente nMin
und nMax
, die man nur paarweise setzen kann.
Hier müssen wir übriegens nur die nackten Werte einsetzen (ausser eben die
Berechnung von Pixeln in Zeilen), alle Berechnungen wird Windows selbstständig
durchführen.
siScrollInfo.cbSize = sizeof(siScrollInfo); siScrollInfo.fMask = SIF_RANGE | SIF_PAGE; siScrollInfo.nMin = 0; siScrollInfo.nMax = TextRect.bottom / iZeichenhoehe; siScrollInfo.nPage = rect.bottom / iZeichenhoehe;
Die SetScrollInfo
Funktion (im
MSDN: SetScrollInfo
)
benutzen wir schließlich um die Daten in der SCROLLINFO
Struktur an
Windows zu übergeben. Der erste Parameter ist der Handle zu dem Fenster mit den
Scrollleisten. Der zweite Parameter gibt an, ob die horizontale oder vertikale Scrollleiste
gemeint ist, kann also die Werte SB_VERT
oder SB_HORZ
haben. Der
dritte Parameter ist der Zeiger auf unsere SCROLLINFO
Struktur. Der letzte
Parameter bestimmt, ob die Scrollbar neu gezeichnet werden soll oder nicht.
SetScrollInfo(hWnd, SB_VERT, &siScrollInfo, TRUE);
Nach der Übergabe berechnet Windows sofort die Position neu und setzt diese dann auch
entsprechend. Nun muss aber noch der Fensterinhalt entsprechend der Position neu gezeichnet
werden, also besorgen wir uns über die GetScrollInfo
Funktion
(im MSDN: GetScrollInfo
)
die neue Position und setzen unser Rechteck entsprechend. InvalidateRect
müssen wir nicht aufrufen, da unsere Fensterklasse die Styls CS_HREDRAW
und
CS_VREDRAW
hat.
siScrollInfo.fMask = SIF_POS; GetScrollInfo(hWnd, SB_VERT, &siScrollInfo); rect.top = -iZeichenhoehe * siScrollInfo.nPos; return 0; }
In der WM_PAINT
Nachricht hat sich gegenüber dem letzten Tutorial nichts
getan.
case WM_PAINT: { PAINTSTRUCT ps; HDC hDC; hDC = BeginPaint(hWnd, &ps); { DrawText(hDC, szText, lstrlen(szText), &rect, DT_WORDBREAK); } EndPaint(hWnd, &ps); return 0; }
Nun fangen wir wieder die WM_VSCROLL
Nachricht ab, in der wir Veränderungen
der vertikalen Scrollleiste bearbeiten. Auch hier brauchen wir wieder eine Instanz der
SCROLLINFO
Struktur, damit wir die Änderungen direkt übergeben
können. Die Variable iPosition
benutzen wir, um die alte Position zu
speichern, denn wenn sich diese nicht ändert, brauchen wir das Fenster nicht neu zu
zeichnen.
case WM_VSCROLL: { SCROLLINFO siScrollInfo; int iPosition;
Dann lassen wir uns von Windows alle Scrollinformationen übergeben, die jedoch noch
nicht geupdated sind. Die alte Scrollposition speichern wir in der iPosition
Variablen.
siScrollInfo.cbSize = sizeof(siScrollInfo); siScrollInfo.fMask = SIF_ALL; GetScrollInfo(hWnd, SB_VERT, &siScrollInfo); iPosition = siScrollInfo.nPos;
Im niederwertigen Wort von wParam
ist der Wert gespeichert, der die Aktion
festlegt. Nun manipulieren wir die Elemente der SCROLLINFO
Struktur in
Abhängigkeit zur Aktion.
switch (LOWORD(wParam)) { case SB_TOP: siScrollInfo.nPos = siScrollInfo.nMin; break; case SB_BOTTOM: siScrollInfo.nPos = siScrollInfo.nMax; break; case SB_LINEUP: siScrollInfo.nPos -= 1; break; case SB_LINEDOWN: siScrollInfo.nPos += 1; break; case SB_PAGEUP: siScrollInfo.nPos -= siScrollInfo.nPage; break; case SB_PAGEDOWN: siScrollInfo.nPos += siScrollInfo.nPage; break;
Die SB_THUMBTRACK
Aktion wird immer dann ausgelöst, wenn der Benutzer den
Balken direkt mit der Maus verschiebt. Beim Verschieben wird dann die Nachricht gesendet,
damit das Programm den Bildschirm schonmal aktualisieren kann. Dies ist aber nur bei
kleineren und sehr schnellen Programm günstig. Denn wenn das Nachzeichnen zu lange
dauert, kommt das Programm nicht mehr hinterher und das Programm wird langsam und reagiert
sehr träge. Dies sollte man dann vermeiden. Entweder man zeichnet beim Verschieben
nur eine Skizze des Bildes oder lässt es ganz weg und bearbeitet, wie im letzten Tutorial
auch, nur die SB_THUMBPOSITION
Aktion, die das Ende der Verschiebeaktion
mitteilt. Die neue Position steht in dem Element nTrackPos
. Man braucht sie
also nur noch zu übernehmen.
case SB_THUMBTRACK: siScrollInfo.nPos = siScrollInfo.nTrackPos; break; default: return 0; }
Nun ist die neue Position in siScrollInfo.nPos
gespeichert, jedoch kann diese
Position ungültig sein (z.B. negativ), jedoch ist dies egal, da die Prüfung
auf Korrektheit SetScrollInfo
übernimmt, weswegen wir den Wert einfach
so übergeben können.
siScrollInfo.fMask = SIF_POS; SetScrollInfo(hWnd, SB_VERT, &siScrollInfo, TRUE);
Da wir die korrigierte Position nicht kennen, besorgen wir uns die wieder über
GetScrollInfo
. Diese Position vergleichen wir dann mit der alten Position,
denn wenn sich nichts getan hat, brauchen wir auch nicht neuzeichnen.
GetScrollInfo(hWnd, SB_VERT, &siScrollInfo); if (siScrollInfo.nPos != iPosition) {
Zuerst setzen wir das Zeichenrechteck neu, damit der Text auch an der richtigen Stelle
landet. Danach würde es eigentlich reichen, wenn wir InvalidateRect
aufrufen. Jedoch wird es erstens ganz schön flimmern und zweitens ist es relativ
langsam, da der größte Teil ja eigentlich nicht neu gezeichnet,
sondern nur verschoben werden müsste. Dies verschieben und anschließend
neu zeichnen lassen macht die Funktion ScrollWindowEx
(im MSDN:
ScrollWindowEx
). Der erste Parameter ist der Handle zu dem Fenster, welches
gescrollt werden soll. Der zweite gibt die horizontale und der dritte die vertikale
Scrollreichweite an. Hier rechnen wir noch schnell die tatsäschliche Scrollweite aus.
Die nächsten vier Parameter sind für uns unwichtig und
bleiben bei Null. Mit dem letzte Parameter übergeben wir zwei Flags.
SW_INVALIDATE
und SW_ERASE
, welche bewirken, dass der
neu zu zeichnende Bereich für ungültig erklärt wird und dass er vor dem
Zeichnen mit der Hintergrundfarbe gelöscht wird.
rect.top = -iZeichenhoehe * siScrollInfo.nPos; ScrollWindowEx(hWnd, 0, iZeichenhoehe * (iPosition - siScrollInfo.nPos), NULL, NULL, NULL, NULL, SW_INVALIDATE | SW_ERASE); } return 0; }
Die Bearbeitung der WM_KEYDOWN
Nachricht ist nun ganz einfach. Wir senden,
bei entsprechendem Tastendruck, einfach uns selber eine Nachricht vom Typ
WM_VSCROLL
und überlassen die Bearbeitung ganz dieser Nachricht. Das ist
auch der Grund, weshalb wir bei der Bearbeitung der WM_VSCROLL
Funktion
einen Zweig für SB_TOP
und SB_BOTTOM
haben, denn mit der
Maus sind diese Ereignisse nicht zu erreichen. Das MAKEWPARAM
Macro setzt zwei
Wörter zu einem doppel Wort zusammen.
case WM_KEYDOWN: { switch (wParam) { case VK_DOWN: PostMessage(hWnd, WM_VSCROLL, MAKEWORD(SB_LINEDOWN, 0), 0); return 0; case VK_UP: PostMessage(hWnd, WM_VSCROLL, MAKEWORD(SB_LINEUP, 0), 0); return 0; case VK_HOME: PostMessage(hWnd, WM_VSCROLL, MAKEWORD(SB_TOP, 0), 0); return 0; case VK_END: PostMessage(hWnd, WM_VSCROLL, MAKEWORD(SB_BOTTOM, 0), 0); return 0; case VK_PRIOR: PostMessage(hWnd, WM_VSCROLL, MAKEWORD(SB_PAGEUP, 0), 0); return 0; case VK_NEXT: PostMessage(hWnd, WM_VSCROLL, MAKEWORD(SB_PAGEDOWN, 0), 0); return 0; } break; } case WM_DESTROY: { PostQuitMessage(0); return 0; } } return DefWindowProc(hWnd, message, wParam, lParam); }
In diesem Tutorial hast du gelernt, wie du gut aussehende und leicht zu bedienende Scrollleisten programmierst. Dieses Programm (Screenshot) ist ein Beispiel für solche Scrollleisten. Diese Variante ist natürlich der aus dem letzten Tutorial vorzuziehen, jedoch ist das letzte Tutorial für das Verständnis der Scrollbars unabdingbar.