Non-Client Area
« | 05 Jan 2020 | »Microsoft Windows stellt für seine Fenster einen Mechanismus bereit, der in vielen Frameworks nicht abgebildet wird.
Neben dem grafischen Inhalt des Fensters, der sogenannten Client-Area darf es optional auch noch einen Rahmen drum herum geben, die Non-Client-Area.
Wozu sind Non-Client-Areas gut?
Zwischen dem X11 Fenstersystem
und Windows gibt es einen wichtigen Unterschied:
X11 beauftragt den installierten Window-Manager, sich um die “Umrahmung” eines
Fensters zu kümmern. Der Entwickler erstellt per API
sein Fenster und kann nur dessen Inhalt kontrollieren und der Window-Manager
zeichnet eine Titelleiste, Min- und Maximierungsbuttons “von selbst” dazu,
wenn man es ihm nicht explizit untersagt.
Microsoft hatte in den Anfängen von Windows aber nicht die Kapazitäten, Fenster
durch mehrere “Teilnehmer” zusammensetzen zu lassen und brachte daher alles im
Anwendungsprozess unter.
Wenn man nun die Koordinatensysteme für den Inhalt eines Fensters und sein
“Drum-herum” trennen möchte, dann kommt man eben zur Idee der Client- und
Non-Client-Area.
Primär wird dieses Feature für Programmfenster (Frame-Windows) benutzt,
die eine Titelleiste haben. Diese Leiste wird von der
DefWindowProc()
automatisch nach den eingestellten Designvorgaben gezeichnet, doch das kann
vom Entwickler stets überschrieben werden.
Manche Programme wollen eben ihr eigenes Design so hervorheben, doch die
meisten Apps belassen es zum Glück beim Standard Design.
Es ist aus meiner Sicht auch kontraproduktiv, das Standardverhalten eines Rahmenfensters abzuändern, schließlich fügen sich nur so alle Programme in ein Einheitliches Look-and-Feel ein.
Wie manipuliert man Non-Client-Areas?
Es gibt vor allem zwei Fensternachrichten, die man Abfangen und bearbeiten muss, wenn man die Non-Client-Area verändern möchte:
WM_NCCALCSIZE
: Die wird beim Verändern der Größe des Fensters aufgerufen oder bei einem entsprechendenSetWindowPos()
und gibt dem Entwickler die Möglichkeit festzulegen, wie breit der Rahmen der Non-Client-Area um die Client-Area herum sein soll.lparam
zeigt dabei immer auf eineRECT
Struktur, die die gesamte Fenstergröße abdeckt. Verkleinert man dieses Rechteck (left und top werden erhöht während bottom und right verkleinert werden), schafft man damit eine neue kleinere Client-Area, und was dann eben “nicht” Client-Area ist, ist die Non-Client-Area.WM_NCPAINT
: Diese spezielle PAINT Routine soll dann in die Non-Client-Area hineinzeichnen, was man eben sehen möchte. Sie wird vor WM_PAINT aufgerufen, arbeitet aber nicht mitBeginPaint()
und EndPaint(). Man kann sich aber mitGetWindowDC()
den Gerätekontext zum Zeichnen selbst holen und loslegen.
Wozu kann man Non-Client-Areas noch nutzen?
Mein konkretes Beispiel ist: Ein Frame-Panel.
Wir kennen alle die “inneren” Rahmen zur Strukturierung von Fensterformularen. Diese Rahmen sind aber in Windows nicht als Container konzipiert wo man Kindelemente wie Textboxen und Buttons hineinlegen kann.
Anstatt dessen sind es in Wahrheit Buttons mit einem speziellen
Button-Style,
die man nicht anklicken kann.
Das war wohl so gedacht, dass man zuerst einen Innenrahmen auf sein
Programmfenster legt und dann Buttons, Texte und Checkboxen einfach
darüber legt.
Das sieht dann optisch so aus, als wären die Elemente Kinder des Innenrahmens, doch in Wahrheit liegt alles auf dem Programmfenster.
Im GATE Projekt stört mich dieser Ansatz der WinAPI, weil auch GTK
sein GtkFrame
als Container (GtkBin
) implementiert hat.
Wenn man also in der WinAPI einen Button als Innenrahmen erzeugt und
Kinder hineinsteckt, dann passt das Koordinatensystem nicht, denn auf
(0, 0)
wird das Rechteck und der Text gezeichnet. Man müsste die Kinder
also künstlich einrücken lassen.
Genau hier kommt die Non-Client-Area ins Spiel:
- Wir definieren ein neues Kind-Fenster, das als Container fungieren
kann (
WS_EX_CONTROLPARENT
). - In dem Fenster überschreiben wir dann
WM_NCCALCSIZE
und zwacken links, rechts und unten ein paar Pixel ab, und oben stehlen wir mindestens so viel, wie wir für die Schriftart brauchen. - In der umgeleiteten Nachricht
WM_NCPAINT
holen wir mitGetWindowDC()
denDC
des gesamten Fensters (und damit der Non-Client-Area) und stellen mit FillRect, DrawEdge und DrawText das Aussehen des Innenrahmens nach. - Mit
SetWindowPos(..., SWP_DRAWFRAME | SWP_FRAMECHANGED)
garantieren wir beim Erstellen des Fensters, dassWM_NCCALCSIZE
auch tatsächlich aufgerufen wird. (Das ist regulär nicht erforderlich, wenn man seine eigene WindowProc richtig geschrieben hat, doch im GATE Projekt erfolgen im UI Zwischenschritte (Stichwort Subclassing), woWM_NCCALCSIZE
ganz am Anfang noch nicht umgeleitet ist.)
Fertig!
Jetzt haben wir ein Container-Control, dessen Kinder mit dem Koordinatensystem
(0, 0)
innerhalb des vorhin selbst gezeichneten Rahmens starten.
Das macht das Arbeiten dann wesentlich einfacher und hilft auch dabei
seinen Code besser kapseln zu können.
Fazit
Tatsächlich ist es jetzt hier gerade das erste mal in meinem Leben, dass ich die Non-Client-Area in einem Kind-Element nutze. Es war bisher nicht erforderlich.
Das obige Beispiel mit dem “Innenrahmen” hatte ich vor über 10 Jahren schon
mal vor meinen Augen, doch damals versuchte ich es mit Koordinatenumrechnungen
und musste dann feststellen, dass das Button-Control im Frame-Modus
sehr hässlich zu flimmern begann, wenn sich die Fensterbreite änderte.
Und dieser Bug wurde dann wieder mit
WM_PAINT
Hacks gelöst.
Mit der Non-Client-Area Methode ist die Sache meines Erachtens viel besser gelöst und nebenbei auch portabler zu anderen Frameworks.
Jedenfalls find ich’s cool, dass der Ansatz out-of-the-box funktionierte.