MSDN Magazin > Home > Ausgaben > 2008 > Juni >  GUI-Bibliothek: Einbringen der Einfachheit von ...
GUI-Bibliothek:
Einbringen der Einfachheit von Windows Forms in systemeigene Anwendungen
John Torjo

Themen in diesem Artikel:
  • Das Problem bei der GUI-Programmierung
  • Erstellen von Fensterobjekten
  • Behandeln von Ereignissen und Benachrichtigungen
  • Formulare und Steuerelemente
In diesem Artikel werden die folgenden Technologien verwendet:
Win32-API, C++
Das Problem bei der GUI-Programmierung in C++ ist, dass die meisten Bibliotheken auf einer Ebene arbeiten, die den Programmierer zu stark belastet. Sie verlassen sich auf C-ähnliche Strukturen oder ihre Wrapperklassen blenden nicht genug an Komplexität aus. Auch vereinfachen sie die Ereignisprogrammierung nicht genug, stattdessen sind Sie gezwungen, die zugrunde liegenden WM_-Meldungen zu kennen.
In diesem Artikel wird eGUI++ vorgestellt, eine C++-Bibliothek, die für den Clientprogrammierer geschrieben wurde und die eine grundlegende Schnittstelle für den Umgang mit GUI-Anwendungen bereitstellt. Komplexität wird ausgeblendet und die Ereignisprogrammierung durch vollständiges Ausblenden von Informationen zu WM_-Meldungen enorm vereinfacht. Sie müssen nicht mit einer C-ähnlichen, rohen Struktur arbeiten, sondern arbeiten ausschließlich mit Klassen. Insgesamt ist der eGUI++-Clientcode einfach zu lesen und einfach zu schreiben.
eGUI++ ist nur für Windows® geeignet. Meiner Meinung nach sind plattformübergreifende GUI-Anwendungen nicht realistisch, es sei denn, es handelt sich um eine äußerst einfache Anwendung, ein einfaches Testframework, ein Prototyp oder reine Lehrzwecke. Noch wichtiger ist aber, dass die Vorteile, die das zugrunde liegende Betriebssystem bereitstellt, genutzt werden sollten. Für Windows XP und Windows Vista® sind das ziemlich viele.

Systemeigen und Mobil
Für diejenigen, die nach CLR-Code suchen, existiert bereits verwaltetes C++. Das ist eine hervorragende Plattform, und deshalb gibt es keinen Grund, sie zu verbessern. Der Rest unter Ihnen, der sich nach einer guten Bibliothek sehnt, um systemeigenen Windows Code für Windows 2000 und neuere Betriebssysteme zu erstellen, sollte diesen Artikel weiterlesen. Das Ergebnis kann sich sehen lassen: Die Bibliothek lässt sich einfach verwenden und nutzt die Vorteile des Betriebssystems, für das Sie entwickeln. Microsoft® .NET Framework ist nicht erforderlich. Der von Ihnen geschriebene Code funktioniert wie C++. Außerdem ist der von Ihnen geschriebene Code nicht spezifisch für Visual C++-Compiler. Sie könnten ihn sogar mit g++ (GNU C++-Compiler) 4.1 kompilieren. Wenn Sie die Win32®-API wrappen, gibt es im Grunde nichts, was Sie vom Schreiben von mobilem Code abhält.
Allerdings brauchen Sie für eine nicht triviale GUI eine gute IDE, z. B. Visual Studio® 2005 oder Visual Studio 2008 Express Edition. Ich habe die Bibliothek so optimiert, dass sie in Visual Studio 2005 Express und höher integriert werden kann, um so eine bessere GUI-Erfahrung bereitzustellen. Dabei lag der Schwerpunkt auf der Codevervollständigung. Dadurch wird sichergestellt, dass Ihnen die IDE beim Erstellen einer neuen GUI-Klasse oder beim Erweitern einer vorhandenen Klasse möglichst viel Hilfestellung leistet.
Das Schreiben von GUI-Anwendungen soll richtig Spaß machen. Deshalb waren die Ziele beim Erstellen der eGUI++, das Lesen und Schreiben von GUI-Code möglichst zu vereinfachen. Es wurde zum Beispiel wo immer möglich Codevervollständigung implementiert. Die GUI-Programmierung ist jetzt sicher. (Wenn ein Fehler auftritt, wird er ggf. bereits während der Kompilierung abgefangen. Andernfalls wird zur Laufzeit eine Ausnahme ausgelöst.) eGUI++ unterstützt den Ressourcen-Editor (sie wird in den Ressourcen-Editor von Visual Studio 2005 und neueren Versionen integriert).

Keine windows.h
Das Hauptproblem beim Einbinden der windows.h ist das Fehlerpotenzial. Warum sollten Sie ein Ereignis überwachen, das niemals stattfindet? Stellen Sie sich vor, eine button-Klasse wartet auf ein Tastaturereignis, das sich niemals ereignen wird.
Warum ist windows.h trotzdem erforderlich? Sie sollten Windows Anwendungen in C++ unter Verwendung regulärer C++-Klassen schreiben können. Deshalb brauchen Sie die Interna von windows.h nicht zu kennen: kein WM_LBUTTONDBLCLK, WM_LBUTTONUP und andere ellenlange Ereignisse. Kein LPNMITEMACTIVATE, NMHDR oder andere dubiose C-Strukturen. Außerdem keine C-ähnlichen Aufrufe. Die primäre Aufgabe einer guten GUI-Bibliothek unter C++ sollte die Abstraktion der Win32-API sein, sodass Sie mit Klassen arbeiten können.
Haben Sie genug davon, mit reinen C-Strukturen zu arbeiten, wie mit der in Abbildung 1? Mir geht es jedenfalls so. Daher schreibe ich nun Code wie den folgenden:
wnd<rebar> w = new_(parent);
rebar::item i(rebar::item::color | rebar::item::text);
w->add(i); 
// The old way 
hwndRB = CreateWindowEx(WS_EX_TOOLWINDOW,
  REBARCLASSNAME, NULL,
  WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS|
  WS_CLIPCHILDREN|RBS_VARHEIGHT|
  CCS_NODIVIDER,
  0,0,0,0, hwndOwner, NULL, g_hinst, NULL);
...
rbi.cbSize = sizeof(REBARINFO);  
rbBand.cbSize = sizeof(REBARBANDINFO);
rbBand.fMask  = RBBIM_COLORS | RBBIM_TEXT |
  RBBIM_BACKGROUND;
rbBand.fStyle = RBBS_CHILDEDGE;
Wie Sie sehen, blendet eGUI++ die Komplexität aus: Sie arbeiten nur mit C++-Klassen. Sie müssen keine komplizierte API-Funktionen (wie CreateWindowEx), Konstantennamen (Konstanten der Form WS_*) oder komplexe C-Strukturen wie REBARPARAMINFO im Kopf haben.
eGUI++ richtet sich an Windows 2000 und höher. Standardmäßig richtet es sich an Windows XP SP2. Sie können jedoch auch ein anderes Betriebssystem auswählen, sodass weniger oder mehr Features verfügbar sind. Wenn Sie ein anderes Betriebssystem auswählen möchte, brauchen Sie nur #define EGUI_OS auf eine andere Betriebssystemkonstante zu setzen, bevor Sie einen beliebigen eGUI++-Header einbinden (siehe Abbildung 2).
// code from eGUI++
struct os {
 typedef enum type {
 win_2k,
 win_2k_sp4,
 win_xp,
 win_xp_sp2,
 win_vista
 };
};

#ifndef EGUI_OS
#define EGUI_OS os::win_xp_sp2
#endif
Sie hören sicher gerne, dass es im Code nur wenige #ifdefs gibt und er sich daher viel einfacher lesen lässt. Stattdessen wird Ihnen auffallen, dass einige der Eigenschaften nur für ein bestimmtes Betriebssystem gelten:
property<int,os::win_xp> some_prop;
Wenn Sie für ein früheres Betriebssystem entwickeln und z. B. versuchen, die oben genannte Eigenschaft zu verwenden, wird während der Kompilierung ein Fehler ausgelöst.

Behandeln einzelner Fenster
Im Allgemeinen entscheidet der Programmierer über die Lebensdauer eines Objekts. Aber ein großes Problem bei der GUI-Programmierung ist die Tatsache, dass es einen Unterschied zwischen dem sichtbaren Fenster und dem Fensterobjekt gibt: Der Benutzer entscheidet, wann ein Fenster geschlossen wird. In Ihrem Code könnte nun ein gültiger Zeiger oder ein Verweis auf ein Fensterobjekt existieren, obwohl das Fenster bereits vom Benutzer zerstört wurde. Dies macht es schwierig, eine 1:1-Relation beizubehalten, bei der ein Fenster auf dem Bildschirm einer Objektinstanz und umgekehrt entspricht. Aus diesem Szenario ergibt sich die eindeutige Einschränkung, dass keine lokalen Fensterinstanzen vorliegen dürfen (die im vorhandenen Bereich zerstört werden können):
{
form f(...);
f.show();
...
}
Angenommen das Formular mit dem Namen f wird auf dem Bildschirm angezeigt. Was soll passieren, sobald Sie den Bereich von f beenden? Ihnen stehen zwei Möglichkeiten zur Verfügung: Entweder zerstören Sie das Formular (auf dem Bildschirm), oder Sie belassen das Formular auf dem Bildschirm und zerstören die entsprechende C++-Instanz. Keine der beiden Möglichkeiten ist sinnvoll. Im ersten Fall könnte dies den Benutzer verwirren, der sich fragen wird, wo das Formular geblieben ist. Im zweiten Fall bleibt das Formular auf dem Bildschirm, aber es reagiert nicht mehr auf Ereignisse, weil die entsprechende C++-Instanz nicht mehr vorhanden ist. Außerdem könnte der Benutzer bereits das Formular auf dem Bildschirm geschlossen haben, während f noch im Bereich ist. Daher arbeiten Sie schließlich mit einem Objekt, das nicht mehr auf dem Bildschirm existiert.
Die Lösung: Greifen Sie auf das Fenster immer über einen (indirekten) Zeiger zu. Wenn der Benutzer das Fenster auf dem Bildschirm schließt, wird die entsprechende C++-Instanz als ungültig markiert. Wenn Sie nun versuchen, darauf zuzugreifen, wird eine Ausnahme ausgelöst. Sie können jederzeit herausfinden, ob ein Fenster gültig ist oder nicht, und Sie können das Fenster so zerstören:
wnd<> w = ...;
// is window valid?
if ( is_valid(w) ) w->do_something();
// is window valid?
if ( w) w->do_something();

// destroy the window
delete_(w);
Weil jedem Fenster auf dem Bildschirm eine C++-Instanz zugeordnet ist, arbeiten Sie mit Fenstern mithilfe der Vorlagenklasse wnd<>, die einen freigegebenen Zeiger (Verweiszähler) auf ein Fenster darstellt. Die wnd<>-Klasse hat ein optionales Argument: Den Fenstertyp. Das ist zu erwarten. Standardmäßig ist es window_base. Es kann sich auch um eine Fensterklasse handeln, z. B. text, label, rebar, edit und so weiter. Abbildung 3 zeigt einige Beispiele für die Verwendung von Fensterobjekten und einer Umwandlung zwischen ihnen.
// when constructing a window, you can specify its type
wnd<> w = new_<form>(parent);
w->bg_color( rgb(0,0,0));

// when constructing a window, if you don't specify its type,
// it will guess it, based on who you assign it to
wnd<button> b = new_(w, rect(10,10,200,20) );
b->events.click += &my_func;

// destroying a window
delete_(b);

// casting - if it fails, it throws
wnd<form> f = wnd_cast(w);

// casting - if it fails, returns null
if ( wnd<edit> e = try_wnd_cast(e) )
  e->text = "not nullio";
Beachten Sie, dass Sie ein Fenster mithilfe der Funktion new_ erstellen und es mithilfe der Funktion delete_ löschen (und wieder ist es in der Regel der Benutzer, der ein Fenster schließt). Wenn zusätzlich ein Fenster vom Typ X vorliegt (window_base ist der Standard) und Sie wissen möchten, ob es auch vom Typ Y ist, können Sie die Umwandlung verwenden. Die Umwandlung ist immer explizit. Es gibt zwei Arten von Umwandlungen: wnd_cast löst, wenn es fehlschlägt, eine Ausnahme aus, und try_wnd_cast gibt bei einem Fehler ein Fenster vom Typ Null zurück.
In gewissen Fällen soll bei der Entwicklung einer eGUI++-Klasse neben der Ableitung von der Basisklasse ein zusätzliches Verhalten geerbt werden, z. B. resizability, skinability und so weiter. In diesem Fall können Sie mehrere wiederverwendbare Verhaltensklassen erstellen und zusätzlich von ihnen ableiten.

Einfacher Code
Es ist wirklich erfreulich, dass sich GUI-Code so einfach schreiben und lesen lässt. Es wurde tief in die Trickkiste gegriffen, damit die Codevervollständigung möglichst hilfreich ist. Beim Umgang mit einer großen GUI-Bibliothek gibt es immer wieder Dinge, die unter den Tisch fallen können: Namen von Eigenschaften und Ereignissen, Kennzeichen und so weiter. Dies alles wurde hier berücksichtigt. Für die Dokumentation wurde doxygen verwendet, ein hervorragendes Programm. Je länger ich es verwende, desto mehr sagt es mir zu.
Das Durchsuchen der Dokumentation ist ebenfalls äußerst einfach. Sie brauchen für den Namen einer Eigenschaft lediglich w-> einzugeben, und es werden, wie Abbildung 4 zeigt, die Methoden und Eigenschaften angezeigt (die Eigenschaften werden als Membervariablen angezeigt und lassen sich so ganz einfach unterscheiden). Die Namen von Ereignissen, die eine Klasse behandelt, sind leicht zu merken: Sie brauchen nur „class_name::ev::“ einzugeben, und nach dem Bereichsoperator springt die Codevervollständigung ein und zeigt die Ereignisse an. Aber erst beim Umgang mit Kennzeichen glänzt eGUI++ richtig. Sie können für jede Eigenschaft, die aus einer Kombination von Kennzeichen bestehen kann, die verfügbaren Optionen eines Kennzeichens herausfinden, indem Sie der Eigenschaft des Kennzeichens lediglich „.“ hinzufügen. Auch hier kommt die Codevervollständigung wieder zum Tragen. Als Bonus wurde das Überladen von Operatoren hinzugefügt. Daher ist dieser Code korrekt:
w->text = "hello";
w->text += " world";
w->style |= w->style.tiled;
Abbildung 4 Codevervollständigung
Nur für den Fall, dass Sie die Liste der Ihnen zur Verfügung stehenden Steuerelemente vergessen haben, wurde extra dafür ein Namespace mit dem Namen „egui::ctrl“ hinzugefügt.

Steuerelemente im Vergleich zu Formularen
Wenn Sie in der Vergangenheit mit Win32-GUI-Programmierung gearbeitet haben, sind Ihnen Dialoge vertraut. Sie kennen auch die Tatsache, dass sich das Erstellen eines Dialogs mit API (::CreateDialog) sehr stark von der Erstellung eines Fensters (::CreateWindow[Ex]) unterscheidet. Als Programmierer sollten Sie sich nicht an die beiden komplizierten Funktionen mit ihren ziemlich unterschiedlichen Merkmalen erinnern müssen. Beide sind Fenstertypen. eGUI++ verfügt lediglich über eine Möglichkeit, ein Fenster zu erstellen: Die new_ function.
Im Hinblick auf die unterschiedlichen Arten von Fenstern ist der Name „Formular“ anschaulicher als der Name „Dialog“. Er beschreibt ein Fenster, das andere Steuerelemente anzeigt, die die enthaltenen Daten steuern. Erlaubt sind beide Namen, aber ich bevorzuge Formular. Im Code sieht dies folgendermaßen aus:
typedef form dialog;
Im Prinzip gibt es nur zwei Arten von Fenstern: Steuerelemente und Formulare. Ein Steuerelement ist ein Fenster, das eine Reihe von Daten anzeigt, die der Benutzer möglicherweise ändern darf. Jede Steuerelementklasse leitet sich von der Klasse „control“ ab. Ein Formular ist ein Fenster, das eine oder mehrere Steuerelemente und etwas seiner eigenen Logik enthält (zum Beispiel die Logik, die die Manipulation gewisser Daten ermöglicht).
Die verfügbare Funktionalität jedes Fenstertyps ändert sich je nach den voraussichtlichen Funktionen des Fensters. Zum Beispiel ermöglicht das Formular im Gegensatz zum Steuerelement die Aufzählung untergeordneter Steuerelemente. Auf diese Weise reduziert sich die Fehleranfälligkeit Ihres Codes. Auch müssen Sie sehr selten Steuerelemente erstellen. Meist befinden sie sich bereits im Formular, weil Sie sie dort mithilfe des Ressourcen-Editors abgelegt haben.
Es gibt zwei Arten von Formularen: Modale Dialoge und Meldungen. Sie können einen modalen Dialog erstellen, indem Sie bei der Erstellung des Formulars lediglich „form::style::modal“ hinzufügen. Sie können eine Meldung erstellen, indem Sie die Funktion „msg_box<>“ verwenden und die Schaltflächen als Vorlagenargumente angeben:
if ( msg_box<mb::ok | mb::cancel>("q") == mb::ok)
    std::cout << "ok pressed";
Außerdem weiß „msg_box<>“ zur Kompilierzeit, ob eine Schaltflächenkombination funktioniert oder nicht.
// ok
   msg_box<mb::yes | mb::no>("q");
   // compile-time error
   msg_box<mb::ok | mb::yes>("q");

Formularprogrammierung
Ein Formular ist das, was in der Win32-API als Dialog bezeichnet wird. Für Windows Forms wurde bewiesen, dass Formularprogrammierung eine erfolgreiche Strategie ist. Auf jedem Formular befinden sich einige Steuerelemente, und jedes Formular löst eine einzelne Aufgabe. Sie können anstelle der veralteten und komplizierten einfachen Dokumentschnittstelle (Single Document Interface. SDI) oder der mehrfachen Dokumentschnittstelle (Multiple Document Interface, MDI) Registerkarten verwenden, die Steuerelemente oder andere Formulare enthalten können. Deshalb werden Ihnen keine CFrameWnd, CMDIChildWnd oder ähnliches begegnen. Diese sind nicht erforderlich. Wenn Sie in einem Formular mehrere Formulare hosten müssen, verwenden Sie einfach die tab_form-Klasse. Sie ermöglicht das Hinzufügen untergeordneter Formulare, jedes auf seiner eigenen Registerkarte.

Behandeln von Formularen
Assistenten liegen mir wirklich nicht, aber ich bin der Meinung, dass Programmieraufgaben durch sie gelegentlich erleichtert werden. In diesem Fall wurde für das Erstellen eines Formulars ein Neue Klasse-Assistent erstellt. Wählen Sie in der Class-Ansicht „Add Class“ (Klasse hinzufügen) und unter Kategorien „eGUI“ aus. Wählen Sie auf der linken Seite „eGUI Form“ (eGUI-Formular) aus, und klicken Sie auf „Add“ (Hinzufügen). Legen Sie abschließend den Klassennamen fest (Abbildung 5). Der Assistent erstellt eine Headerdatei mit dem Namen „<dlgname>.h“, eine Quelldatei mit dem Namen „<dlgname>.cpp“ und eine zusätzliche Headerdatei mit dem Namen „<dlgname>_form_resource.h“, die die eGUI++ intern verwalten.
Abbildung 5 Klasse hinzufügen (Klicken Sie auf das Bild, um es zu vergrößern)
Die letzte Headerdatei enthält alle Namen der Steuerelemente, die im Formular verwendet werden. Deshalb müssen Sie keine zusätzlichen Steuerungsvariablen erstellen und Datenaustausch verwenden, wie unter MFC sonst üblich. Sie verwenden die Steuerelemente direkt. Angenommen es liegt Ihnen ein Anmeldedialog vor, der zwei Bearbeitungsfelder (Benutzername und Kennwort) und zwei Schaltflächen enthält (OK und Abbrechen), siehe Abbildung 6.
Abbildung 6 Bearbeitungsfelder und Schaltflächen
Die folgenden Dateien werden generiert:
// login.h
#pragma once
#include "login_form_resource.h"
struct login : form, 
 private form_resource::login {};

// login.cpp
#include "stdafx.h"
#include "login.h"
Der Code ist wirklich ziemlich einfach. Es gibt keinen Assistenten-ähnlichen Code wie „enum {IDD = ... }“ oder Meldungszuordnungen. Sie müssen keinen benutzerdefinierten Konstruktor bereitstellen, wenn Sie das nicht wollen. Der Standardkonstruktor funktioniert ohne Probleme.
Die login-Klasse leitet sich privat von form_resource::login ab, die in login_form_resource.h implementiert wird (diese Datei wird von der eGUI++-Bibliothek verwaltet). Die form_resource::login-Klasse enthält Informationen über die Steuerelemente des Formulars (Name und Typ sowie die Möglichkeit, Benachrichtigungen von den Steuerelementen abzufangen). Sie können auswählen, ob Sie die Art des Zugriffs für die Ableitung ändern wollen, obwohl dies nicht empfehlenswert ist. Die Formulare sind genau wie die Mitgliederdaten der Klasse meistens privat. Die Steuerelemente von Formularen sollten in der Regel privat sein.
Jetzt sieht das generierte form_resource::login ungefähr so aus:
// login_form_resource.h
#pragma once
struct form_resource::login {
 // ... (code to allow 
 // handling of notifications)
 wnd<edit> username;
 wnd<edit> passw;
 wnd<button> ok, cancel;
};
Das ermöglicht Ihnen ein problemloses Ändern der Steuerelemente des Formulars. Angenommen Sie wollen sicherstellen, dass das Kennwort „secretword“ lautet:
void login::on_button_click(ev::button_click &, ok_) {
 if ( passw->text == "secretword")
 { pass_ok = true; visible = false; }
}
Wie Sie sehen, wird genau wie in Visual Basic® ein Formular ausgeblendet, indem seine visible-Eigenschaft auf „False“ festgelegt wird.

Weg mit den alten IDs
Sie haben wahrscheinlich schon früher mit dem Ressourcen-Editor gearbeitet. Vermutlich sind Ihnen dabei die vielen Arten von Ressourcenpräfixen begegnet, z. B. ID_, IDD_, IDC_, IDR_, IDS_ und so weiter. Präfixe eignen sich gut für den Ressourcen-Editor. Im Code sind es jedoch nur zusätzliche Informationen, die Sie weder kennen noch interessieren müssen. In einer eGUI++-Anwendung müssen Sie die Präfixe nicht mehr im Kopf haben, weil sie nicht mehr zum Einsatz kommen.
Zum Beispiel sind die oben genannten Namen (Benutzername, Kennwort, OK, Abbrechen) Verknüpfungen des Ressourcen-Editors. Die eGUI++-Bibliothek entfernt automatisch ihre ID*-Präfixe. Die ursprünglichen Namen wären IDC_username, IDC_passw, IDOK und IDCANCEL.

Ereignisse und Benachrichtigungen
Wie bereits erwähnt, müssen Sie keine WM_-Meldungen auswendig wissen. Allerdings ist die Behandlung von Ereignissen nicht einfach. Es gibt eine große Menge von Ereignissen, und Sie brauchen eine einfache Möglichkeit, um herauszufinden, auf welche Ereignisse Sie (möglichst problemlos) reagieren können. Sie brauchen einfache Möglichkeiten, um von Ereignissen benachrichtigt zu werden, die sich auf den Steuerelementen Ihres Formulars ereignen, um Steuerelemente erweitern zu können und um Ihre eigenen Ereignisse hinzufügen zu können.
Jede Fensterklasse (Steuerelement oder Formular) kann Ereignisse generieren. Für jede gibt es einen Ereignishandler, der alle Ereignisse abfängt. Für jedes Ereignis wird eine Funktion definiert, die dieses Ereignis behandelt. Diese Funktion ist virtuell und ihre Implementierung hat keine Auswirkungen. Jede Ereignishandlerfunktion verfügt über ein Argument: die Ereignisdaten.
Für vorhandene Steuerelemente trägt die entsprechende Ereignisklasse den Namen „handle_events::control_name“. Jede existierende eGUI++-Fensterklasse wnd_name leitet sich bereits von handle_events::wnd_name ab. Wenn Sie eine vorhandene Fensterklasse erweitern, können Sie die entsprechenden Ereignisse jederzeit behandeln. (Der Einfachheit halber beginnen alle Ereignishandler mit „on_“.) Beispiel:
struct my_btn : button {
 void on_char(ev::char& e) {
 cout << "typed " << e.ch;
 }
};
Diejenigen unter Ihnen, die bereits mit anderen GUI-Bibliotheken gearbeitet haben, wissen, dass die Dinge nicht immer so einfach sind, wie sie zunächst aussehen. Vorhandene Steuerelemente senden keine Ereignisse, sondern Benachrichtigungen. Die Benachrichtigungen entsprechen Meldungen vom Typ WM_COMMAND/WM_NOTIFY, und sie werden an das übergeordnete Element des Steuerelements gesendet, nicht an das Steuerelement selbst. Auf den ersten Blick scheint das einleuchtend zu sein: Das übergeordnete Element des Steuerelements (des Formulars) muss benachrichtigt werden. Dadurch wird jedoch das Erweitern von Steuerelementklassen relativ schwierig. Was ist, wenn Sie eine Struktur erzeugen wollen, die das aktuelle Dateisystem visualisiert? Sie müssten Ereignisse abfangen, z. B. Elementerweiterung (TVN_ITEMEXPANDING), die an das übergeordnete Element des Steuerelements gesendet werden. Dann brauchen Sie eine Möglichkeit, um die Benachrichtigung an das Steuerelement darunter zu übergeben.
Bei eGUI++ sind Benachrichtigungen Ereignisse. Deshalb werden sie zunächst immer an das Steuerelement gesendet und dann an das übergeordnete Element des Steuerelements. Wenn Sie eine Steuerelementklasse über Vererbung erweitern, wird jede Benachrichtigung in ein anderes Ereignis umgewandelt. Wenn Sie zum Beispiel ein Listensteuerelement erstellen wollen, das anstelle eines Bearbeitungssteuerelements ein Kombinationsfeld anzeigt, wenn der Benutzer die erste Spalte bearbeitet, würde der Code so aussehen:
struct list_with_combo : list {
 ...
 void on_begin_label_edit(
  ev::begin_label_edit & e) {
 e.allow_default = false;
 combo->rect(...);
 combo->visible = true;
 }
 wnd<combo_box> combo;
};
Sie können ein Ereignis behandeln, indem Sie eine Ereignishandlerfunktion ungefähr so überladen:
struct my_btn : button {
 void on_char(ev::char& e);
};
Hier reagieren Sie auf das Ereignis „character pressed“ oder, für die Freunde der Win32-API, auf die Meldung WM_CHAR.
Denken Sie daran, dass das Ereignisargument für die on_my_event-Ereignishandlerfunktion immer vom Typ ev::my_event ist. Alle Ereignisse, die Ihre Klasse behandeln kann, sind ev:: struct. Die Codevervollständigung zeigt Ihnen durch die einfache Eingabe von ev:: alle Ereignisse an, die Ihre Klasse behandeln kann (siehe Abbildung 7). Die einfachste Möglichkeit zum Feststellen der Ereignisinformationen besteht darin, „e.“ einzugeben und die Codevervollständigung alle Daten anzeigen zu lassen, die sich auf das Ereignis beziehen (siehe Abbildung 8).
Abbildung 7 Codevervollständigung zeigt die Ereignisse an
Abbildung 8 Abrufen der Ereignisinformationen
Sie können die Dokumentation nach den Ereignissen eines Steuerelements durchsuchen: Sie brauchen lediglich das Steuerelement auszuwählen, dann seine ev-Klasse, und alle Ereignisse werden angezeigt. Die Bibliothek kann das gleiche Ereignis an mehrere Ereignishandler senden (zum Beispiel werden Benachrichtigungen an das Steuerelement und dann an das übergeordnete Element des Steuerelements gesendet).
Alle Ereignisse besitzen die .sender.-Eigenschaft. Dies ist das Steuerelement, das das Ereignis gesendet hat (nützlich für Benachrichtigungen und besonders, um zu wissen, wer die Benachrichtigung gesendet hat). Alle Ereignisse besitzen die .handled-Eigenschaft, die zwei Werte enthalten kann: handled_partially (Standard) und handled_fully. Wenn Sie diese Eigenschaft auf handled_fully setzen, können Sie die Verarbeitung von Ereignissen beenden: Selbst wenn es mehr Ereignishandler gibt, werden sie nicht aufgerufen. Wenn Sie zum Beispiel die edit-Klasse erweitert haben und nicht wollen, dass das übergeordnete Element bei Textänderungen benachrichtigt wird, schreiben Sie den folgenden Code:
struct independent_edit : edit {
                      void on_change(ev::change &e) {
                      e.handled = handled_fully;
                      }
                     };
Die Erweiterung von Steuerelementen ist einfach, wie oben bereits gezeigt wurde. Jedoch sollte der Umgang mit Benachrichtigungen in Formularen genauso einfach sein. Sie müssen bei der Behandlung einer Benachrichtigung wissen, wer sie gesendet hat (e.sender). Aber noch wichtiger ist, dass Sie auf eine Benachrichtigung eines bestimmten Steuerelements reagieren können. Deshalb erhält die Ereignishandlerfunktion ein zusätzliches Argument: den Namen des Steuerelements, gefolgt von einem Unterstrich (_). Wenn Sie zum Beispiel herausfinden wollen, was der Benutzer in das Bearbeitungsfeld „Benutzername“ eintippt, führen Sie diesen Code aus:
void login::on_change(
 edit::ev::change &e, username_) {
 cout << "name=" << e.sender->text;
}
Angenommen Sie möchten eine Währungsumrechnung zwischen US-Dollar und Euro vornehmen. Wenn Sie einen Wert in das Feld EUR eingeben, aktualisiert sich das Feld USD. Wenn Sie einen Wert in das Feld USD eingeben, aktualisiert sich das Feld EUR, wie in Abbildung 9 gezeigt. Hier ist der entsprechende Code:
struct convert : form, form_resource::convert {
 double rate; 
 convert() : rate(1.5) {}
 int mul_str(const string& a, double b) { ... }
 void on_change(edit::ev::change&, eur_) {
 usd->text = mul_str ( eur->text, rate); }
 void on_change(edit::ev::change&, usd_) {
 eur->text = mul_str ( usd->text, 1/rate); }
};
Abbildung 9 Währungsrechner
Der Code erklärt sich praktisch von selbst: mul_str multipliziert eine Double mit einer Zeichenfolge, indem die Zeichenfolge in eine Double konvertiert und anschließend mit dem Umrechnungskurs multipliziert wird.
Es war ziemlich viel Arbeit erforderlich, damit die Ereignisse so wie oben behandelt werden können. Angenommen es liegt Ihnen ein Formular mit drei Bearbeitungsfeldern vor. Jedes Bearbeitungsfeld generiert einen bestimmten Satz an Ereignissen. Für jedes dieser Ereignisse (zum Beispiel on_change) ließ sich entweder eine überschreibbare Funktion für jedes der Steuerelemente generieren
void on_change(edit::ev::change& e, ctrlname_);
oder eine überschreibbare Funktion generieren:
void on_change(edit::ev::change& e);
Ich bevorzuge die erste Lösung. Der Clientcode ist erheblich einfacher (und dem Visual Basic Ansatz sehr viel ähnlicher). Sie können leicht erkennen, was behandelt wird (im Gegensatz zur zweiten Lösung, bei der Sie innerhalb der Implementierung des Ereignisses manuell abfragen müssten, welches Steuerelement das Ereignis mithilfe von e.sender generiert hat).
Deshalb wurde hier die erste Lösung implementiert. Intern aber steckt viel Arbeit dahinter. eGUI++ überwacht den Ressourcen-Editor. Wenn ein neues Steuerelement hinzugefügt oder ein Steuerelement umbenannt wird, werden alle Dateien vom Typ <dlgname>_form_resource.h aktualisiert. Beachten Sie, dass Sie für jede der Dateien vom Typ <dlgname>_form_resource.h in der Klasse form_resource::<dlgname> alle Benachrichtigungen vorhandener Steuerelemente überschreiben müssen und für jede dieser überschriebenen Benachrichtigungen die zu sendenden Steuerelemente finden müssen. Der nächste Schritt besteht darin, eine Implementierung zu erzeugen, die für jedes Steuerelement an eine andere überschreibbare Funktion weitergeleitet wird. Abbildung 10 zeigt als Beispiel den Code für ein Anmeldeformular mit zwei Bearbeitungsfeldern und zwei Schaltflächen.
struct form_resource::login {
  wnd<edit> name;
  wnd<edit> passw;
  wnd<button> ok, cancel;

  typedef ... ok_;
  typedef ... cancel_;
  typedef ... name_;
  typedef ... passw_;

  virtual void on_change(edit::ev::change& e, name__) {}
  virtual void on_change(edit::ev::change& e, passw__) {}

  virtual void on_change(edit::ev::change& e) {
    if ( e.sender == name) on_change(e, name__());
    else if ( e.sender == passw) on_change(e, passw__());
  }
  // ... same for other edit notifications

  virtual void on_click(button::ev::click & e, ok__) {}
  virtual void on_click(button::ev::click & e, cancel__) {}
  virtual void on_click(button::ev::click & e) {
    if ( e.sender == ok) on_click(e, ok__());
    else if ( e.sender == cancel) on_click(e, cancel__() );
  }
  // ... same for other button notifications
};
Sie können schließlich auch Ihre eigenen Ereignisse durch Ableiten von new_event<> erzeugen. Ob Sie vorhandene Ereignisse oder Ihre eigenen Ereignisse senden, der Vorgang ist der gleiche: Sie verwenden die send_event-Funktion:
struct hover : new_event<hover> {
 int x,y; // position
 hover(int x,int y) : x(x),y(y) {}
};

w->send_event( hover(x,y) );
Die Bibliothek ist threadsicher. Nur als Randbemerkung: Jedes Fenster besitzt eine m_cs-Mutexvariable (im Grunde ist handelt es sich dabei um eine CRITICAL_SECTION), die sicherstellen soll, dass jeder Methodenzugriff threadsicher ist. Wenn Sie eine Fensterklasse erweitern, können Sie die m_cs Variable wiederverwenden oder ganz nach Wunsch selbst erstellen.

Menüs, Tastenkombinationen und mehr
Wenn Sie bereits früher an der GUI programmiert haben, dann wissen Sie, dass durch Drücken eines Menübefehls und Drücken einer Taste ein WM_COMMAND ausgelöst wurde. Wenn Sie infolgedessen ein WM_COMMAND empfangen, ist es schwierig herauszufinden, ob dieses Ereignis von einem Steuerelement oder von einem Menü (oder in Wirklichkeit von einer Tastenkombination) stammt. eGUI++ löst den ersten Teil des Problems, indem das Menü direkt auf dem Formular (Dialog) platziert wird. Wenn ein Befehl, der an ein Formular gesendet wurde, nicht von einer Schaltfläche gesendet wurde, kommt er von einem Menü.
Damit wären die Menübefehle klargestellt. Werfen wir jetzt einen Blick auf die Tastenkombinationen. Das Problem bei den Tastenkombinationen ist, dass eine Taste jederzeit (zum Beispiel innerhalb eines Bearbeitungsfelds) gedrückt werden kann. Tastenkombinationen (Beschleuniger) werden zuerst an das unmittelbare Fenster gesendet, dann zum Formular, das das Fenster hostet, dann zum übergeordneten Element des Formulars und dann durch die Hierarchie nach oben, bis das oberste Fenster erreicht wird. Wenn zum ersten Mal ein Ereignishandler für diese Tastenkombination auftaucht, wird die Verarbeitung angehalten (eine gegebene Tastenkombination wird nicht von zwei oder mehr Fenstern verarbeitet).
Es bleiben die Symbolleisten: Sie gehen mit Menüs und Tastenkombinationen Hand in Hand. Wird eine Symbolleistenschaltfläche gedrückt, wird das Ereignis in einen Menübefehl übersetzt und direkt an das Formular weitergeleitet, das es hostet. Dabei ist egal, ob der Befehl von einem Menü, einer Tastenkombination oder einer Symbolleistenschaltfläche stammt.
Angenommen Sie implementieren ein Formular, das einen Menübefehl etwa so behandelt:
void on_menu_command( ev::menu&, 
 menu::some_menu_id) { ... }
Um zwei Menübefehle, new_file und open_file zu behandeln, werden Sie diese Handler erstellen:
void on_menu_command( ev::menu&,
    menu::new_file) { ... }
   void on_menu_command( ev::menu&,
    menu::open_file) { ... }

Registersteuerelemente und Formulare
Registerkarten sind ein sehr beliebtes GUI-Paradigma. Das Registersteuerelement wurde so erweitert, dass die tab_type-Eigenschaft die Werte „normal“ oder „one_dialog_per_tab“ enthalten kann (in diesem Fall hostet das Steuerelement andere Formulare). Im zweiten Fall können Sie neue Formulare wie dieses hinzufügen:
tab->add_form<form_type>( new_([args]) );
Wenn Sie das bereits besprochene Anmeldeformular hinzufügen wollen, schreiben Sie den folgenden Code:
tab->add_form<login>( new_() );
Nachdem mindestens ein Formular hinzugefügt wurde, kann nun die Anzahl der Registerkarten angegeben werden, die dieses Registerkartenformular enthalten soll:
tab->count = 5;
Hier wurden fünf Registerkarten angegeben. Hierdurch wird das zuletzt hinzugefügte Formular ausgewählt und so oft wie nötig dupliziert. Wenn, wie bereits angenommen, nur eine Registerkarte existiert, wird das Formular auf der ersten Registerkarte weitere vier Mal geklont. Das ist richtig. Sie können jedes vorhandene Fenster klonen!
Bis jetzt wurde die Handhabung von Ereignissen durch intrusive Aktionen demonstriert. Mit anderen Worten, Sie erweitern eine Fensterklasse und reagieren schließlich auf die entsprechenden Ereignisse (oder Sie reagieren beim Implementieren eines Formulars auf die entsprechenden Meldungen). Manchmal aber müssen Sie ein Verhalten implementieren, das für mehrere Fenster gilt (die kaum miteinander in Verbindung stehen).
Nehmen Sie zum Beispiel resizeability und skinability. Sie können auf intrusive Art und Weise implementiert werden (durch Erstellen einer Reihe von Klassen, die das Verhalten implementieren und Ableiten der GUI-Klassen von ihnen). Dadurch wird jedoch der Code komplizierter, und er ist nicht immer brauchbar (nehmen Sie zum Beispiel skinability). Wenn das Verhalten nicht intrusiv implementiert wird, kann es in anderen Anwendungen erneut verwendet und sogar problemlos ausgeschaltet werden.
Erstellen Sie zum Beispiel eine nicht intrusive Ereignishandlerklasse, erstellen daraus eine Instanz, und registrieren Sie sie. Wird ein neues Fenster erstellt, wird Ihre Handlerinstanz benachrichtigt, und Sie können auswählen, ob Sie diese Instanz überwachen wollen. Wenn Sie sie überwachen wollen, müssen Sie manuell festlegen, welche Ereignisse überwacht werden sollen, etwa so:
// monitor button clicks
struct btn_handler : non_intrusive_handler {
 void on_new_window_create(wnd<> w) {
 if ( wnd<button> b = try_cast(w)) {
  b->events.on_click += mem_fn(&on_click,this);
 }
 }
 void on_click(button::ev::click&) { ... }
};
Die Registrierung des Ereignishandlers ist einfach. Führen Sie einfach den folgenden Code aus:
btn_handler bh;
window_base::add_non_intrusive_handler(bh);

Resizeability kann je nach Anwendung auf viele Arten implementiert werden. Zum Beispiel kann das Ereignis on_size in jedem Formular überschrieben und die Positionen der Steuerelemente auf Basis der neuen Formulargröße (eine schlechte Idee, viel Arbeit) aktualisiert werden. Weiterhin können Sie in jedem Formular Relationen zwischen den Steuerelementen erstellen, z. B. „a.x = b.x + b.width + 4;“ (das ist sehr flexibel, erfordert aber auch viel Arbeit).
Alternativ können Sie in jedem Formular die Steuerelemente für jede Achse sowohl als vergrößerbar als auch als verschiebbar markieren. Wurde ein Steuerelement als vergrößerbar für eine bestimmte Achse markiert, und die Größe des Formulars ändert sich, aktualisiert sich die Größe des Steuerelements. Wenn ein Steuerelement als verschiebbar für eine bestimmte Achse markiert wurde und sich die Größe des Formulars ändert, wird dieses Steuerelement verschoben. Dies sollte für die meisten Anwendungen ausreichen. Diese Idee wurde von CResizeWindow aus WTL ausgeliehen und mithilfe eines nicht intrusiven Handlers implementiert. Angenommen es liegt ein Dialog wie in Abbildung 11 vor. Wenn Sie möchten, dass das Ergebnis nach einer Größenänderung wie in Abbildung 12 aussehen soll, verwenden Sie den folgenden Code:
resize(name, axis::x, sizeable);
resize(desc, axis::x | axis::y, sizeable);
resize(ok, axis::x | axis::y, moveable);
resize(cancel, axis::x | axis::y, moveable);
Abbildung 11 Dialog
Abbildung 12 Dialog mit geänderter Größe
Jeder GUI-Vorgang, der fehlschlägt, löst eine Ausnahme aus. Auf diese Weise erfahren Sie, dass ein Fehler aufgetreten ist. Im Debugmodus wird eine fehlgeschlagene Assertion generiert, und das Programm unterbricht den Debugmodus. Das ist erheblich besser, als wenn der Fehler still ignoriert wird, während gleichzeitig der Fehler (visuell) erkannt und nach ihm gesucht wird.

Integration in Visual Studio
Visual Studio ist eine großartige IDE, und einer der Hauptvorteile sind die Erweiterungsmöglichkeiten. eGUI++ profitiert hiervon und wird deshalb mit einem Add-In geliefert, das einen Assistenten für neue Formularklassen enthält. Weiterhin ist eine Visual Basic-ähnliche Leiste vorhanden, über die Sie Steuerelementbenachrichtigungen in Formularen handhaben können. Dazu brauchen Sie nur ein Steuerelement auszuwählen. Sie sehen dann eine Liste mit den Benachrichtigungen, die dieses Steuerelement generieren kann. Beachten Sie, dass die bereits behandelten Benachrichtigungen fett hervorgehoben werden. Wenn Sie auf ein Ereignis klicken, wird ein Handler hinzugefügt, wenn er nicht bereits vorhanden ist. Ein Beispiel dafür zeigt Abbildung 13.
Abbildung 13 Handhabung von Steuerelementbenachrichtigungen in Formularen (Klicken Sie auf das Bild, um es zu vergrößern)
Intern überwacht eGUI++ den Ressourcen-Editor, damit er die Dateien vom Typ _form_resource.h nach einer eventuellen Änderung ggf. aktualisiert. Außerdem arbeitet er mit Codevervollständigung, wie bereits ausführlich beschrieben.

Implementation des Verhaltens
Nach Erstellen der GUI muss nun im nächsten Schritt das Verhalten implementiert und die Datenbindung ermöglicht werden. Viele Formulare sind nur für das Sammeln von Daten von Bedeutung. Zunächst einmal können Sie eine generische Formularklasse implementieren, die während der Erstellung die zu manipulierenden Daten erhält und sie an die Steuerelemente des Formulars bindet. Dann können Sie einen Satz an Regeln für die Validierung der Daten angeben. Wenn die Validierungsregeln bei der Zerstörung erfolgreich sind, werden die Originaldaten mit den Werten der Steuerelemente aktualisiert. Andernfalls bleiben die ursprünglichen Daten unverändert. Deshalb müssen Sie für jedes neue Formular nur das Formular im Ressourcen-Editor erstellen und dann einen Satz an Regeln festlegen, der bei der Validierung der Daten verwendet werden soll (im Gegensatz zum Erstellen einer neuen Formularklasse und dem Duplizieren der Logik).
Im nächsten Schritt können Sie die Lücke zwischen Arrays und Sammlungen der Standardvorlagenbibliothek (Standard Template Library, STL) sowie zwischen Listensteuerelementen und Struktursteuerelementen schließen. Nehmen Sie wir zum Beispiel ein Array aus Mitarbeitern und ein Listensteuerelement. Das Steuerelement lässt sich an das Array etwa so binden:
list_ctrl->bind(employees);
Wie Sie vielleicht schon erwartet haben, wird dadurch das Listensteuerelement aktualisiert. Außerdem wird jede Änderung an den Zellen des Listensteuerelements automatisch mit dem Mitarbeiterarray synchronisiert.
Mit eGUI++ sollte eine fantastische Bibliothek erstellt werden, mit der die GUI-Programmierung Spaß macht. Es bleibt zu hoffen, dass Sie als C++-Programmierer dieser Aussage zustimmen. Sie finden die Quellen und Binärdateien unter torjo.com.

John Torjo ist C++-Programmierer, der C++ selbst nach 10 Jahren Programmierarbeit immer noch großartig findet. John hat Spaß am Protokollieren und an der GUI, und er weiß Herausforderungen zu schätzen. Wenn Sie eine geeignete Problemstellung haben, senden Sie ihm eine E-Mail. Weitere Informationen finden Sie auf seiner Website unter torjo.com.

Page view tracker