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 ' ------------------------------------