Non-Client Area

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 entsprechenden SetWindowPos() 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 eine RECT 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 mit BeginPaint() und EndPaint(). Man kann sich aber mit GetWindowDC() 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 mit GetWindowDC() den DC 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, dass WM_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), wo WM_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.