Die WinAPI Plattform

Tutorials

Tutorial Nummer 15

(15.) Scrollbars

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.

webmaster@win-api.de