In diesem Tutorial zeige ich dir, wie man Text, der einfach zu groß ist, um auf einmal in ein Fenster zu passen, mit Hilfe von Scrollbars doch noch in ein Fenster bekommt. Scrollbars sind für den Benutzer ziemlich einfach zu benutzen, denn jeder ist mit ihnen vertraut.
Wir schreiben ein Programm, welches einen langen Text in ein Fenster ausgibt. Passt der Text nicht komplett in das Fenster, werden Scrollbars angebracht.
#define STRICT #include <windows.h> LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); const char szAppName[] = "Tutorial 15: Scrollbars";
Den Text, der ausgegeben werden soll, speichern wir in szText
.
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) {
Als ersters deklarieren wir zwei Konstanten. Die eine Konstante speichert die anfangs Breite des Fensters und die andere die anfangs Höhe.
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 = GetStockObject(WHITE_BRUSH); wc.lpszMenuName = 0; wc.lpszClassName = szAppName; RegisterClass(&wc);
In der CreateWindow
Funktion müssen wir angeben, dass wir Scrollbars wollen.
Dies machen wir einfach, indem wir WS_VSCROLL
mit zu dem Fensterstil
hinzufügen. Wollten wir auch noch eine horizontale Scrollleiste, müssten wir
zusätzlich WS_HSCROLL
mit angeben. Anstatt CW_USEDEFAULT
geben wir unsere Konstanten an.
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) {
Als nächstes brauchen wir ein paar static
Variablen. Einmal ist das die
Konstante iRand
, die den Abstand zwischen dem Fensterrand und den Text (jedoch
nur rechts und links) speichert. iZeichenhoehe
speichert die Höhe eines
Zeichens. Was natürlich variieren kann, weshalb meine Angabe von 16 nicht stimmen muss.
Im "richtigen Leben" müsste man sich diese Information erst von Windows
über die GetTextMetrics
(im MSDN: GetTextMetrics
) Funktion besorgen. Die RECT
Struktur speichert diesmal nicht die Maße des Fensters, sondern den Bereich, in den
der Text gezeichnet werden soll. Wenn also nach unten gescrollt wurde, hat rect.top
einen negativen Wert, damit der obere Teil nicht sichtbar ist und der untere zu sehen ist.
iScrollRange
speichert, wie viele Scrollstufen es gibt. Steht diese Variable auf
1, gibt es zwei Möglichkeiten: Der Scrollbalken ist ganz oben (Scrollposition ist 0) oder
ganz unten (Scrollposition ist 1). Je größer diese Zahl ist, desto mehr
Zwischenstufen gibt es. iScrollPos
speichert, wo wir gerade zwischen Null und
iScrollRange
sind. Am Anfang sind wir ganz oben, also bei Null.
static const int iRand = 20; static const int iZeichenhoehe = 16; static RECT rect; static int iScrollRange; static int iScrollPos; switch (message) { case WM_SIZE: {
In der WM_SIZE
Nachricht, müssen wir alle Daten bezüglich der
Scrollbars sammeln. Wir müssen wissen, wie viele Scrollstufen es geben soll, bei
welcher Srollstufe wir gerade sind und ob überhaupt gescrollt werden muss. Für
jede Zeile, die nicht komplett angezeigt werden kann, soll es eine Scrollstufe geben, weshalb
wir ersteinmal wissen müssen, wie viele Zeilen wir haben. Dies können wir über
die DrawText
Funktion herrausfinden, für die brauchen wir jedoch einen
gültigen hDC
. Dann brauchen wir noch eine zweite RECT
Struktur,
in die DrawText
die Maße eintragen soll.
HDC hDC; RECT TextRect;
Danach füllen wir die rect
Struktur mit den neuen Fensterwerten. Rechts und
Links berechnen wir gleich einen zuvor festgelegten Rand hinzu. Weil DrawText
diese Werte auch braucht, weil die Zeilenanzahl ja von der Zeilenbreite abhängt, diese
jedoch durch die neuen Text Maße überschrieben werden, kopieren wir die Struktur
einmal.
rect.left = iRand; rect.top = 0; rect.right = LOWORD(lParam) - iRand; rect.bottom = HIWORD(lParam); TextRect = rect;
Nun besorgen wir uns einen gültigen Device Context mit der Funktion
GetDC
(im MSDN: GetDC
). Danach rufen wir ganz normal die
DrawText
Funktion auf, mit dem Unterschied, dass wir zu den Flags noch
DT_CALCRECT
angeben. Dies bewirkt, dass DrawText
den Text nicht
zeichnet, sondern nur Anhand der gegebenen Maße das Rechteck, in dem der Text
gestanden hätte, zu berechnen. Diese Werte werden in die TextRect
Struktur
eingetragen.
hDC = GetDC(hWnd); { DrawText(hDC, szText, lstrlen(szText), &TextRect, DT_WORDBREAK | DT_CALCRECT); } ReleaseDC(hWnd, hDC);
Wenn die Höhe des Textes größer ist, als das Fenster, dann sind Scrollbars nötig, um den gesamt Text darzustellen. In der nächsten Zeile berechnen wir, wie viele Zeilen über den unteren Rand hinausgucken, also auch, wie viele Scrollpositionen es geben soll. Wir teilen einfach die übriege Höhe in Pixeln durch die Anzahl an Pixeln eines Zeichens. Die Höhe eines Zeichens müsste man, wie auch schon oben erwähnt, dynamisch berechnen. Da wir immer eine Position mehr brauchen, als durch dieses Verfahren errechnet wird, addieren wir noch eins hinzu. Danach kontrollieren wir, ob es nötig ist, die Scrollposition weiter hinaufzusetzen, weil es möglicherweise nicht mehr so viele Scrollpositionen gibt.
if (TextRect.bottom > rect.bottom) { iScrollRange = (TextRect.bottom - rect.bottom) / iZeichenhoehe + 1; iScrollPos = (iScrollRange < iScrollPos) ? iScrollRange : iScrollPos;
Natürlich müssen wir Windows noch mitteilen, dass wir die Scrollposition ändern
möchten. Dies machen wir mit der SetScrollPos
Funktion (im MSDN: SetScrollPos
). Der erste Parameter ist der Handle zu
dem Fenster, mit der Scrollleiste. Der zweite Parameter ist entweder SB_VERT
oder
SB_HORZ
, kommt drauf an, welche Scrollleiste man meint. Wir haben ja nur eine
vertikale, also nehmen wir SB_VERT
. Der dritte Paramter bestimmt die neue
Scrollposition. Und der vierte Parameter bestimmt, ob die Scrollbar danach neu gezeichnet
werden soll, oder nicht. Normalerweise kann man hier immer TRUE
angeben. Danach
müssen wir noch die rect
Struktur manipulieren, damit auch der richtige
Textausschnitt im Fenster erscheint. Wir geben rect.top
eine so große
negative Zahl, dass der nach oben gescrollte Text nicht mehr angezeigt wird.
SetScrollPos(hWnd, SB_VERT, iScrollPos, TRUE); rect.top = -iScrollPos * iZeichenhoehe; }
Wenn wir keine Scrollbar brauchen, setzen wir die Anzahl der Scrollpositionen und den Anfang der Zeichenopertion auf Null.
else { iScrollRange = 0; rect.top = 0; }
Zum Schluss setzen wir noch die Anzahl der Scrollpositionen mit der Funktion
SetScrollRange
(im MSDN: SetScrollRange
). Die ersten beiden Parameter sind
von SetScrollPos
bekannt. Der dritte Parameter gibt die obere Scrollposition an.
Der vierte Paramter gibt dann die untere Scrollposition an. Und der fünfte Parameter
ist wieder für das Neuzeichnen zuständig.
SetScrollRange(hWnd, SB_VERT, 0, iScrollRange, TRUE); return 0; }
Die WM_PAINT
Nachricht ist diesmal nichts besonderes. Es wird einfach der Text
in das vorgegebene Rechteck gezeichnet.
case WM_PAINT: { PAINTSTRUCT ps; HDC hDC; hDC = BeginPaint(hWnd, &ps); { DrawText(hDC, szText, lstrlen(szText), &rect, DT_WORDBREAK); } EndPaint(hWnd, &ps); return 0; }
In diesem Tutorial bearbeiten wir die WM_VSCROLL
Nachricht, die wir jedesmal
bei einer Änderung der Scrollbars erhalten. Im niederwertigen Wort von wParam
ist ein Code gespeichert, der uns genau mitteilt, was passiert ist. Im höherwertigen
Wort steht die (alte) Position des Scrollbalkens. Dies ist nur der aktualisierte Wert, wenn
der Balken direkt verschoben wurde, also nicht durch das Klicken auf die Pfeile.
lParam
speichert den Handle des Fensters. Um nun auf die einzelnen Aktionen
eingehen zu können, benutzen wir eine switch/case
Konstruktion.
case WM_VSCROLL: { switch (LOWORD(wParam)) {
Die erste Konstante, die wir bearbeiten, ist SB_LINEUP
. Diese Art der Nachricht
kommt, wenn der Benutzer auf den oberen Pfeil der Scrollleiste geklickt hat. Das
SB
steht für ScrollBar. In diesem Fall prüfen wir, ob die
Scrollleiste schon ganz oben angelangt ist, ist sie das nicht, vermindern wir die
Scrollposition um eins.
case SB_LINEUP: iScrollPos = (iScrollPos > 0) ? (iScrollPos - 1) : 0; break;
Die SB_LINEDOWN
ist das Gegenstück zu SB_LINEUP
. Die
Bearbeitung ist entsprechend, nur dass hier iScrollRange
die untere Grenze ist.
case SB_LINEDOWN: iScrollPos = (iScrollPos < iScrollRange) ? (iScrollPos + 1) : iScrollRange; break;
Wenn der SB_PAGEUP
Nachrichtentyp kommt, dann hat der Benutzer auf die
Scrollleiste oberhalb des Balkens geklickt. In diesem Fall lasse ich den Text ganz nach
oben scrollen. Man hätten ihn da auch nur vier Zeilen oder eben eine Seite nach oben
scrollen lassen können. Mit SB_PAGEDOWN
ist es fast genauso, hier lassen
wir den Text nach ganz unten scrollen.
case SB_PAGEUP: iScrollPos = 0; break; case SB_PAGEDOWN: iScrollPos = iScrollRange; break;
Kommt der SB_THUMBPOSITION
Nachrichtentyp, dann hat der Benutzer den Balken
direkt irgendwo hin geschoben. In diesem Fall müssen wir nur noch die neue
Scrollbalkenposition übernehmen. Die anderen Nachrichtentypen zu Scrollleisten
ignorieren wir in diesem Tutorial.
case SB_THUMBPOSITION: iScrollPos = HIWORD(wParam); break; default: return 0; }
Nun benutzen wir wieder die SetScrollPos
Funktion, um die geänderte
Scrollposition Windows mitzuteilen. Danach berechnen wir noch den Zeichenbereich, damit nur
der gewünschte Teil gezeichnet wird, und lassen danach mit Hilfe der
InvalidateRect
Funktion den Anwendungsbereich neu zeichnen.
SetScrollPos(hWnd, SB_VERT, iScrollPos, TRUE); rect.top = -iScrollPos * iZeichenhoehe; InvalidateRect(hWnd, NULL, TRUE); return 0; } case WM_DESTROY: { PostQuitMessage(0); return 0; } } return DefWindowProc(hWnd, message, wParam, lParam); }
Das Programm in diesem Tutorial stellt Text im Anwendungsbereich dar. Wenn der Platz nicht reicht, werden Scrollbars eingeblendet (Screenshot). Jedoch sind die Scrollleisten nur über die Mouse zu steuern, möchte jemand die Tastatur benutzen, so wird es nicht funktionieren. Weiteres im nächsten Tutorial.