Windows Programmierung: Splitbar
Willemers Informatik-Ecke
Der Splitbar ist der verschiebbare Balken, der im Windows-Explorer seit Windows 95 die Baumdarstellung der Verzeichnisse von der Liste der Dateien auf der rechten Seite trennt. Leider ist dieser kein Kontrollelement der Win32-API, sondern muss vollständig selbst programmiert werden, was aber gar nicht so schwer ist. Als Nebeneffekt kann man ihn auch leicht für Programme unter Windows 3.x verwenden, da er nicht auf Spezialitäten der Win32 aufsetzt.

Grundgedanke

Im Anwendungsfenster befinden sich zwei Kontrollelemente. Im Explorer ist das Baum und ListView. In meinem Beispiel Baum und Edit. Es wird ein Spalt zwischen beiden Elementen frei gelassen. Die Fensterfunktion merkt sich die x-Position dieses Spaltes in der Variablen VBarPos.

Bewegt sich die Maus über diesen Spalt, wird die Mauszeiger auf einen Doppelpfeil umgeschaltet. Wird die Maus niedergedrückt und verschoben, wird damit die VBarPos geändert. Beim Loslassen der Maus werden die Kontrollelemente neu positioniert.

Zur optischen Rückmeldung wird ein invertierter Streifen beim Verschieben durch das Bild geschoben. Die Technik ist die gleiche wie beim Aufspannen eines Markierungsrechtecks, des sogenannten Gummibandes.

Ereignisse in der Fensterfunktion

Zunächst sind die Nachrichten der Fensterfunktion abzufangen. Dabei interessieren die Mausereignisse Bewegung, Tastendruck und Loslassen. Aber auch das Ereignis zur Grössenänderung WM_SIZE muss angepasst werden.

static HWND hEdit;
static HWND hToolbar;
static HWND hListe;
static int VBarPos = 0;
POINTS pos;	// for mouse operations
static bool TrackSplit = false;

	...
	case WM_SIZE:
		reSize(hWnd, hToolbar, hEdit, hListe, VBarPos);
		break;

	case WM_MOUSEMOVE:
		pos = MAKEPOINTS(lParam);
		if (TrackSplit) {
			drawCapture(hWnd, VBarPos);
			VBarPos = pos.x;
			drawCapture(hWnd, VBarPos);
		}
		if (pos.x<=VBarPos+2 && pos.x>=VBarPos-2) {
			SetCursor(LoadCursor(0, IDC_SIZEWE));
		}
		break;

	case WM_LBUTTONDOWN:
		pos = MAKEPOINTS(lParam);
		if (pos.x<=VBarPos+2 && pos.x>=VBarPos-2) {
			TrackSplit = true;
			SetCapture(hWnd);
			VBarPos = pos.x;
			drawCapture(hWnd, VBarPos);
			SetCursor(LoadCursor(0, IDC_SIZEWE));
		}
		break;

	case WM_LBUTTONUP:
		pos = MAKEPOINTS(lParam);
		if (TrackSplit) {
			TrackSplit = false;
			drawCapture(hWnd, pos.x);
			VBarPos = pos.x;
			ReleaseCapture();
			
			reSize(hWnd, hToolbar, hEdit, hListe, VBarPos);
		}
		break;

Maustaste nach unten

Beim Druck der Taste im Bereich des Schiebebalken wird in der Statusvariablen TrackSplit festgehalten, dass die Taste unten ist. Die Funktion SetCapture wird gebraucht, damit man nun den Kontakt zur Maus solange nicht verliert, bis die Taste wieder losgelassen wird. Die selbstgeschriebene Funktion drawCapture soll die optische Rückmeldung übernehmen. In der Variablen VBarPos wird die x-Position des Schiebebalkens festgehalten.

Mausbewegung

Eine Mausbewegung bei festgehaltenem Balken muss die VBarPos aktualisieren und die Position des Phantombalkens (drawCapture) verändern. Des weiteren muss der Mauszeiger im Bereich des Balkens immer auf Doppelpfeil stehen, damit der Anwender merkt, was er verändert.

Maustaste loslassen

Erst wenn die Taste losgelassen wird, findet das Neupositionieren der Fenster statt. Dazu kann leicht die Befehlsfolge der Nachricht WM_SIZE verwendet werden. Ansonsten sind Aufräumarbeiten fällig. TrackSplit ist nun wieder false, der letzte Phantombalken wird entfernt und mit ReleaseCapture wird die Maus auch für andere Fenster wieder frei.

Grössenänderung

Bei einer WM_SIZE-Nachricht werden die beiden Kontrollelemente in ihrer Position anhand der Grösse des Clientfensters korrigiert. Die Variable VBarPos enthält die x-Position des Schiebebalkens und bestimmt damit einen weiteren Parameter der Aussengrenze der Kontrollelemente.

Ein Sonderfall kann eintreten, wenn das Fenster über den Schiebebalken hinweg verkleinert wird. Der Schiebebalken muss in jedem Fall in das Fenster hinein, damit er weiter zugreifbar bleibt.

#define BARWIDTH 4

void reSize(HWND hWnd, HWND hToolbar, HWND hEdit, HWND hListe, int &VBarPos)
{
RECT rcl, win;

  GetClientRect(hWnd, &win);	// get (new) window size
  // Get the size of the toolbar
  SendMessage(hToolbar, TB_AUTOSIZE, 0L, 0L);
  GetWindowRect(hToolbar, &rcl);

  int xSize = win.right;
  int ySize = win.bottom;

  if (VBarPos==0)	VBarPos = xSize/4;
  if (VBarPos < BARWIDTH) VBarPos=5;
  if (VBarPos > xSize-BARWIDTH) VBarPos = xSize;
  if (VBarPos<0) VBarPos=0;
  // Size the rich edit control to the right
  MoveWindow(hEdit, VBarPos+BARWIDTH/2, rcl.bottom - rcl.top, 
             xSize - VBarPos - BARWIDTH/2, ySize, TRUE );
  // Size the tree control to the left
  MoveWindow(hListe, 0, rcl.bottom - rcl.top, VBarPos-BARWIDTH/2, ySize, TRUE );
}

Optische Rückmeldung

Damit der Anwender das Gefühl vermittelt bekommt, er verschiebe etwas, ist es notwendig den Balken zumindest andeutungsweise an der aktuellen Position zu zeichnen. Damit diese Rückmeldung flott geht, wird der Balken hier wie beim Explorer auch durch ein invertiertes Rechteck stilisiert. Durch die Intervertierung erreicht man, dass das Rechteck verschwindet, wenn man es ein zweites Mal zeichnet. Also wird einmal beim Niederdrücken der Maustaste gezeichnet. Bei jeder Bewegung wird zunächst mit der alten Position noch einmal gezeichnet, um den alten Balken zu löschen und ein weiteres Mal an der neuen Position, damit der Eindruck entsteht, der Balken bewege sich. Beim Loslassen der Maustaste muss mit der alten Position ein letztes Mal gezeichnet werden, damit der Phantombalken wieder verschwindet. Die zentrale Funktion dazu ist drawCapture.

void drawCapture(HWND hWnd, int x)
{
RECT r;

	GetClientRect(hWnd, &r);
	int y = r.bottom;
	HDC hDC = GetDC(hWnd);
	SetROP2(hDC, R2_NOT);
	MoveToEx(hDC, x-BARWIDTH/2, 0, 0);
	LineTo(hDC, x-BARWIDTH/2, y);
	LineTo(hDC, x+BARWIDTH/2, y);
	LineTo(hDC, x+BARWIDTH/2, 0);
	LineTo(hDC, x-BARWIDTH/2, 0);
	ReleaseDC(hWnd, hDC);
}

Hier wird die Grösse des Fensters für die Höhe des Balkens berechnet. Mit SetROP2 und R2_NOT wird invertierendes Zeichnen eingestellt und anschliessend das Rechteck um den Balken gezeichnet. Will man das Verhalten des Explorers komplett kopieren, müsste man an dieser Stelle die x-Position der linken oberen Ecke auf 0 setzen. Dass auch Microsoft dieses Verfahren im Visual Studio ändert, zeigt, dass das Umrahmen des Balkens die optisch naheliegendere Lösung ist.

Etwas unschön ist noch, dass der Balken durch die Toolbar geht. Dies ist mit den ersten Zeilen aus reSize leicht zu ändern und ist hier der Übersichtlichkeit halber weggelassen.

Hintergründig

Bisher erscheint der Schiebebalken in weiss. Das ist die Hintergrundfarbe des Fensters. Der Anwender wird aber einen grauen Schiebebalken erwarten, also in der Farbe der Rahmenelemente. Das könnte man auch zeichnen. Einfacher ist es jedoch, den Hintergrund des Fensters wird auf grau zu setzen, indem er bei der Registrierung hbr.Background auf COLOR_WINDOW gesetzt wird. Der Hintergrund ist später sowieso nur im Splitbalken zu sehen.

WNDCLASSEX wcex;

  wcex.cbSize = sizeof(WNDCLASSEX); 
  ...
  wcex.hbrBackground	= (HBRUSH)(COLOR_WINDOW);//(COLOR_WINDOW+1);
  ...
  RegisterClassEx(&wcex);