Die WinAPI Plattform

Tutorials

Tutorial Nummer 16

(16.) Moderne Scrollbars und die Tastatur

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.

webmaster@win-api.de