Größenänderung von Fenstern einschränken

Veröffentlicht: 18. Aug 2003 | Aktualisiert: 27. Jun 2004

Von Mathias Schiffer

Die Microsoft Newsgroups sind eine Quelle schier unerschöpflichen Wissens, das nahezu auf Knopfdruck abrufbar ist: Hunderte deutschsprachige Entwickler vom Einsteiger bis zum Profi treffen sich hier täglich virtuell, um Fragen zu stellen oder zu beantworten. Auch themennahe Probleme, Ansichten und Konzepte werden miteinander diskutiert. Sie kennen die Microsoft Newsgroups noch nicht? Detaillierte Information für Ihre Teilnahme finden Sie auf der Homepage der Microsoft Support Newsgroups.

Diese Kolumne greift regelmäßig ein besonders interessantes oder häufig nachgefragtes Thema aus einer der Entwickler-Newsgroups auf und arbeitet es aus.

Aus der Visual Basic Newsgroup microsoft.public.de.vb:

Ich möchte erreichen, dass wenn ein Formular vergrößert oder verkleinert wird, es während der Größenänderung bei einer gewissen Höhe bzw. Breite das Formular nicht mehr weiter vergrößert bzw. verkleinert werden kann. Wie kann ich das erreichen?

Die einfachste Variante, eine Form nicht über ein gewisses Maß hinaus vergrößern oder verkleinern zu können, besteht in der Nutzung des Resize-Ereignisses der Form. Zwar können Sie hier die Größenänderung nicht blockieren, jedoch können Sie Minimal- und Maximalkoordinaten direkt wieder setzen, wenn diese über- oder unterschritten wurden.

Ein kurzes Beispiel demonstriert das einfache Vorgehen:

' Simple Größenbeschränkung durch Zurücksetzen: 
Private Sub Form_Resize() 
Dim MaxWidth As Single 
Dim MaxHeight As Single 
Dim MinWidth As Single 
Dim MinHeight As Single 
  ' Nur für normale Fenster (weder maximiert noch minimiert) 
  ' ist ein Setzen der Height- und Width-Eigenschaften 
  ' möglich: 
  If Me.WindowState <> vbNormal Then 
 Exit Sub 
  End If 
  ' Maximale Höhe und Breite definieren: 
  MaxWidth = Me.ScaleX(640, vbPixels, Me.ScaleMode) 
  MaxHeight = Me.ScaleY(480, vbPixels, Me.ScaleMode) 
  ' Bei Überschreitung der Grenzwerte zurücksetzen: 
  If Me.Width > MaxWidth Then 
 Me.Width = MaxWidth 
  End If 
  If Me.Height > MaxHeight Then 
 Me.Height = MaxHeight 
  End If 
  ' Minimale Höhe und Breite zurücksetzen: 
  MinWidth = Me.ScaleX(320, vbPixels, Me.ScaleMode) 
  MinHeight = Me.ScaleY(200, vbPixels, Me.ScaleMode) 
  ' Bei Unterschreitung der Grenzwerte zurücksetzen: 
  If Me.Width < MinWidth Then 
 Me.Width = MinWidth 
  End If 
   If Me.Height < MinHeight Then 
 Me.Height = MinHeight 
  End If 
End Sub

Dieses kleine Beispiel erfüllt bereits seinen Zweck: Wenn der Anwender das Fenster größer oder kleiner als durch die Grenzwerte vorgegeben ziehen will, gelingt ihm dies zwar, jedoch wird die Fenstergröße daraufhin sofort wieder zurückgesetzt.

Dieses Vorgehen hat einen erheblichen Nachteil, der sich Ihnen beim Ausprobieren nicht verschließen wird: Durch das ständige Hin und Her zwischen der Größenänderung durch den Nutzer und derjenigen durch Ihren Code flackert die Darstellung des Fensters sehr erheblich.

Fazit: Zwar war dieser Lösungsansatz schnell implementiert, das Ergebnis vermag jedoch nicht abschließend zu überzeugen. Der folgende Lösungsansatz verhilft zu einem perfekten Ergebnis - ist dafür aber auch deutlich aufwendiger in der Implementation: Schönheit hat ihren Preis.

 

Die Fensternachricht WM_MINMAXINFO nutzen

Soll eine Form in ihrer Größe verändert werden - egal ob durch Code, durch den Mauszeiger, durch Maximieren oder durch Minimieren -, so erhält die Form von Windows eine Fensternachricht, die symbolisch mit der Konstanten WM_GETMINMAXINFO bezeichnet wird. Die Nachricht dient dem Zweck, die maximal bzw. minimal erlaubte Größe des Fensters zu ermitteln.

Sie können diese Nachricht an das Fenster über die Technik des "Subclassings" umleiten. Damit können Sie diese Nachricht in Ihrem Code erhalten, bevor sie beim eigentlichen Empfänger - dem Fenster - ankommt. Greifen Sie so in den Weg der Nachricht ein, obliegt es auch Ihnen, die abgefangene Nachricht an ihren eigentlichen Empfänger weiterzuleiten.

Der Trick dabei: Niemand verlangt, dass Sie diese Nachricht unverändert weiterleiten müssen. Eigentlich könnten Sie dem Empfänger das Eintreffen der von Ihnen abgefangenen Nachricht sogar komplett verschweigen (was sicher nicht generell höflich wäre, aber der Zweck heiligt die Mittel).

Hinweis zur Subclassing-Technik: In diesem Artikel soll nicht näher auf die Natur und die Feinheiten des Subclassings unter Visual Basic eingegangen werden. Allein über diese Technik, die wir hier nur als Lösungshilfe einsetzen werden, wurden bereits alleine seitenlange Artikel geschrieben. Sollten Sie mit dieser Technik noch nicht vertraut sein, so finden Sie in Ihrer MSDN Library diverse einführende Artikel zum Thema - etwa "Intercepting Windows Messages in Visual Basic" von Deborah L. Cooper. Am Ende dieses Artikels finden Sie einen bewusst einfach gehaltenen, kompletten Beispielcode, mit dessen Hilfe Sie auch erste Gehversuche in diesem Metier wagen können.

Schauen wir uns nun den einzig relevanten Parameter der Nachricht WM_GETMINMAXINFO genauer an: Im letzten ihrer Parameter lParam wird ein benutzerdefinierter Typ MINMAXINFO übergeben. Er enthält Angaben über die minimale und maximale Größe eines Fensters bei einer Größenveränderung (etwa durch "Ziehen" mit dem Mauszeiger). Weiterhin birgt der Typ Daten über die Position und Größe eines maximierten Fensters - das durchaus nicht die gesamte Arbeitsfläche bedecken muss, wie es für gewöhnlich der Fall ist (da dies unser Problem nicht betrifft, bleibe es bei der reinen Erwähnung). Diese Angaben werden jeweils als POINTAPI-Koordinatentupel angegeben:

Private Type POINTAPI 
  x As Long  ' Horizontale Angabe (Links / Breite) 
  y As Long  ' Vertikale Angabe (Oben / Hoehe) 
End Type 
Public Type MINMAXINFO 
  Reserved  As POINTAPI  ' Reserviert = bedeutungslos 
  MaxSize   As POINTAPI  ' Fenstergröße im maximierten Zustand 
  MaxPosition  As POINTAPI  ' Fensterposition im max. Zustand 
  MinTrackSize As POINTAPI  ' Minimale Fenstergröße beim "Ziehen" 
  MaxTrackSize As POINTAPI  ' Maximale Fenstergröße beim "Ziehen" 
End Type

Trifft also die Nachricht WM_GETMINMAXINFO ein, so können Sie im mit ihr übergebenen benutzerdefinierten Typ MINMAXINFO diese Grenzwerte festlegen - wie immer beim Windows-API in der Pixelskala, in die Werte anderer Skalen ggf. zunächst umgerechnet werden müssen.

Da Ihre Subclassing-Empfängerfunktion - nennen wir sie WindowProc - generisch auf eintreffende Nachrichten aller Art reagieren können muss, steht nun nur noch der Übergang von ihrem idealerweise als Long deklarierten Parameters lParam in eine MINMAXINFO-Variable an. Mithilfe der besser als "CopyMemory" bekannten API-Funktion RtlMoveMemory stellt dies kein größeres Problem dar. Sodann können Sie einfach die Werte der MINMAXINFO-Variablen nach Ihrem Belieben definieren. Damit auch Windows von diesen Angaben erfährt, ist danach wieder der umgekehrte CopyMemory-Weg zu beschreiten, der die geänderten Daten wieder der Verantwortung des WindowProc-Parameters lParam übergibt:

Private Declare Sub CopyMemory _ 
  Lib "kernel32" Alias "RtlMoveMemory" ( _ 
  ByRef Destination As Any, _ 
  ByRef Source As Any, _ 
  ByVal LenBytes As Long) 
' [...] 
Private Function WindowProc(ByVal hWnd As Long, _ 
 ByVal Message As Long, _ 
 ByVal wParam As Long, _ 
 ByVal lParam As Long _ 
 ) As Long 
Dim MMI As MINMAXINFO 
  If Message = WM_GETMINMAXINFO Then 
 ' lParam-Zeiger in MINMAXINFO kopieren: 
 CopyMemory MMI, ByVal lParam, Len(MMI) 
 ' Die Werte der MINMAXINFO-Variablen belegen: 
 With MMI 
   ' Minimale Fenstergröße beim "Ziehen": 
   .MinTrackSize.x = 320 ' Pixel 
   .MinTrackSize.y = 200 ' Pixel 
   ' Maximale Fenstergröße beim "Ziehen": 
   .MaxTrackSize.x = 640 ' Pixel 
   .MaxTrackSize.y = 480 ' Pixel 
 End With 
 ' Die geänderten MINMAXINFO-Daten wieder der 
 ' Verantwortung von lParam überstellen: 
 CopyMemory ByVal lParam, MMI, Len(MMI) 
  End If 
  ' ... 
End Function

Der Dokumentation der Nachricht WM_GETMINMAXINFO ist zu entnehmen, dass beim Auswerten dieser Nachricht die Empfängerfunktion 0 zurückgeben sollte. Daher müssen wir für diesen Fall weder einen Rückgabewert für die Funktion WindowProc festlegen (da der Standardwert 0 ist), noch würde es besonderen Sinn machen, diese Nachricht an das originale Empfängerfenster weiterzuleiten - obgleich selbst das nicht schädlich für unser Ansinnen wäre. Alle anderen Nachrichten, die bei Ihrer umleitenden Empfängerfunktion eintreffen, müssen aber selbstverständlich an den originalen Empfänger weitergeleitet werden.

 

Genug der Theorie - Beispielcode!

Zum Abschluss dieses Artikels erhalten Sie im Folgenden einen vollständigen, sehr einfach gehaltenen Beispielcode, mit dem Sie die vorgestellte Möglichkeit direkt ausprobieren können. Dieses Beispielprojekt vom Typ "Standard-EXE" besteht aus einer Form und einem Standardmodul.

Bitte denken Sie beim Ausprobieren und Experimentieren daran, während aktiven Subclassings grundsätzlich nie den "Stop"-Button der VB-Entwicklungsumgebung zu verwenden, sondern die Anwendung beim Testen in der Entwicklungsumgebung durch einen Klick auf die Schließen-Schaltfläche der eingesetzten Form zu bewirken: Nur hiermit wird das Subclassing wieder aufgehoben, so dass die Anwendung problemfrei beendet werden kann.

Tipp: Kopieren Sie den gesamten Quelltext zunächst in WRITE.EXE und erst von dort aus in Ihre Entwicklungsumgebung, um den Verlust von Zeilenumbrüchen zu vermeiden.

' --------------------------- 
' --- FORMMODUL FORM1.FRM --- 
' --------------------------- 
Option Explicit 
Private Sub Form_Load() 
  Subclass Me, True 
End Sub 
Private Sub Form_Unload(Cancel As Integer) 
  Subclass Me, False 
End Sub 
' ------------------------------------ 
' --- STANDARDMODUL MINMAXINFO.BAS --- 
' ------------------------------------ 
Option Explicit 
' Verwendete Konstante: 
Private Const WM_GETMINMAXINFO As Long = &H24& 
Private Const WM_DESTROY As Long = &H2& 
Private Const GWL_WNDPROC As Long = -4& 
' Verwendete Typen: 
Private Type POINTAPI 
  x As Long  ' Horizontale Angabe (Links / Breite) 
  y As Long  ' Vertikale Angabe (Oben / Hoehe) 
End Type 
Public Type MINMAXINFO 
  Reserved  As POINTAPI  ' Reserviert = bedeutungslos 
  MaxSize   As POINTAPI  ' Größe im maximierten Zustand 
  MaxPosition  As POINTAPI  ' Position im max. Zustand 
  MinTrackSize As POINTAPI  ' Minimale Fenstergröße beim "Ziehen" 
  MaxTrackSize As POINTAPI  ' Maximale Fenstergröße beim "Ziehen" 
End Type 
' Verwendete API-Prozeduren/-Funktionen 
Private Declare Function SetWindowLong _ 
  Lib "user32" Alias "SetWindowLongA" ( _ 
  ByVal hWnd As Long, _ 
  ByVal Index As Long, _ 
  ByVal NewValue As Long _ 
  ) As Long 
Private Declare Function CallWindowProc _ 
  Lib "user32" Alias "CallWindowProcA" ( _ 
  ByVal OriginalWindowProc As Long, _ 
  ByVal hWnd As Long, _ 
  ByVal Message As Long, _ 
  ByVal wParam As Long, _ 
  ByVal lParam As Long _ 
  ) As Long 
Private Declare Sub CopyMemory _ 
  Lib "kernel32" Alias "RtlMoveMemory" ( _ 
  ByRef Destination As Any, _ 
  ByRef Source As Any, _ 
  ByVal LenBytes As Long) 
Private lOldWindowProc As Long ' Adresse der originalen WindowProc 
Public Function Subclass(ByVal f As Form, _ 
 Optional ByVal Active As Boolean = True _ 
 ) As Boolean 
  If Active Then ' Subclassing soll aktiviert werden 
 If lOldWindowProc = 0 Then 
   lOldWindowProc = SetWindowLong(f.hWnd, GWL_WNDPROC, _ 
 AddressOf WindowProc) 
   Subclass = CBool(lOldWindowProc) 
 End If 
  Else ' Subclassing soll beendet werden 
 If lOldWindowProc <> 0 Then 
   SetWindowLong f.hWnd, GWL_WNDPROC, lOldWindowProc 
   lOldWindowProc = 0 
 End If 
  End If 
End Function 
Private Function WindowProc(ByVal hWnd As Long, _ 
 ByVal Message As Long, _ 
 ByVal wParam As Long, _ 
 ByVal lParam As Long _ 
 ) As Long 
Dim MMI As MINMAXINFO 
  If Message = WM_GETMINMAXINFO Then ' das interessiert uns sehr! 
 ' lParam-Zeiger in MINMAXINFO kopieren: 
 CopyMemory MMI, ByVal lParam, Len(MMI) 
 ' Die Werte der MINMAXINFO-Variablen MMI belegen: 
 With MMI 
   ' Minimale Fenstergröße beim "Ziehen": 
   .MinTrackSize.x = 320 ' Pixel minimale Breite 
   .MinTrackSize.y = 200 ' Pixel minimale Höhe 
   ' Maximale Fenstergröße beim "Ziehen": 
   .MaxTrackSize.x = 640 ' Pixel maximale Breite 
   .MaxTrackSize.y = 480 ' Pixel maximale Höhe 
 End With 
 ' Die geänderten MINMAXINFO-Daten wieder der 
 ' Verantwortung von lParam überstellen: 
 CopyMemory ByVal lParam, MMI, Len(MMI) 
  Else  ' das interessiert uns wenig... 
 ' Alle anderen Nachrichten einfach direkt an 
 ' die originale Empfängerfunktion weitergeben: 
 WindowProc = CallWindowProc(lOldWindowProc, hWnd, _ 
  Message, wParam, lParam) 
  End If 
End Function 
' ------------------------------------