Programmierung von Fenstern

Zur Erstellung eines Fensters muß es zunächst beim System angemeldet werden und das Fensterhandle ermittelt werden. Anschließend wird es gezeichnet. Vor Beendigung des Programmes wird das Fenster geschlossen und abgemeldet.

Es müssen die Funktionen erstellt werden, die auf die verschiedenen Ereignisse reagieren. Dabei ist beispielsweise eine Funktion erforderlich, die jederzeit den Fensterinhalt im aktuellen Zustand rekonstruieren kann. Diese Funktion wird benötigt, wenn das System das Programm zum Neuzeichnen des Fensters auffordert.

Anmelden und Abmelden von Fenstern

Ein Fenster ist nicht nur einfach ein Rechteck auf dem Bildschirm. Es ist ein Objekt, das verschoben oder vergrößert werden kann, das auf Mausklicks oder Tastendrücke reagiert und zu einem bestimmten Programm gehört. Da die grafische Oberfläche in dieser Form mit jedem Fenster umgehen muß, ist es notwendig, daß sie eine Fensterliste hält, in der alle Fenster eingetragen sind. Bevor man ein Fenster benutzen kann, muß es also beim System angemeldet werden und von diesem in die Liste eingetragen werden. Das Programm erhält ein Handle zurück, an dem beide das Fenster eindeutig wiedererkennen können. Erst im nächsten Schritt wird die grafische Darstellung des Fensters durchgeführt und damit das Fenster eröffnet.

Erzeugen und Eröffnen von Fenstern

Das Zeichnen der Rahmenelemente wird meist vom System übernommen. Das Programm muß diesem die gewünschten Rahmenelemente des neuen Fensters angeben. Zu diesem Zweck wird ein Parameter übergeben, der die Rahmenelemente enthält. Jedes Bit dieses Parameters steht für ein Rahmenelement. Damit die Rahmenelemente im Listing leichter erkennbar sind, verwendet man Konstanten, die in den include-Dateien definiert sind. Durch Odern oder Addition dieser Konstanten ergibt sich besagter Parameter. Hier ein Beispiel für die Eröffnung eines Fensters unter GEM:


   /* Die gewuenschten Rahmenelemente */
   FensterStil = CLOSER | MOVER | SIZER  . . . ;

   FensterHandle = wind_create(FensterStil,maxx,maxy,maxw,maxh);
   if (FensterHandle < 0) {
      /* kein Fenster mehr zur Verfuegung */
   } else {
      if (wind_open(FensterHandle, posx,posy,posw,posh) == 0) {
         /* Fehler aufgetreten */
      }
      . . .
   }
   . . .

   wind_close(FensterHandle);  /* Schliessen */
   wind_delete(FensterHandle); /* Abmelden */

Zu Anfang sieht man die Definition der Rahmenelemente. Im Beispiel soll das Fenster eine Schließbox, einen verschiebbaren Titelbalken und eine Vergrößerungsbox enthalten. Mit diesem Parameter wird die Funktion wind_create aufgerufen, die das Fensterhandle zurückliefert, sofern GEM noch Fenster zur Verfügung hat. Da GEM ursprünglich eine begrenzte Zahl von Fenstern hatte, ist die Abfrage, ob noch ein Fensterhandle zur Verfügung steht, von besonderer Bedeutung. Die Positionskoordinaten mit dem Präfix max sind die maximal zulässige Ausdehnung des Fensters. Im Normalfall verwendet man hier die Dimensionen des Desktops. Sie sagen etwas darüber aus, wie groß das Fenster im Laufe des Programms höchstens werden kann. Das Fenster ist nach diesem Aufruf noch nicht auf dem Bildschirm sichtbar.

Erst die Funktion wind_open erzeugt auf dem Bildschirm einen sichtbaren Rahmen des Fensters. Der weiße Hintergrund des Fensters muß durch den Programmierer erzeugt werden, indem er anschließend ein weiß gefülltes Rechteck mit den Koordinaten des Arbeitsbereiches zeichnet. Die Koordinaten mit dem Präfix pos legen die tatsächliche Position des Fensters fest. Da eine GEM-Applikation den ganzen Bildschirm für sich allein hat, werden hier meist ebenfalls die Desktopdimensionen abzüglich eines kleinen Anstandsrestes verwendet.

Fensterklasse und Fensterfunktion

Ganz ähnlich sieht das Anmelden eines Fensters bei MS-Windows und Presentation Manager aus. Das Erzeugen und Eröffnen wird in zwei Schritten durchgeführt und die Bestandteile des Rahmens werden in einer Variablen bitweise zusammengesetzt. Allerdings wird vor die Erstellung eines Fensters die Definition einer Fensterklasse gestellt. Hier ein Beispiel für die Anmeldung eines Fensters:

#define FENSTERKLASSE "FensterKlasse"

   WinRegisterClass (Handle,
            FENSTERKLASSE,
            (PFNWP) FensterFunktion,
            CS_SIZEREDRAW,
            0);

   RahmenHandle = WinCreateStdWindow (
            HWND_DESKTOP,
            0L, &FensterStil,
            FENSTERKLASSE,
            "Fenstertext",
            0, (HMODULE) 0L,
            ID_MAIN, /* Kennung des Menues, sonst 0 */
            &ClientHandle);

   WinSetWindowPos(RahmenHandle,
            HWND_TOP,
            xlinksoben, ylinksoben,
            xrechtsunten, yrechtsunten,
            Option);

Der Aufruf von WinRegisterClass hat die Aufgabe, eine Klasse von Fenstern zu definieren, die die gleiche Fensterfunktion verwenden. Das bedeutet, daß beim Eintreffen von Ereignissen in einem dieser Fenster das System die gleiche Funktion aufrufen wird. Die Fensterklasse wird durch eine Zeichenkette definiert. Der Name ist frei wählbar, dient aber auch bei der Kommunikation zwischen Prozessen als Adresse. Dementsprechend ist es sinnvoll, die Applikationsfensterklasse dem Zweck des Programms entsprechend zu benennen. Weitere Fensterklassen des Programms bennent man üblicherweise so, daß man den Namen der Applikationsfensterklasse voranstellt. Da der Inhalt des Strings für die korrekte Zuordnung eines Fensters zu seiner Fensterklasse von Bedeutung ist und Tippfehler innerhalb des Strings vom Compiler nicht bemerkt würden, wird hier die Stringkonstante per #define einer symbolischen Konstante zugeordnet. Wird der Name der Konstanten falsch eingegeben, meldet der Compiler, daß er den Namen nicht kennt. Sollte man sich im Namen der Fensterklasse bei der Definition vertippt haben, hat dies im Normalfall überhaupt keinen Einfluß auf den Ablauf des Programms, solange nicht eine zweite Fensterklasse zufällig den gleichen Namen hat.

Im nächsten Schritt wird das Fenster erzeugt. Damit ist es noch nicht auf dem Bildschirm zu sehen. Es wird lediglich das Fensterhandle angefordert und das Fenster beim Presentation Manager angemeldet. Durch die Angabe der Fensterklasse weiß das System bereits jetzt, welche Applikationsfunktion für die Ereignisse zuständig ist. Bereits an dieser Stelle wird die Fensterfunktion das erste Mal aufgerufen. Die Nachricht lautet WM_CREATE und bedeutet, daß das Fenster soeben erzeugt wurde.

Betrachtet man noch einmal den Funktionsaufruf von WinCreateStdWindow genauer, fällt die Existenz zweier Handles auf. Das hängt damit zusammen, daß ein Standardfenster bei MS-Windows und Presentation Manager eigentlich aus zwei Elementen besteht: dem Rahmen und dem Fensterinhalt. Dabei ist der Rahmen das Elternfenster des Inhalts. Der Fensterinhalt wird auch Clientbereich des Fensters genannt. Das im Beispiel RahmenHandle genannte Handle ermöglicht den Zugriff auf den Rahmen. Es wird benötigt, wenn auf Rahmenbestandteile zugegriffen werden muß. Dieses Handle wird zum Beispiel bei der Behandlung von Menüs relevant, die als Bestandteil des Rahmens verstanden werden. Das zweite Handle, im Beispiel ClientHandle genannt, bezeichnet den Arbeitsbereich, also den Inhalt des Fensters. Dieses Handle erhält auch die Fensterfunktion als ersten Parameter, da dieses Handle für alle Objekte, die sich innerhalb des Fensters tummeln, zuständig ist und auch benötigt wird, wenn innerhalb des Arbeitsfensters gezeichnet wird.

Die Funktion WinSetWindowPos stellt das Fenster schließlich dar. Der Name dieser Funktion ist insofern etwas irreleitend, als daß es keineswegs erforderlich ist, an dieser Stelle konkrete Bildschirmpositionen vom Programm aus festzulegen. Vielmehr handelt es sich um eine Funktion, die verschiedene Aufgaben ausführt, je nachdem, wie der letzte Parameter besetzt ist. In der folgenden Tabelle sind einige der möglichen Konstanten und ihre Bedeutung aufgeführt, die der Funktion mitgegeben werden können. Es kann durch Odern oder Addieren der Konstanten erreicht werden, daß auch mehrere Aktionen gleichzeitig durchgeführt werden.

SWP_SIZE Änderung der Fenstergröße
SWP_MOVE Verschieben eines Fensters
SWP_HIDE Das Fenster wird unsichtbar
SWP_SHOW Das Fenster wird sichtbar
SWP_ACTIVATE Aktiviere das Fenster

Zum Schluß des Programms sollte man das Hauptfenster wieder abmelden. Dies wird mit der Funktion WinDestroyWindow(FensterHandle) erledigt. Auch diese Funktion führt zu einer Nachricht an die Fensterfunktion. Sie kann nun noch Datenbestände sichern. Das Entfernen des Fensters kann sie nicht mehr verhindern.

Beim Erzeugen eines Fensters wird als erster Parameter ein Fensterhandle angegeben. Dabei handelt es sich um das Handle des Elternfensters. Für das Hauptfenster einer Applikation ist dies der Desktop, welcher die Konstante HWD_DESKTOP als Fensterhandle hat. Um ganz genau zu sein, muß hier das Handle des Fensterinhaltes angegeben werden. Wird ein weiteres Fenster angelegt und diesem das Clienthandle des Applikationsfensters als erster Parameter übergeben, wird dieses innerhalb des Fensterinhalts dargestellt. Das heißt, daß es auch nicht über den Rand des Elternfensters hinaus dargestellt wird. Es können beliebig viele Generationen von Fenstern erzeugt werden, die jeweils von den Eltern begrenzt werden.

Erzeugung von Widgetbäumen

Während die Erzeugung von Fenstern bei den anderen grafischen Oberflächen relativ ähnlich abläuft, findet sich bei X ein etwas anderes Konzept. Es beginnt damit, daß der Rahmen eines Fensters nicht in den Verantwortungsbereich des Anwendungsprogrammierers fällt, sondern zum Window-Manager \index{Window-Manager} gehört. Des weiteren versteht man bei X unter dem Begriff Fenster ein Objekt der Xlib. Das Fenster bildet den Hintergrund eines jeden Widgets, so daß also jedes Widget sein eigenes Fenster besitzt und dieses mit der Erzeugung des Widgets angelegt wird.

Betrachtet man das Fenster eines X-Programmes, so besteht dieses im Normalfall aus mehreren Widgets. Um die Zusammensetzung eines solchen Fensters zu verstehen, ist es zunächst notwendig, die Widgets in drei Kategorien einzuteilen.

Um ein Anwendungsfenster zu erzeugen, wird ein Baum von Widgets aufgebaut. Dabei wird dem Widget bei seiner Erzeugung die Kennung des Elternfenster mitgegeben. Dies war bereits beim Presentation Manager oder MS-Windows anhand der Fenster zu sehen. Aus Sicht des Programmierers werden die funktionalen Widgets ausgewählt und in solchen Management-Widgets zusammengefaßt, die der gewünschten Anordnungsstrategie entsprechen. Diese Module können wiederum in anderen Management-Widgets zusammengefaßt werden. Schließlich wird das passende Shell-Widget verwendet, um den Kontakt zum Window-Manager zu gewährleisten.

Auch unter X gibt es eine Funktion, die ein Widget erzeugt und davon getrennt eine andere Funktion, die das erzeugte Widget zur Darstellung bringt. Zur Veranschaulichung dienen Ausschnitte aus dem bereits gezeigten Programm. Die Anmeldung der Callbackfunktionen sind herausgenommen, da sie bereits betrachtet wurde. Auch die Initialisierung des Programmes ist an dieser Stelle nicht interessant und aus dem Listing entfernt worden.

...

void initWidgetBaum(Widget top)
{
Widget wRahmen, wCount, wQuit;

    wRahmen = XtCreateManagedWidget("rahmen",
                    xmRowColumnWidgetClass, top, NULL, 0);
    wMsg = XtCreateManagedWidget("message",
                    xmLabelWidgetClass, wRahmen, NULL, 0);
    wCount = XtCreateManagedWidget("count",
                    xmPushButtonWidgetClass, wRahmen, NULL, 0);
    wQuit = XtCreateManagedWidget("quit",
                    xmPushButtonWidgetClass, wRahmen, NULL, 0);
}

main(Cardinal argc, char **argv)
{
Widget topShell;
...

    ...
    topShell = XtAppCreateShell(NULL, Applikation,
                     applicationShellWidgetClass, display, NULL, 0);
    initWidgetBaum(topShell);
    XtRealizeWidget(topShell);
    ...

Zunächst wird die Widgetkennung der Applikation bestimmt. Diese ist von der Klasse ApplicationShell und heißt im Beispiel topShell. Die Shell ist wie gesagt unsichtbar, erhält aber bei Erscheinen auf dem Bildschirm vom Window-Manager einen Rahmen. Sie kann nur ein Widget aufnehmen. In der Funktion initWidgetBaum wird als erstes ein Management-Widget der Klasse RowColumn angelegt. Dieses ordnet seine Kinder einfach untereinander an. Bei Erzeugung dieses Widgets wird das Shell-Widget als Vater angegeben. Die folgenden Widgets, ein Label und zwei Knöpfe, sind wiederum Kinder dieses Management-Widgets. Man erkennt dies daran, daß ihnen die Widgetkennung des RowColumn-Widget als Parameter übergeben wird. Die Entstehung der Widgets geschieht in drei Schritten. Zunächst werden sie erzeugt (create). Damit ist der Speicher zur Verfügung gestellt und die Widgets sind initialisiert. Im nächsten Schritt werden sie vom Programm gehandhabt (manage). Dies ist der Vorgang, daß sie in die Widget-Hierarchie des Programmes eingegliedert werden und die Platzansprüche mit den anderen Widgets abgestimmt werden. Im letzten Schritt wird das Shell-Widget dargestellt (realize). Das hat zur Folge, daß auch alle gehandhabten Kinder zur Darstellung gebracht werden. Der Einfachheit halber wird mit der Funktion XtCreateManagedWidget das Erzeugen und Handhaben in einem Schritt ausgeführt. Nachdem der Baum erstmalig dargestellt ist, reicht es aus, ein Widget zu handhaben, um es darzustellen. Entsprechend taucht die Funktion XtRealizeWidget typischerweise nur einmal in einem Programm auf.

Beim Handhaben werden die Größenausdehnungen der Widgets festgelegt. Die sichtbaren Widgets reichen ihre Idealvorstellungen bezüglich ihrer Größe an das Eltern-Management-Widget weiter. Dieses stellt die Anforderungen aller seiner Kinder zusammen und reicht sie an ein gegebenenfalls darüberliegendes Managementwidget bis sie zum Rahmenwidget gelangen, welches die Anforderungen an den Window-Manager weiterleitet. Dieser stellt fest, welche Ausdehnung auf dem Schirm möglich ist und reicht diese wieder zurück an das Rahmenwidget, welches es an die Managementwidgets weitergibt. Diese bestimmen, welche sichtbaren Widgets welchen Raum erhalten und reichen sie ihrerseits weiter. Wie man sich vorstellen kann, ist dieser Vogang recht aufwendig. Aus diesem Grund empfielt es sich, Management-Widgets zunächst nur zu erzeugen und erst dann zu handhaben, wenn alle Kinder angelegt worden sind. Auf diese Weise erspart man die erneute Abstimmung nach der Erzeugung jedes Kindwidgets.

Sehr interessant ist bei diesem Konzept die völlige Abwesenheit von Koordinaten als Parameter beim Erstellen eines Fensters. Das hängt damit zusammen, daß die Größe des benötigten Fensters im Normalfall ohne den Eingriff des Programmierers bestimmt wird, sondern durch die Ermittlung eines Kompromisses zwischen dem Platz, den der Inhalt eines Widgets benötigt und dem Platz, den der Window-Manager zur Verfügung stellen kann. Dieser Mechanismus bedingt, daß sich das Programm auch nicht mit einem Vergrößerungsereignis befassen muß.

Das MainWindow-Widget

Für die Gestaltung des Hauptfensters werden immer wieder die gleichen Elemente zusammengesetzt. Zuoberst befindet sich die Menüleiste, darunter ein Arbeitsbereich, der meist mit Rollbalken ausgestattet ist. Ober- oder unterhalb des Arbeitsbereich findet sich oft noch ein Bereich, in dem Kommandos ausgegeben werden oder der Programmstatus angezeigt wird.

OSF/Motif unterstützt diese Art von Hauptfenstern durch ein eigenes Management-Widget namens MainWindow, das genau diese Bestandteile aufnehmen kann. Da allein das Einhängen der Menüleiste in den Kopf des Fensters einfacher ist, als die Motage in einem eigenen Form-Widget, findet man diesen Typ in fast jedem OSF/Motif-Programm.

Die Rollbalken sind optional und können weggeblendet werden. Dies ist empfehlenswert bei Elementen, die in erster Linie mit Text arbeiten, da sie über einen auf Text spezialisierten Rollbalken verfügen. Diese sind sehr viel schneller als die Allzweckrollbalken des MainWindow, die auf Pixelbasis operieren. Für das Zusammensetzen des MainWindow gibt es eine eigene Funktion.

    /* Das MainWindow wird zusammengesetzt */
    XmMainWindowSetAreas(wRahmen,
                         wMenu    /* Menue */,
                         wPara,   /* Command */
                         NULL,    /* horiz.  Scrollbar */
                         NULL,    /* vertik. Scrollbar */
                         XtParent(wList));

Die Paramter sind die Widgetkennungen der beteiligten Elemente. Der erste Parameter ist die Kennung des MainWindow. Es folgen Menü, Kommandoelement, die Rollbalken und schließlich der Arbeitsbereich. In dem Beispiel oben werden die Rollbalken des MainWindow ausgeblendet, indem NULL übergeben wird. Die zuletzt angegebene Liste besitzt eigene Rollbalken. Aus diesem Grund muß das Elternwidget angegeben werden, da die Rollbalken die Außengrenze des Elements festlegen. Das besonders interessante an diesem Element ist, daß es quasi eine Flankenunterstützung des Style Guide darstellt. Die Verwendung des MainWindow ist Style-Guide-konform. Das heißt, daß es für den Programmierer einfacher ist, sich an die Regeln zu halten, als sie zu umgehen.

Die Verwaltung der Fensterereignisse

Nachdem das Fenster glücklich erstellt worden ist, muß sich das Programm mit den Ereignissen befassen, die auf ein Fenster einwirken können. Bei den für das Fenster ausgelösten Nachrichten ergeben sich für alle Systeme ähnliche Ereignisse. Sie werden im Normalfall mit Konstanten benannt. Hier eine Aufstellung der wichtigsten Nachrichten: Betrachten wir die einzelnen Nachrichten im Detail. Die meistbenötigte Funktion ist die zum Bearbeiten der Aufforderung zum Neuzeichnen des Fensterinhalts. Dies wird erforderlich, wenn das Fenster zwischenzeitlich verdeckt war oder aus einer Position außerhalb des sichtbaren Bereichs wieder zurückgeschoben wurde. Im Normalfall wird das Programm in der Lage sein müssen, seinen Fensterinhalt selbst jederzeit wiederherstellen zu können. Nähere Aspekte werden an anderer Stelle behandelt.

Die Nachricht über das Anklicken der Schließbox betrifft auch die Anwendung, da es bedeuten kann, daß der Benutzer den Fensterinhalt verwirft. Die Applikation muß in einem solchen Fall gegebenenfalls bisher ungesicherte Daten speichern oder nach Rückfrage verwerfen. Eine Abmeldung des Fensters ist in jedem Fall notwendig. Die Vergrößerungsbox des Fensters bringt beim ersten Anklicken das Fenster auf volle Bildschirmgröße und beim zweiten Mal auf die letzte Größe zurück. Bei PM und MS-Windows braucht die spezielle Nachricht für die Vergrößerungsbox nicht beachtet zu werden, da die Standardfensterfunktion die Größenveränderung automatisch durchführt. Danach wird eine weitere Nachricht gesendet, daß die Größe des Fensters sich geändert hat. Für das Programm reicht es normalerweise aus, auf diese Nachricht zu reagieren. Es erhält mit dieser Nachricht die neuen Koordinaten des Fensters. Bei GEM erfordert diese Nachricht die Speicherung der aktuellen Fensterposition und die manuelle Positionierung des Fensters auf die maximale Fenstergröße. Unter GEM muß auch festgestellt werden, ob das Fenster bereits auf maximale Fenstergröße eingestellt war, um es auf die Originalgröße reduzieren zu können. Mit dem Aufkommen von Großbildschirmen wird der ursprüngliche Gedanke, daß mit Hilfe eines einzigen Klicks die Applikation den gesamten Bildschirm erhält, nicht mehr so interessant. Das Vergrößern auf volle Größe ist in vielen Fällen sogar eher lästig. Aus diesem Grund geht der Trend auf dem Macintosh dahin, daß eine Applikation auf den Empfang einer Fullsize-Nachricht den maximalen Bildschirmausschnitt einnimmt, der für den Fensterinhalt sinnvoll ist.

Die Nachricht, daß ein hinter einem anderen Fenster liegendes Fenster angeklickt wurde und damit in den Vordergrund kommt, kann bei PM oder MS-Windows ignoriert werden, da das Umschalten zwischen den Fenstern vom System gehandhabt wird. Hier würde eventuell eine andere Fensterfunktion aktiviert, wenn die beiden Fenster nicht zur gleichen Fensterklasse gehören. Ansonsten ist das Umschalten nur durch ein anderes Fensterhandle in der Nachricht bemerkbar. GEM-Programme brauchen dagegen diese Nachricht, da sie selbst das Fenster in den Vordergrund bringen müssen. Dies ist noch recht einfach durch einen einfachen Funktionsaufruf zu bewerkstelligen. Bei Applikationen mit unterschiedlichen Fenstertypen kann dieses Ereignis eine Uminterpretation der eintreffenden Ereignisse bedeuten, da hier nicht für jedes Fenster eine eigene Fensterfunktion realisierbar ist. Allerdings kann in solchen Fällen für jede Fensterart eine eigene Ereignisschleife geschrieben werden, die bei Wechsel des Fensters aufgerufen wird. Der Macintosh kennt noch eine zusätzliche Nachricht, daß ein Fenster als oberstes Fenster verdrängt wurde. Dies ist erforderlich, da der Programmierer dort die Deaktivierung des Rollbalkens selbst ausführen muß und auch markierte Bereiche wieder auflösen sollte, da nach den Stilvorschriften immer nur im aktiven Fenster ein Block markiert sein soll.

Das Verschieben eines Fensters führt bei GEM zu einer Veränderung aller Fensterkoordinaten des Inhalts. Das Programm muß das Verschieben des Fensters selbst veranlassen. Wiederum sind PM und MS-Windows Programme von dieser Nachricht meist nicht betroffen. Das Verschieben führt die Standardfensterfunktion selbst durch und da die Koordinaten fensterbezogen sind, hat es keinen Einfluß auf die Applikation. Lediglich in dem Fall, daß ein Kindfenster teilweise aus dem Bereich des Elternfenster herausgeschoben wird, kann eine Erzeugung von Rollbalken im Elternfenster erforderlich machen.

GEM erhält die Fensterereignisse wie alle anderen Ereignisse nach dem Aufruf der Funktion evnt_multi als Rückgabewert. Als Parameter an evnt_multi wird unter anderem ein Array übergeben, das nach einem Fensterereignis nähere Informationen enthält. Dies sind einmal das Handle des betroffenen Fenster und die Bildschirmpositionen, die für die Bearbeitung des Ereignisses benötigt werden. Letztere können nur im Zusammenhang mit der Nachricht interpretiert werden. Beim Verschieben geben sie z.~B. die Zielposition an.

   aktion = evnt_multi(handle, . . ., Bereich, . . .      );
   switch(aktion) {
      . . .
      case WMMOVED:
         Verschiebe(Bereich);
      . . .
   }
   . . .

void Verschiebe(int *Bereich)
{
   /* Bereich[0] bis [2] enthalten nachrichtenspezifische Details.
    */
   wind_set(message[3], /* Handle des Fensters */
         WF_CURRXYWH,   /* Position aendern    */
         Bereich[4],    /* neue x-Koordinate */
         Bereich[5],    /* neue y_Koordinate */
         Bereich[6],    /* Fensterbreite (unveraendert) */
         Bereich[7]);   /* Fensterhoehe (unveraendert) */
}

Der Mehraufwand für die Verschiebung des Fensters, die in anderen Oberflächen vom System übernommen wird, hält sich in Grenzen. Es sind die Offsetwerte für die Koordinaten zu korrigieren, falls sich das GEM-Programm diese in eigenen Variablen merkt, um sie nicht jedesmal vom System erfragen zu müssen[1] und es ist die Verschiebung des Fensters durchzuführen, was durch eine Systemfunktion leicht zu realisieren ist.

Bei PM und bei MS-Windows ist die Verwaltung der Fenster durch die Fensterfunktion geprägt. Liegt ein Ereignis für ein Fenster an, ruft das System die Funktion auf, die für diese Fensterklasse registriert wurde. Im folgenden sind die wichtigsten Nachrichten aufgeführt:

WM_CREATE Das Fenster wurde erzeugt, ist aber noch nicht sichtbar
WM_PAINT Es ist notwendig, den Inhalt des Fensters neu aufzubauen
WM_SIZE Die Größe des Fensters ist verändert worden
WM_CLOSE Das Fenster ist geschlossen worden
WM_COMMAND Eine Taste oder ein Druckknopf im Fenster wurde gedrückt
WM_CONTROL Ein Kontrollelement des Fensters ist betätigt worden

Die Nachricht WM_CREATE tritt genau einmal nach dem Erzeugen des Fensters auf. Es ist bis dahin noch nicht auf dem Bildschirm zu sehen. Wer also zum Beispiel bei Erscheinen des Fensters gern sein Konterfei erscheinen lassen möchte, wird an dieser Stelle auf Granit beißen. Diese Nachricht dient dazu, Vorbereitungen für die Fensterbearbeitung treffen, wie etwa das Anfordern von Speicher, um später Bildschirmkopien herstellen zu können oder das Erzeugen (nicht das Darstellen!) von Kindfenstern. Hier werden häufig auch die Standardtexte, die später im Fenster erscheinen sollen, in den Variablen vorbesetzt. Angezeigt werden sie allerdings erst beim Eintreffen der WM_PAINT-Nachricht.

Die WM_PAINT-Nachricht trifft erstmals bei der ersten Darstellung des Fensters ein. Ansonsten erscheint sie immer dann, wenn ein Ereignis eingetreten ist, was ein Neuzeichnen des Fensterinhalts erforderlich macht.

In einem PM-Programm kann man alle Ausgabefunktionen, die den Aufbau des Bildschirms bewirken, an dieser Stelle konzentrieren. Ergibt sich eine Änderung des Bildschirms an einer anderen Stelle des Programms, werden die Parameter für die Ausgabefunktionen verändert und anschließend eine WM_PAINT-Nachricht durch den Aufruf der Funktion WinInvalidateRect provoziert. Man übergibt ihr den neu zu zeichnenden Bildschirmbereich und braucht sich an dieser Stelle nicht weiter um das Zeichnen zu kümmern. Das System wird bei nächster Gelegenheit eine WM_PAINT-Nachricht auslösen und die Fensterfunktion wird den Bereich neu zeichnen.

Die WM_SIZE-Nachricht tritt auf, wenn das Fenster in seiner Größe verändert worden ist. Meist führt das dazu, daß auch Teile des Bildschirms neu gezeichnet werden müssen. Man korrigiert an dieser Stelle die Koordinaten und legt gegebenenfalls zusätzliche Informationen in Variablen ab, die später bei Neuzeichnen des Fensters verwendet werden. Gerade bei Grafikapplikationen stellt sich die Frage, ob die Vergrößerung des Fensters einen größeren Ausschnitt zeigt oder die Skalierung der Grafik verändert. In letztem Fall muß die gesamte Grafik neu aufgebaut werden. Aus diesem Grund gibt es zwei verschiedene Redraw-Strategien. Normalerweise wird bei Vergrößerung nur der hinzugekommene Bereich neugezeichnet. Wird der Fensterklasse bei ihrer Definition die Eigenschaft SIZEREDRAW mitgegeben, führt jede Veränderung der Größe des Fensters, also auch eine Verkleinerung, zu einer WM_PAINT-Nachricht für den gesamten Fensterinhalt.

WM_CLOSE ist die Nachricht, daß der Benutzer soeben das Fenster geschlossen hat. Hier ist die Möglichkeit, Speicherbereiche oder andere Ressourcen freizugeben, die dieses Fenster bis jetzt belegt hat. Handelt es sich um die Fensterfunktion des Hauptfensters, kann man davon ausgehen, daß der Benutzer das Programm beenden will. In diesem Fall sendet man die WM_QUIT-Nachricht, die dem System mitteilt, daß das Programm beendet werden soll. Diese Nachricht wird auch die Hauptereignisschleife in der Hauptfunktion beenden und damit die Abmeldung des Programmes einleiten. Bei einem normalen Fenster wird man das System durch Aufruf der Funktion WinDestroyWindow zum Entfernen des Fensters auffordern.

WM_COMMAND ist die Nachricht, daß ein Kommando eingetroffen ist. Um festzustellen, durch welche konkrete Aktion das Ereignis ausgelöst wurde, wird der erste Parameter zerlegt. Im ersten Wort steht das Kommando. Es folgt nun typischerweise eine weitere Fallunterscheidung.

WM_CONTROL wird ausgelöst, wenn eines der Kontrollelemente im Arbeitsbereich des Fensters betätigt wurde.

Die WM_CONTROL und WM_COMMAND Fälle werden im Zusammenhang mit den Dialogboxen näher betrachtet.

Ganz anders verhalten sich die Widgets unter X. Sie übernehmen vollständig die optischen Aspekte der Benutzeraktionen. Wird beispielsweise die Maus über einem Druckknopf gedrückt, wird dieser eingedrückt dargestellt. Wird die Maustaste anschließend über dem Druckknopf losgelassen, geht der Knopf wieder nach außen und der Knopf wurde angewählt. Die sichtbare Reaktion des Knopfes wird bereits bei der Konstruktion des Widgets realisiert, indem für die verschiedenen Ereignisse der Struktur des Widget jeweils ein Zeiger auf eine Funktion zugefügt wurde, die bei Eintreffen des Ereignisses aufgerufen wird. Zu jedem Widget gehören bereits Standardfunktionen, die insbesondere für die grafische Umsetzung der Ereignisse zuständig sind. Dadurch scheint ein Widgetbaum bereits selbstständig zu arbeiten, ohne daß die Applikation eine einzige Funktion beisteuert. Darüber hinaus ``kennt'' jedes Widget sein eigenes Aussehen und kann dieses durch entsprechende Standardfunktionen jederzeit rekonstruieren. Aus diesem Grund braucht eine Bearbeitung der Nachricht zum Neuzeichnen des Bildschirms nicht durch das Programm erfolgen, solange es sich der Standardwidgets bedient. Aus dem gleichen Grund ist auch das Verschieben eines Fensters kein Ereignis, das der Programmierer bearbeiten muß.

Auch die Veränderung der Fenstergröße wird durch die Widgets selbstständig geregelt. Diese Aufgabe übernehmen die verwendeten Management-Widgets, die nicht nur die anfängliche Ausrichtung ihrer Kinder steuern, sondern auch Änderungen der Außenmaße an ihre Kinder weiterleiten. Je nach Art des Widgets wird hier nach unterschiedlichen Strategien vorgegangen. Die einfachsten Managementwidgets sind solche, die ihre Kinder vorzugsweise senkrecht oder waagerecht anordnen. So werden die Auswahlknöpfe in einer Menüleiste vorzugsweise waagerecht angeordnet. Wenn der Platz nach rechts nicht reicht, wird eine weitere Zeile angelegt, in der die restlichen Knöpfe angeordnet werden.

Neben den einfachen Management-Widgets, die die ihnen zugewiesene Fläche gleichmäßig auf ihre Kinder verteilen, gibt es die Constraint-Widgets. Diese sind in der Lage für jedes Kind unterschiedliche Anordnungsstrategien zu verwenden, insbesondere im Falle einer Veränderung der Größe. Ein typischer Vertreter dieser Klasse ist das Formwidget. Das Besondere am Formwidget ist die Möglichkeit, daß die Kindwidgets in ihrer Position durch ihr Verhältnis zu anderen Kindwidgets oder zum Rahmen festgelegt werden können. Ferner kann eingestellt werden, welche Widgets eine Vergrößerung des Außenrahmens nachvollziehen und welche bei Vergrößerung des Gesamtfensters in ihrer Größe unverändert bleiben.

Auf diese Weise erstellt der X Programmierer einen Baum von Widgets, in dem er die Abhängigkeiten der Widgets voneinander festlegt und durch die Auswahl der Managementwidgets bestimmt, nach welcher Strategie sich eine Vergrößerung oder Verkleinerung des Gesamtfensters auf die einzelnen Widgets auswirkt. Nachdem dieser Baum einmal erstellt ist und die Abhängigkeiten festgelegt sind, reagiert das Fenster selbstständig auf alle typischen Fensterereignisse.

Lediglich in dem Fall, daß eine direkte Grafikprogrammierung benötigt wird, wie etwa bei CAD-Systemen, müssen die entsprechenden Mechanismen vom Anwendungsprogrammierer übernommen werden. Dazu gibt es unter OSF/Motif das DrawingArea-Widget, welches die freie Gestaltung des Inhalts zuläßt. In diesem Fall ist natürlich auch die Bearbeitung der Ereignisse, wie Neuzeichnen und Größenveränderung, vom Programm selbst zu bewerkstelligen. Zu diesem Zweck besitzt dieses Widget eigene Callbacklisten, für die die entsprechenden Routinen geschrieben werden müssen.

Koordinatenunabhängigkeit

Eine Grundregel bei der Erstellung von Applikationen unter grafischen Oberflächen ist die koordinatenunabhängige Programmierung. Durch die Festlegung von Koordinaten werden Programme auf die Randbedingungen der Entwicklungsmaschine festgelegt und sind oft nicht in der Lage, unter anderen Auflösungen zu arbeiten. Bei Systemen mit geringerer Auflösung können solche Programme sogar eventuell gar nicht mehr laufen. In diesem Zusammenhang bekommt vor allem das Verhalten des Programmes bei Vergrößerung oder Verkleinerung von Fenstern eine besondere Bedeutung. Koordinatenunabhängig heißt nicht nur, daß das Programm auf beliebigen Bildschirmen läuft, sondern auch, daß dem Benutzer gestattet wird, die Dimensionen der Fenster möglichst frei zu verändern und das Programm die Größenveränderung möglichst optimal umsetzt.

Positionen kann man durch die Verwendung von Verhältnissen ausdrücken. So könnte das Hauptfenster der Applikation zum Beispiel 80% der Höhe und 60% der Breite eines Bildschirms einnehmen. Dazu wird die Desktopgröße ermittelt und mit dem entsprechenden Faktor verrechnet. Eine solche Strategie ist bei Applikationen sinnvoll, die einen möglichst großen Anteil des Bildschirms benötigen wie etwa Textverarbeitungen oder Grafikprogramme. In anderen Fällen könnte die Verwendung der Zeichensatzgrößen sinnvoller sein. Hier rechnet man in Anzahl von Zeilen oder Buchstabenbreiten. Dazu ermittelt man die Fontgröße in Punkten. Lediglich bei Abstandsrändern macht es Sinn, in konstanten Punkten zu rechnen. Spätestens, wenn man die Ergebnisse auf einem Drucker, der typischerweise eine deutlich höhere Auflösung besitzt als der Bildschirm, ausgibt, wird sich die Verwendung der Systemwerte bezahlt machen, da man die gleichen Funktionen einfach auf den Drucker umleiten kann.

Unter PM gibt es die Möglichkeit, das Hauptfenster vom System positionieren zu lassen. Dies wird erreicht, indem dem Fensterstil die Konstante FCF_SHELLPOSITION hinzugefügt wird. Damit die vom PM vorgeschlagene Größe tatsächlich verwendet wird, darf beim Darstellen mit der Funktion WinSetWindowPos das Fenster nur aktiviert und sichtbar gemacht werden. Man kann dies gut kontrollieren, indem man die Parameter für die Fensterpositionen mit Nullen besetzt. Der Vorteil dieses Vorgehens gegenüber einer selbstgewählten Fensterposition liegt darin, daß beim mehrfachen Start des Programms nicht alle Hauptfenster auf der gleichen Stelle liegen. Der Benutzer kann dann kaum feststellen, daß er das Programm wirklich mehrfach gestartet hat.

Die Initialisierung des Fensters würde etwa wie folgt aussehen, wenn das System die Position des ersten Erscheinens festlegen soll:

   FensterStil=FCF_SHELLPOSITION | FCF_SIZEBORDER | FCF_TITLEBAR;
   Option = SWP_SHOW | SWP_ACTIVATE;

   . . .

   RahmenHandle = WinCreateStdWindow (HWND_DESKTOP,
            0L, &FensterStil, FENSTERKLASSE,
            "Fenstertext", 0, (HMODULE) 0L,
            ID_MAIN, &ClientHandle);

   WinSetWindowPos(RahmenHandle, HWND_TOP,
            0, 0, 0, 0, Option);

Dem Benutzer sollte die Möglichkeit geschaffen werden, die Größe und Position des Fensters beliebig zu verändern. Dazu benötigt das Fenster einen Verschiebebalken und ein Vergrößerungselement.

Allerdings sollte man sich auch darüber klar sein, daß man das Versprechen, das man dem Benutzer mit dem Vergrößerungselement gibt, auch einhalten sollte. So sollte eine Listbox innerhalb eines Fensters auch wachsen, wenn der Benutzer das Fenster größer macht. Er wird dies vermutlich vergrößern, weil er mehr sehen will. Darauf sollte eine Anwendung aber nicht so reagieren, daß sie dem Benutzer mehr Hintergrund zur Verfügung stellt. Für die Fensterfunktion bedeutet dies, daß beim Ereignis WM_SIZE die Position der Liste anhand der neuen Fensterausdehnung mittels WinSetWindowPos neu gesetzt wird.

Formularfenster

Das Form-Widget ist von besonderem Interesse, da es die koordinatenunabhängige Anordnung von Kontrollelementen ermöglicht und dabei eine erstaunliche Flexibilität vorweist. Das Form-Widget ist zwar nur unter den Widget Sets vonX verfügbar, aber es bietet Anhaltspunkte, wie auch unter den anderen Oberflächen eine koordinatenunabhängige Programmierung angegangen werden könnte. Das Form-Widget zählt zu den Constraint-Widgets. Es ist damit in der Lage, mehrere Widgets aufzunehmen und anzuordnen. Darüber hinaus kann es jedes Kindwidget individuell behandeln und dessen Wünsche in die Gestaltung einfließen lassen. Dazu "vererbt" das Constraintwidget seinen direkten Kindern einige Eigenschaften, die eigentlich zum Rahmen gehören, aber der Einfachheit halber beim Kind eingestellt werden. Diese enthalten Angaben über die gewünschten Nachbarn und die Art der Abstände. Im folgenden Beispiel wird vom Formwidget des Athena Widget Set ausgegangen, das etwas einfacher ausgelegt ist als das Gegenstück unter OSF/Motif. Leider unterscheiden sich die Namen der Eigenschaften und die Möglichkeiten der beiden Formwidgets etwas, aber das prinzipielle Vorgehen ist sehr ähnlich.

Zunächst wird die Anordung der Kinder von links oben zeilenweise nach rechts unten festgelegt. Dies geschieht durch die Reihenfolge ihrer Erzeugung. Dabei wird als Elternwidget immer das Formwidget angegeben. Nun wird bei jedem Widget in der Eigenschaft fromHoriz eingestellt, welches Widget der nächste Nachbar nach links und in fromVert, welches Widget der nächste Nachbar nach oben ist.

In der Abbildung hat das Eingabefeld als linken Nachbarn das Label und als oberen Nachbarn die Menueleiste. Die Liste hat keinen linken Nachbarn. Als oberen Nachbarn könnte sowohl das Label als auch das Eingabefeld eingetragen werden. Es wird nicht unbedingt ein direkt angrenzendes Widget verwendet, sondern es ist das Widget relevant, welches die gewünschte Ausdehnung nach unten hat. Der Abbruch-Knopf hat als linken Nachbarn den Knopf Sichern, auch wenn dieser weit entfernt ist.

Nachdem auf diese Weise die Anordnung festgelegt wurde, wird im nächsten Schritt durch Einstellungen der Ränder das Vergrößerungsverhalten bestimmt. Dazu haben die Widgets vom Formwidget je eine Eigenschaft für jeden Rand erhalten, die sinnigerweise left, right, top und bottom heißen. Sie geben an, wie sich der Rand im Vergrößerungsfall zu welchem Nachbarn verhält. Standardmäßig sind sie auf Rubber (Gummi) eingestellt. Beim Vergrößern des Außenrahmens vergrößern sich so die Zwischenabstände und die Widgets selbst. Um dieses Verhalten zu verändern, kann eine oder mehrere Seiten angekettet (Chain) werden. Dadurch wird der Abstand, der bei der Erzeugung des Formulars vorlag, bei einer Größenänderung des Rahmens nicht mehr verändert. Das heißt, daß der Raumgewinn beim Vergrößern vollständig dem Widget zugute kommt. Wird ein Widget auf der linken Seite nach links und auf der rechten Seite nach rechts verkettet, kann man sich leicht vorstellen, daß das Widget bei Vergrößerung des Rahmens auseinandergezogen wird. Soll dies nicht erfolgen, wird sowohl die linke als auch die rechte Seite mit dem linken Nachbarn verkettet. Damit bleibt die horiziontale Ausdehnung des Widgets konstant. In der Abbildung sind diese Verkettungen angedeutet.

Contraint-Widgets

Die Knöpfe und das Label werden ihre ursprüngliche Ausdehnung nicht verändern. Dagegen ist die Liste nach allen vier Richtungen verankert, so daß jede Vergrößerung des Raumes vollständig der Liste zugeführt wird. Die Menüleiste hat keinen Eintrag nach rechts, so daß die ursprüngliche Gummiverbindung zum rechten Rand erhalten bleibt. Das bedeutet, daß eine Verbreiterung zwischen der Menüleiste und seinem rechten Rand aufgeteilt wird. Beim Eingabefeld ist hier eine Verkettung nach rechts eingesetzt worden, um eine Verbreiterung des Rahmens vollständig dem Eingabefeld zugute kommen zu lassen. Der Nachteil dieses Verfahrens ist, daß auch eine Verkleinerung direkt auf das Eingabefeld einwirkt, so daß das Eingabefeld kleiner werden kann als das Labelfeld. Ist dieses Verhalten unerwünscht, kann die Verkettung des rechten Randes auch nach links erfolgen, um ein immer gleich großes Eingabefeld zu erhalten. Die folgende Funktion erzeugt das in der Abbildung skizzierte Fenster.

void initWidgetBaum(Widget Shell)
{
Widget wAussen, wMenueLeiste, wList, wEingabe, wLabel,
       wSichern, wAbbruch;
Arg arg[10];
Cardinal n;

    n=0;
    XtSetArg(arg[n], XtNresizable, True); n++;
    wAussen = XtCreateManagedWidget(äussen", formWidgetClass,
                          Shell, arg , n);
    n=0;
    XtSetArg(arg[n], XtNleft, XtChainLeft); n++;
    XtSetArg(arg[n], XtNtop, XtChainTop); n++;
    XtSetArg(arg[n], XtNbottom, XtChainTop); n++;
    XtSetArg(arg[n], XtNorientation, XtEhorizontal); n++;
    wMenueLeiste = XtCreateManagedWidget("menueleiste", boxWidgetClass,
                          wAussen, arg , n);
    initMenue(wMenueLeiste);  /* baut den Rest des Menues */
    n=0;
    XtSetArg(arg[n], XtNfromVert , wMenueLeiste); n++;
    XtSetArg(arg[n], XtNright, XtChainLeft); n++;
    XtSetArg(arg[n], XtNleft, XtChainLeft); n++;
    XtSetArg(arg[n], XtNtop, XtChainTop); n++;
    XtSetArg(arg[n], XtNbottom, XtChainTop); n++;
    wLabel = XtCreateManagedWidget("eingabelabel", labelWidgetClass,
                wAussen, arg, n);
    n=0;
    XtSetArg(arg[n], XtNfromVert , wMenueLeiste); n++;
    XtSetArg(arg[n], XtNfromHoriz , wLabel); n++;
    XtSetArg(arg[n], XtNright, XtChainRight); n++;
    XtSetArg(arg[n], XtNleft, XtChainLeft); n++;
    XtSetArg(arg[n], XtNtop, XtChainTop); n++;
    XtSetArg(arg[n], XtNbottom, XtChainTop); n++;
    XtSetArg(arg[n], XtNeditType , XawtextEdit); n++;
    wEingabe = XtCreateManagedWidget("eingabefeld",
                       asciiTextWidgetClass, wAussen, arg, n);
    n=0;
    XtSetArg(arg[n], XtNfromVert , wLabel); n++;
    XtSetArg(arg[n], XtNright, XtChainRight); n++;
    XtSetArg(arg[n], XtNleft, XtChainLeft); n++;
    XtSetArg(arg[n], XtNtop, XtChainTop); n++;
    XtSetArg(arg[n], XtNbottom, XtChainBottom); n++;
    wListe = XtCreateManagedWidget("liste", listWidgetClass,
                          wAussen, arg, n);
    n=0;
    XtSetArg(arg[n], XtNfromVert , wView); n++;
    XtSetArg(arg[n], XtNright, XtChainLeft); n++;
    XtSetArg(arg[n], XtNleft, XtChainLeft); n++;
    XtSetArg(arg[n], XtNtop, XtChainBottom); n++;
    XtSetArg(arg[n], XtNbottom, XtChainBottom); n++;
    wSichern = XtCreateManagedWidget(ßichern", commandWidgetClass,
                          wAussen, arg, n);
    n=0;
    XtSetArg(arg[n], XtNfromVert , wView); n++;
    XtSetArg(arg[n], XtNfromHoriz , wSichern); n++;
    XtSetArg(arg[n], XtNright, XtChainRight); n++;
    XtSetArg(arg[n], XtNleft, XtChainRight); n++;
    XtSetArg(arg[n], XtNtop, XtChainBottom); n++;
    XtSetArg(arg[n], XtNbottom, XtChainBottom); n++;
    XtCreateManagedWidget(äbbruch", commandWidgetClass,
                          wAussen, arg, n);
}

Wie im Listing zu sehen, wird das Setzen der Eigenschaften durch Füllen eines Feldes vom Typ Arg erreicht. Dazu steht die Funktion XtSetArg zur Verfügung. Der zweite Parameter der Funktion beschreibt die zu verändernde Eigenschaft und der dritte den neuen Wert. Das Array wird dem Widget bei seiner Erzeugung mitgegeben.

Die Tatsache, daß die Abstände bei Erzeugung des Formwidgets eingefroren werden, kann zu dem Problem führen, daß sich die Ausgangspositionen der Ränder zu weit links befinden, da sie ja von links nach rechts aufgebaut werden. Dies betrifft zum Beispiel den Abbruchknopf, der entgegen der Grafik nicht am rechten Rand ``kleben'' wird, sondern sich dicht an seinem linken Nachbarn anordnen wird und bei Erzeugung des Rahmens den Abstand zum rechten Rand ermittelt und einfriert. Bereits die geringste Verkleinerung des Rahmens wird nun dazu führen, daß sich der Abbruchknopf unter seinen linken Nachbarn schiebt. Ein ähnliches Problem tritt bei der Ausdehnung der Liste auf. Da diese bei Erzeugung des Widgets noch keine Einträge hat, wird sie nur die minimale Größe anfordern und damit einen sehr großen rechten Rand einfrieren.

Um dies zu verhindern, müssen beim Athena Widget Set auf die Koordinaten der Widgets zugegriffen werden. Dabei werden die Ausgangspositionen bzw. die Breiten der betroffenen Widgets vom Programmierer anhand der Ausdehnung der anderen Widgets vorbesetzt, so daß sie bei ihrer Erzeugung den gewünschten Raum einnehmen. Die einfachere Lösung besteht darin, in den Ressourcedateien Dimensionen vorzubelegen, die die gewünschten Positionierungen bewirken.

Unter OSF/Motif besitzt das Formwidget aus diesem Grund zusätzliche Eigenschaften, die es den Widgets erlauben, die Entfernung zum rechten Rand festzulegen. Zusätzlich bietet es die Möglichkeit, Kindwidgets an festgelegten Positionen zu arretieren. Dazu kann eine virtuelle Skalierung vorgenommen werden. Dies ist beispielsweise hilfreich, wenn man ein Drittel für das Label und zwei Drittel des Raumes für das Eingabefeld vergeben möchte. Dazu wird die Skalierung in der Eigenschaft XmNfractionBase auf drei eingestellt. Das Label erhält die Startposition 0 und die Endposition 1. Das Eingabefeld erhält die Startposition 1 und die Endposition 3. Bei einer Veränderung der Größe wird die Skalierung immer auf die konkret zur Verfügung stehende Ausdehnung angepaßt, so daß die Größenverhältnisse immer gleich bleiben.

Bei PM und MS-Windows werden die Kontrollelemente in der Fensterfunktion angelegt. Kontrollelemente sind aus der Sicht dieser Systeme Sonderfälle von Fenstern. Bei Eintreffen der WM_CREATE-Nachricht werden sie erzeugt, aber noch nicht dargestellt. Da bei Eintreffen der Nachricht das Fenster noch gar nicht auf dem Bildschirm vorliegt, gibt es erstens gar keine Möglichkeit, Ausgaben durchzuführen und es liegen zweitens noch keine Fensterkoordinaten vor, an denen man sich orientieren könnte. Die Darstellung und Positionierung der Kontrollemente wird bei Eintreffen der Nachricht WM_SIZE ausgeführt. In den Parametern der Nachricht findet sich die Größe des Fensterinhalts. Anhand derer kann die Aufteilung des Fensters für die Kontrollelemente vorgenommen werden.

Fensterklassen

Bei MS-Windows und dem Presentation Manager muß jedes Fenster bei seiner Erzeugung einer Fensterklasse zugeordnet werden. Wie bereits beschrieben, ordnet die Klasse dem Fenster eine Fensterfunktion zu, die vom System bei Eintreffen von Ereignissen aufgerufen wird. Daraus ergibt sich, daß alle Fenster einer Klasse auf die eintreffenden Nachrichten mit einer gleichen Funktionalität reagieren.

Der Anwendungsprogrammierer braucht also für Fenster mit gleicher Aufgabenstellung nur eine Fensterfunktion zu schreiben. Fenster mit unterschiedlicher Aufgabenstellung sollten unterschiedliche Fensterfunktionen verwenden. Dazu müssen mehrere Fensterklassen definiert werden. Für eine Applikation, die mit Fenstern für Grafik und Text arbeitet, würde man also jeweils eine Fensterklasse definieren. Bei der Erzeugung eines Fensters würde die jeweils benötigte Klasse als Parameter angegeben.

Fensterklassen und Fensterfunktionen

Es können mehrere Fenster die gleiche Funktion benutzen. Das führt dazu, daß diese Fenster auf die Ereignisse gleich reagieren, sofern der Programmierer nicht anhand des Handles eine Fallunterscheidung vornimmt. Da letzteres die übersicht nicht gerade erhöht, wird man für unterschiedliche Arten von Fenstern auch unterschiedliche Fensterfunktionen schreiben und damit auch verschiedene Fensterklassen einrichten. Gleiche Fensterklassen werden verwendet, wenn man zum Beispiel in mehreren Fenstern gleichartige Dokumente verwaltet. In der Abbildung ist dies am Beispiel der beiden Fensterklassen angedeutet. Die eine Fensterklasse bearbeitet Texte und besitzt dazu eine speziell auf Textverarbeitung zugeschnittene Fensterfunktion. Die andere Fensterklasse enthält Fenster für Grafiken mit einer anderen Fensterfunktion. Beispielsweise werden die Fensterfunktionen einen Mausklick unterschiedlich interpretieren. Während er in der Textklasse zum Neupositionieren des Cursors führen wird, könnte im Grafikfenster damit ein Punkt gesetzt werden.

Äußerlich ist der Unterschied der Fensterklassen nicht unbedingt sichtbar. Alle Fenster sind Nachkommen des Rahmenfensters. In der Abbildung ist dies veranschaulicht.

Anwendung mit Fenstern verschiedener Klassen

Diese Anwendung würde mit mindestens vier Fensterklassen arbeiten. Eine Klasse für das Applikationsfenster, je eine Klasse für Text und Grafik und schließlich eine Fensterklasse für das Steuerungsfenster.

Die Fensterfunktionen bilden Module, die für die zugehörigen Fenster alle Reaktionen auf äußere Ereignisse übernehmen. Es gibt aber Fälle, in denen Fenster auf andere Fenster einwirken müssen. Beispielsweise könnte das Drücken eines Druckknopfes im Steuerungsfenster Auswirkungen auf ein Grafikfenster haben. Zu diesem Zweck ist es möglich, applikationseigene Nachrichten zu definieren, die die Fensterfunktion des Steuerungsfensters an die Adresse eines anderen Fensters versendet. Dazu muß in der Fallunterscheidung der Fensterfunktion ein zusätzlicher Fall für die benutzereigene Nachricht eingetragen werden. Wie bei den Systemnachrichten können auch Benutzernachrichten Parameter übergeben werden. Auf diese Weise ergibt sich fast von selbst der Ansatz für ein objektorientiertes Design der Anwendung. Es ist möglich, völlig abgeschlossene Objekte zu bilden, die miteinander über festgelegte Nachrichten kommunizieren.

Da eine Fensterfunktion alle Nachrichten des zugehörigen Fensters empfängt und im Idealfall allein die Ausgaben in das Fenster kontrolliert, kann durch sie der Charakter eines Fensters fast beliebig verändert werden. Man könnte eine Fensterfunktion definieren, die lediglich einen Text mittig darstellt und eine Benutzernachricht zur Änderung dieses Textes akzeptiert. Das Ergebnis wäre mit einigen Abstrichen einem Label-Widget vergleichbar. Durch Hinzufügung des Empfangs von Mausklicks und einigen Grafikfunktionen wäre daraus auch ein Druckknopf zu gestalten. Tatsächlich sind sämtliche Kontrollelemente von MS-Windows und Presentation Manager so angelegt und damit nichts anderes als Fenster einer vom System vorgegebenen Fensterklasse. Entsprechend gibt es für sie jeweils eine feste Fensterfunktion, die ihre gewünschte Funktionalität realisiert. Aus diesem Grund sind auch alle Kontrollelemente durch das Zusenden von Nachrichten ansprechbar. Dies erklärt auch, warum der Rahmen eines Standardfensters bei der Erzeugung ein ganz gewöhnliches Fensterhandle erhält. Der Rahmen ist nichts anderes als ein spezielles, vom System vordefiniertes Fenster. In manchen Situationen würde man gern die vom System vordefinierten Kontrollelemente für eigene Zwecke anpassen. Dazu wäre eventuell nur eine Kleinigkeit an der Fensterfunktion zu manipulieren. Da aber kein Zugriff auf den Quellcode möglich ist, gibt es hierzu die Möglichkeit der Unterklassenbildung. Soll beispielsweise das Verhalten eines Druckknopfes bei Loslassen der Maustaste verändert werden, erzeugt man hierzu zunächst einen Druckknopf. Anschließend ruft man die Funktion WinSubclassWindow auf.

OrigFensterFkt = WinSubclassWindow(KnopfHandle, MeineFensterFkt);

Als Rückgabewert erhält man die Adresse der systemeigenen Fensterfunktion. Als neuen Parameter übergibt man das Handle des zu manipulierenden Objekts und die Adresse der eigenen Fensterfunktion. Letztere fängt nur die Nachrichten ab, die manipuliert werden sollen und ruft ansonsten die Fensterfunktion des Systems.

Die Verwaltung von Rollbalken

Rollbalken werden verwendet, wenn Fenster wie reale Fenster benutzt werden, bei denen der Betrachter durch einen kleinen Ausschnitt auf ein größeres Objekt schaut. Um einen anderen Teil des Objektes zu sehen, wird der Betrachter in der realen Welt seine Position verändern. Unter grafischen Oberflächen wird dagegen das Objekt hinter dem Fenster mittels Rollbalken solange verschoben, bis man den gewünschten Teil sehen kann. Dabei vermittelt der Rollbalken dem Benutzer zwei Informationen: erstens die Position des Fensters in Bezug zum Objekt und zweitens das Größenverhältnis zwischen Fenster und Objekt. Zu diesem Zweck ist es möglich, den Schieber im Rollbalken zu verschieben und in seiner Größe zu verändern.

Fenster mit Rollbalken

Um die folgenden Aussagen nicht zu abstrakt werden zu lassen, wollen wir von einem Fenster auf einen Text ausgehen, an dem sich ein vertikaler Rollbalken befindet, wie es in der Abbildung zu sehen ist. Zur Berechnung der Rollbalken sind folgende Parameter notwendig:

Textlänge Die Länge des Textes in Zeilen
Textausschnitt Der sichtbare Ausschnitt des Textes in Zeilen
Textposition Die Zeilennummer der ersten sichtbaren Zeile
Rollbalkenlänge Die Länge des Rollbalkens
Schiebergröße Die Größe des Schiebers im Rollbalken
Schieberposition Die Position des Schiebers im Rollbalken

Für die Längen gelten die folgenden Verhältnisse

   Schiebergröße / Rollbalkenlänge} = Textausschnitt / Textlänge

Für die Position gilt:

   Schieberposition / Rollbalkenlänge = Textposition / Textlänge

Um dem Benutzer anzuzeigen, an welcher Stelle des Textes er sich befindet, müssen aus den Textparametern die Rollbalkenparameter errechnet werden.

   Schiebergröße = (Textausschnitt / Textlänge) * Rollbalkenlänge

Da normalerweise mit ganzzahligen Werten gerechnet wird, würde das Ergebnis meist 0 ergeben, da der Textausschnitt bei größeren Texten immer kleiner als die Textlänge ist und eine ganzzahlige Division erfolgt. Aus diesem Grunde muß die Multiplikation vorgezogen werden. Es ergibt sich:

   Schiebergröße = (Textausschnitt * Rollbalkenlänge) / Textlänge

   Schieberposition = (Textposition * Rollbalkenlänge) / Textlänge

Wenn der Benutzer den Rollbalken in Bewegung setzt, muß umgekehrt aus den Rollbalkenparametern die neue Textposition errechnet werden.

   Textposition = (Schieberposition * Textlänge) / Rollbalkenlänge

Bei der Festlegung der Textlänge ist zu überlegen, ob man die Anzahl der im Fenster sichtbaren Zeilen von der Textlänge abzieht. Andernfalls ist der letzte anzuwählende Ausschnitt fast leer, da die oberste Zeile der letzten Textzeile entspricht. Will man dies nicht, muß genau geprüft werden, ob die letzte Zeile noch erscheint. Es ist meist eine Rundung des Restes der Division notwendig.

Soviel zu den grundlegenden Formeln. In der Praxis werden drei Funktionen benötigt. Eine Funktion reagiert auf das Ereignis des Verschiebens. Die Größe des Schiebers muß neu berechnet und gesetzt werden, wenn sich die Textlänge oder der Ausschnitt verändert. Also muß auch das Vergrößern des Fensters eine Schieberkorrektur auslösen. Die dritte Funktion betrifft die Position des Schiebers. Diese wird neu gesetzt, wenn sich die Textposition ändert, also bei Verwendung der Cursortasten, wenn sich die Textlänge ändert oder wenn vorher die Schiebergröße aufgrund von Textveränderungen oder Größenänderungen des Fensters verändert wurde. Bei diesen Berechnungen sind die Fensterdimensionen notwendig. Zur Ermittlung des Textausschnitts muß schließlich berechnet werden, wieviele Zeilen das Fenster aufnehmen kann.

Neben dem Verschieben kann der Rollbalken zum seitenweisen Blättern verwendet werden. Dies wird erreicht, indem der Bereich im Rollbalken unter- oder oberhalb des Schiebers angeklickt wird. Des weiteren befinden sich im Normalfall unten und oben am Schieber kleine Pfeile, die zum zeilenweisen Verschieben verwendet werden. In den meisten Fällen wird ein Programm dem Benutzer die Positionierung im Text auch durch Cursortasten ermöglichen. Der Programmierer muß darauf achten, daß sich der Rollbalken in diesem Falle gleich verhält wie bei der Verwendung der Rollbalkenlemente.

Verhält sich der Rollbalken sehr sprunghaft, kann dies auf das Problem zurückgehen, daß ein Overflow aufgetreten ist. Bei der Berechnung der Schiebergröße wird beispielsweise die Textlänge zunächst mit der Rollbalkenlänge multipliziert. Sei die Rollbalkenlänge beispielsweise 350, so ist bereits bei einer Textlänge von 100 Zeilen mit einem Overflow einer vorzeichenbehafteten 16-Bit-Variablen zu rechnen. Es ist hier für das Zwischenergebnis eine 32-Bit Operation zu erzwingen. Die Ergebnisse der Berechnungen passen dann wieder bequem in eine 16-Bit Variable.

Die Rollbalkenlänge hängt nicht, wie man erwarten könnte, von der tatsächlichen Größe auf dem Bildschirm ab. Bei GEM ist sie konstant 1000, bei den anderen Systemen kann sie vom Programmierer weitgehend frei eingestellt werden. Damit wird erst einmal erreicht, daß die Schieberposition unabhängig von der Fenstergeometrie bestimmt werden kann. Für die Bestimmung der Schiebergröße wird allerdings die Ausschnittgröße benötigt, so daß man nicht umhin kommt, doch die Fensterdimensionen zu ermitteln. Dieser Problematik haben sich Macintosh und MS-Windows dadurch entzogen, daß die Schiebergröße bei ihnen nicht veränderbar ist.

Unter PM wird das Rollbalkenelement wie alle anderen Kontrollelemente als Fenster behandelt, dem man die entsprechenden Einstellungen zusendet. Die Nachrichten lauten:

SBM_SETSCROLLBAR Setze die virtuelle Länge des Rollbalkens
SBM_SETPOS Setze die Position des Schiebers
SBM_SETTHUMBSIZE Setze die Größe des Schiebers
SBM_QUERYPOS Erfrage die Position des Schiebers
SBM_QUERYRANGE Erfrage die virtuelle Länge des Rollbalkens

Beim Empfangen der Nachrichten vom Rollbalken empfängt das Elternfenster in seiner Fensterfunktion eine WM_HSCROLL-Nachricht für den horizontalen und eine WM_VSCROLL-Nachricht für den vertikalen Rollbalken. Dabei wird im zweiten Parameter angegeben, welche Aktionen auf dem Rollbalken ausgeführt wurden.


MRESULT EXPENTRY FensterFunktion (HWND FensterHandle,
                 ULONG Nachricht, MPARAM mp1, MPARAM mp2)
{
HWND RBHandle; /* Rollbalken-Handle */
USHORT pos;

   switch (Nachricht) {
      ...
      case WM_HSCROLL: /* horizontaler Rollbalken */
         /* Bestimme das Handle des Rollbalkens */
         RBHandle = WinWindowFromID(FensterHandle, DID_HORZSCROLL);
         switch(SHORT2FROMMP(mp2) { /* was genau passierte? */
            case SB_SLIDERPOSITION: /* Verschiebung beendet */
               pos = SHORT1FROMMP(mp2);
               /* Applikationseigene Auswertung der Position */
               WinSendMsg(RBHandle,SBM_SETPOS,MPFROMSHORT(pos),NULL);
               break;
            case SB_SLIDERTRACK: /* Verschiebung im Gange */
               /* Analoge Verschiebung des Fensterinhalts */
               break;
            case SB_LINELEFT:  /* eine Position links */
            case SB_LINERIGHT: /* eine Position rechts */
            case SB_PAGELEFT:  /* eine Seite links */
            case SB_PAGERIGHT: /* eine Seite rechts */
            case SB_ENDSCROLL: /* Maustaste losgelassen */
         }
         break;
      case WM_VSCROLL: /* vertikaler Rollbalken */
      ...
         break;
      case WM_PAINT:
    ...
   }
   return WinDefWindowProc (FensterHandle, Nachricht, mp1, mp2);
}

In dem Skelett ist die Bearbeitung von Rollbalkenereignissen angedeutet. Nach dem Empfang der Nachricht WM_HSCROLL wird zunächst das Handle des Rollbalkens ermittelt, um ihm später die Nachricht zum Positionieren zusenden zu können. Anschließend wird untersucht, welche Aktion der Benutzer ausgeführt hat. Die Aktion SB_SLIDERPOS sagt aus, daß der Schieber im Rollbalken an eine bestimmte Stelle geschoben wurde. Aus der ersten Hälfte des zweiten Parameters wird ermittelt, an welcher Stelle der Schieber losgelassen wurde. Im Normalfall wird eine Verschiebung des Bildschirminhalts durchgeführt. Gegebenfalls wird dabei die Position des Schiebers korrigiert. Anschließend wird die Position des Schiebers gesetzt. Das Setzen des Schiebers muß vom Programm aus explizit durchgeführt werden, da sonst der Schieber an seine Ausgangsstellung zurückgleitet. Das gilt auch für GEM und Macintosh.

Die Aktionen vom Typ LINE und PAGE betreffen das Blättern mit Hilfe des Pfeils bzw. des Leerraums über oder unter dem Schieber. Gerade in solch einem Fall kann das Problem auftauchen, daß der Benutzer die Maustaste über dem Pfeilsymbol des Rollbalkens gedrückt hält. Dadurch werden meist schneller Nachrichten ausgelöst, als die Applikation scrollen kann. Das führt dazu, daß der Bildschirm nach dem Loslassen der Maustaste noch einige Zeit weiterscrollt. Um dies zu verhindern, kann man die Anzahl der Aktionen akkumulieren bis keine Nachrichten mehr eintreffen. Zu diesem Zweck erhält man beim Loslassen der Maustaste die Nachricht SB_ENDSCROLL.

Die Aktion SB_SLIDERTRACK wird während des Verschiebens bei jeder Bewegung des Schiebers ausgelöst. Dadurch ist es möglich, den Fensterinhalt analog zum Verschieben scrollen zu lassen. Man erhält diese Nachricht also normalerweise mehrfach, bevor die Aktion SB_SLIDERPOS eintrifft. Ob er den Bildschirm mitscrollen läßt, kann der Programmierer selbst entscheiden. Unter MS-Windows werden zur Steuerung des Rollbalken üblicherweise zwei Funktionen verwendet.

   SetScrollRange(Handle, SB_HORZ, min, max, TRUE);
   SetScrollPosition(Handle, SB_HORZ, pos, TRUE);

SetScrollRange legt die virtuelle Länge des Rollbalkens fest und der Aufruf von SetScrollPosition positioniert den inneren Schieber. Eine Größeneinstellung des Schiebers ist wie gesagt nicht vorgesehen. Wird durch die Einstellung der Bereichsgrenzen min und max das Intervall kleiner oder gleich 1, wird der Rollbalken automatisch unsichtbar und vergrößert damit den Fensterinhalt. Ebenso kann auch das Erfragen des Bereichs bzw. der Position über Funktionien durchgeführt werden.

Eine besonders elegante Lösung bieten die Widgetsets unter X an. Dort gibt es ein Widget, daß mit zwei Rollbalken versehen ist und daß ein weiteres Widget aufnehmen kann. Das heißt, daß der Rollbalken als Elternwidget bei der Erzeugung angegeben wird. In diesem Fall übernimmt das System selbstständig die Kontrolle über den Schieber. Da das unterliegende Widget versuchen wird, seinen Inhalt vollständig darzustellen, reicht es seine Gesamtgröße nach oben. Damit kennt das Rollbalkenwidget alle benötigten Parameter: die maximale Widgetgröße und die eigene Größe als Ausschnittsbereich. Es ermittelt daraus selbstständig Größe und Position des Schiebers. Auch die Veränderung des Schiebers braucht der Programmierer nicht mehr zu überwachen. Das Rollbalkenwidget wird den gewünschten Ausschnitt des Kindwidgets automatisch anzeigen.

Daneben bieten die Widgetsets auch eigenständige Rollbalken an, die direkt programmiert und für eigene Zwecke verwendet werden können. Auch in den oben beschriebenen Konstrukten, in denen ein Widget als Kind von Rollbalken verwendet wird, kann das Programm die Steuerung selbst übernehmen, wenn die Originaleigenschaften unerwünscht sind.


  1. Das Festhalten der Fensterkoordinaten in eigenen Variablen ist bei GEM-Programmierern sehr beliebt. Es darf bezweifelt werden, ob ein eigene Verwaltung effizienter als die Anfrage beim GEM ist. Eine Systemanfrage gewährleistet sicher, daß mit aktuellen Daten gearbeitet wird.
  2. Dieses Vererben ist nicht im Sinne des objektorientierten Vererbens zu sehen.

Inhaltsverzeichnis (C) Copyright 1993-1999 Arnold Willemer