API-Windows-Programmierung (ohne MFC)
Inhalt
1.
WinMain(...) und windows.h
2.
Eigene Fenster erzeugen
3.
Zwei Fenster auf Basis einer WNDCLASS-Struktur erzeugen
4.
Nachrichtenverarbeitung
5.
Interaktionen mit Kindfenstern
6.
DLL-Programmierung
In Windows erzeugen wir normalerweise rechteckige
Fenster, mit denen wir interagieren.
Daher stellen wir zunächst einige fundamentale
Merkmale eines rechteckigen Fensters zusammen,
die wir dann mittels Programm definieren werden:
X,Y...: | Linke obere Ecke (x,y), Höhe (y-size), Breite (x-size) des Fensters |
Icon: | Kleines Bitmap (links oben in der Titelzeile bzw. links in der Taskleiste) |
Sysmenu: | Menü, das sich beim Klick auf das Icon bzw. mit Alt+Space öffnet |
Frame: | kein Rahmen - fixierter Rahmen - mit der Maus veränderbarer Rahmen |
Caption: | Überschrift (String) in der Titelzeile |
Minimize: | Schaltfläche zum Minimieren des Fensters (-> Taskleiste) |
Maximize: | Schaltfläche zum Maximieren des Fensters (-> Taskleiste) |
Cursor: | Zeigerform der Maus innerhalb des Fensters |
Background: | Hintergrundmuster des Fensters |
Menu: | Zum Fenster zugehöriges Menü |
Für den Anfang ist das schon eine ganze Menge. Dies ist typisch für Windows. Aufgrund der optischen/grafischen Unterstützung des Programms muß sich der Programmierer von Anfang an um eine Vielzahl grafischer Details kümmern, die mehr mit Design als mit dem eigentlichen Programmierziel zu tun haben. Aber genau dieses Handwerkzeug muß der Windows-Programmierer zu gebrauchen lernen.
Im nächsten Schritt erstellen wir unser erstes Fenster ohne Assistent und ohne MFC-Bibliothek:
Achten Sie auf folgende grundlegenden
Bestandteile:
Header-Datei windows.h,
Hauptfunktion WinMain(),
Nachrichtenschleife und
Nachrichten-Funktion WndProc():
#include <windows.h> ...
int
WINAPI WinMain(...)
RegisterClass ( &wc );
HWND
hwnd
= CreateWindow ( szName,
...
); //Hier wird das Fenster erzeugt
//Nachrichtenschleife
LRESULT
CALLBACK WndProc (HWND hwnd,UINT
message,
...)
case WM_... :
...
case WM_CLOSE:
case WM_DESTROY:
|
Windows-WinAPI-Grundgerüst |
Konzentrieren Sie sich zunächst auf die
Merkmale
und Funktionen des Fensters.
Zum Thema Nachrichtenverarbeitung kommen wir
später.
Die Fenstermerkmale werden an zwei Stellen
definiert.
Die erste Hälfte legen wir in WNDCLASS-Struktur fest.
Ich benutze bewußt den Begriff
WNDCLASS-Struktur
und nicht den oft zu findenden Begriff Fensterklasse,
denn hier handelt es sich nicht um eine C++-Class
im objektorientierten Sinn, sondern schlicht um eine Struktur.
Diese WNDCLASS-Struktur wird mit dem Befehl RegisterClass
( &... ) "registriert".
Auf Basis dieser WNDCLASS-Struktur können
wir
dann die zweite Hälfte der Fenster-Merkmale festlegen.
Dies erfolgt in der Funktion CreateWindow (...
).
Nachfolgend erstellen wir in einem kompletten
Programm
unser erstes Fenster:
#include
<windows.h>
LRESULT
CALLBACK
WndProc
(HWND, UINT, WPARAM, LPARAM);
int
WINAPI
WinMain
(HINSTANCE hI, HINSTANCE hPrI, PSTR szCmdLine, int iCmdShow)
wc.style
= CS_HREDRAW | CS_VREDRAW; //
CS
= "class
style"
RegisterClass (&wc); HWND
hwnd
= CreateWindow (szName, "", WS_SYSMENU | WS_THICKFRAME,
ShowWindow
(hwnd, iCmdShow);
//
Nachrichten-Schleife
//
Windows-Nachrichten-Prozedur
switch
(message)
case
WM_DESTROY:
return
DefWindowProc
(hwnd, message, wParam, lParam);
|
Wenn wir das obenstehende Programm kompilieren und starten, erscheint folgendes Fenster in der linken oberen Ecke.
Wir analysieren den
Programmcode
nun bewußt nicht der Reihe nach, sondern untersuchen die Merkmale
des Fensters
und prüfen, an welcher
Stelle dies in welcher Form codiert ist.
Zunächst zur
Geometrie.
Fenster sind in Windows normalerweise rechteckig
(andere geometrische
Fensterformen
sind zwar möglich aber nicht gesondert vorgesehen).
Unser rechteckiges Fenster
startet ganz links oben und ist 200 Pixel (Bildpunkte) breit und 100
Pixel
hoch.
Das finden wir in
CreateWindow()
an folgender Stelle:
HWND hwnd
= CreateWindow (
szName, "",
WS_SYSMENU | WS_THICKFRAME,
0, //
initial x position
0, //
initial y position
200, //
initial x size
100,
// initial y size
NULL, NULL,
hI, NULL);
Experimentieren Sie sofort
ausgiebig mit diesen vier Zahlen.
Nur durch eigenes Ausprobieren
und Beobachten versteht und begreift man die Zusammenhänge
wirklich.
Was passiert z.B., wenn man
negative Zahlen für den Ursprung angibt?
Testen Sie die Werte -10,
-10, 200, 100. Es funktioniert! Macht
natürlich
keinen besonderen Sinn,
aber das Programm reagiert
wie erwartet.
Testen Sie nun. ... -1000,
-1000, 200, 100 ...
Das Fenster ist aktiv, aber
nicht im für den Betrachter sichtbaren Bereich.
Schließen können
Sie jetzt nur noch mittels Tastenkombination Alt+F4,
da Sie mit der Maus ja nicht
am Systemmenü anpacken können.
Die Maus kann nur im
sichtbaren
Bereich agieren, während die Tastatur hier auch "unsichtbare"
Fenster
erreicht.
Es gibt aber noch einen
anderen
Weg, das Fenster zu schließen.
Mit der Tastenkombination
Alt+Leertaste können Sie das Systemmenü aktivieren.
Dann können Sie auf
"Schließen"
klicken oder mit der Entertaste bestätigen.
Zum Systemmenü
gehört
übrigens auch das "x" ganz rechts,
das den direkten Zugang zum
Schließen per Mausklick bietet.
Setzen Sie nun die Werte
bitte wieder auf 0, 0, 200, 100
zurück,
damit wir wieder das
vollständig
sichtbare Fenster erzeugen.
Soweit zur Positionierung unseres Fensters.
Wo steht nun eigentlich der
Code für das "Windows-Logo"-Icon (bunte Fahne) ?
Das ist Bestandteil der
WNDCLASS-Struktur
wc:
wc.style
= CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc
= WndProc;
wc.cbClsExtra
= 0;
wc.cbWndExtra
= 0;
wc.hInstance
= hI;
wc.hIcon
= LoadIcon (NULL, IDI_WINLOGO);
wc.hCursor
= LoadCursor (NULL, IDC_ARROW);
wc.hbrBackground
= (HBRUSH) GetStockObject (BLACK_BRUSH);
wc.lpszMenuName
= NULL;
wc.lpszClassName
= szName;
Windows hat folgende vorgegebene Möglichkeiten für den zweiten Parameter:
IDI_APPLICATION
Default application icon.
IDI_ERROR
Hand-shaped icon.
IDI_INFORMATION
Asterisk icon.
IDI_QUESTION
Question mark icon.
IDI_WARNING
Exclamation point icon.
IDI_WINLOGO
Windows logo icon.
Was machen wir nun?
Natürlich
probieren wir sofort sämtliche Varianten aus!
Nur durch Ausprobieren und
Eintippen prägen sich die symbolischen Begriffe
und deren Erscheinungsbild
ein.
IDI_APPLICATION :
IDI_ERROR:
IDI_INFORMATION:
IDI_QUESTION:
IDI_WARNING:
Bevor wir unser erstes Fenster weiter
untersuchen,
eine wichtige Frage:
Woher weiß CreateWindow(), welche registrierte
WNDCLASS-Struktur es als Grundlage nehmen soll?
Die Verknüpfung zwischen der WNDCLASS-Struktur
und CreateWindow(...) wird über einen konstanten Zeiger LPCTSTR
auf den Namen der Fensterklasse gebildet:
char szName[
] = "Fensterklasse";
... wc.lpszClassName = szName; ... HWND hwnd = CreateWindow (szName, ... |
Hier sehen Sie, wie dieser Name die gewählte WNDCLASS-Struktur mit CreateWindow() verknüpft.
Nun zum Kopf unseres Fensters. Dort finden wir vor allem zwei Dinge:
Das Systemmenü (sysmenu) und den Fenstertitel (caption).
Schauen wir in den Programmcode:
HWND hwnd = CreateWindow (szName, "", WS_SYSMENU | WS_THICKFRAME, ...
Beginnen wir mit dem Titel.
Zur Zeit ist der Bereich zwischen den Anführungszeichen leer
(Leerstring).
Schreiben wir einen Titel
hinein:
HWND hwnd
= CreateWindow (szName,
"Dies ist unser erstes
kleines Fenster",
WS_SYSMENU | WS_THICKFRAME, ...
Wir erhalten folgendes Erscheinungsbild. Unser Titel wurde aufgrund Überlänge übrigens abgeschnitten:
Das liegt daran, dass die
Breite
des Fensters (z.Z. 200 Pixel) zu gering ist, um diesen langen Titel
darzustellen.
Wir könnten nun
natürlich
kompliziert die benötigte Breite berechnen, den Platz für das
Icon (links) und das "X" (rechts) addieren,
um diesen Wert dann für
die Fensterbreite zu verwenden. Das werden wir aber nicht machen,
sondern
wir überlassen zunächst dem Betriebssystem die Wahl für
die Fensterbreite. Wie geht dies?
Die Antwort findet man in
MSDN
mittels Suchbegriff CreateWindow.
Man setzt die Fensterbreite
auf CW_USEDEFAULT.
Hierbei wird jedoch leider
auch die angegebene Höhe (hier: 100) ignoriert :
HWND hwnd
= CreateWindow
(
szName,
"Dies ist unser erstes kleines Fenster", //
window caption
WS_SYSMENU |
WS_THICKFRAME,
// window style
0,
0,
CW_USEDEFAULT,
100,
NULL,
NULL,
hI,
NULL
);
Das Fenster wird jetzt richtig groß:
Wenn unsere Höhe einfach ignoriert wird, überlassen wir dem Betriebssystem "beleidigt" einfach die gesamte Geometrie:
HWND hwnd
= CreateWindow
(
szName,
"Dies ist unser erstes kleines Fenster",
WS_SYSMENU | WS_THICKFRAME,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL,
NULL,
hI,
NULL
);
Mal schauen, was passiert, wenn wir das Programm
und damit das zughörige Fenster z.B. dreimal erzeugen.
Die Fenster werden in diesem Fall (CW_USEDEFAULT)
vom
Betriebssystem "kaskadenförmig" angeordnet:
Nun übernehmen wir sofort wieder selbst die Regie. Ändern Sie auf folgende Werte ab:
HWND hwnd
= CreateWindow
(
szName,
"Dies ist unser erstes kleines Fenster",
WS_SYSMENU | WS_THICKFRAME,
0, 0, 400, 100,
NULL,
NULL,
hI,
NULL
);
Bei 400 Pixel Breite wird der Titel vollständig angezeigt:
Verweilen wir zunächst
im oberen Teil des Fensters. Von links nach rechts sehen wir
das Icon für das
Systemmenü,
die Titelzeile
(caption)
und
das "x" des
Systemmenüs.
Von anderen
Windowsanwendungen
kennen Sie sicher die Möglichkeit zum Minimieren ("Wegklicken,
Ablegen")
und zum Maximieren
("Vollbild")
des Fensters. Dies fehlt unserem Fenster noch. Also nichts wie ran:
HWND hwnd
= CreateWindow
(
szName,
"Dies ist unser erstes kleines Fenster",
WS_SYSMENU | WS_THICKFRAME |
WS_MINIMIZEBOX,
0, 0, 400, 100,
NULL,
NULL,
hI,
NULL
);
Zunächst fügen wir (Code siehe oben) den Stil WS_MINIMIZEBOX hinzu. Das ergibt dann folgendes Bild:
Sie sehen, dass Minimieren und Maximieren eine funktionelle Einheit bilden. Aktiviert wurde bisher nur die MINIMIZEBOX.
Das testen wir auch noch umgekehrt, also nur MAXIMIZEBOX:
HWND hwnd
= CreateWindow
(
szName,
"Dies ist unser erstes kleines Fenster",
WS_SYSMENU | WS_THICKFRAME |
WS_MAXIMIZEBOX,
0, 0, 400, 100,
NULL,
NULL,
hI,
NULL
);
Zum guten Schluß fügen wir beide hinzu:
HWND hwnd
= CreateWindow (szName, "Dies ist unser erstes kleines Fenster",
WS_SYSMENU
| WS_THICKFRAME
| WS_MAXIMIZEBOX | WS_MINIMIZEBOX,
0, 0, 400,
100, NULL, NULL, hI, NULL);
Kann man eigentlich in diesem Zusammenhang das Systemmenü weglassen?
HWND hwnd
= CreateWindow (szName, "Dies ist unser erstes kleines Fenster",
WS_THICKFRAME
| WS_MAXIMIZEBOX | WS_MINIMIZEBOX,
0, 0, 400,
100, NULL, NULL, hI, NULL);
Die Antwort ist: Nein! Ohne Systemmenü gibt es kein Icon und keine Minimieren-/Maximieren-Box:
Der Titel ist jedoch nach wie vor sichtbar.
Nun vereinfachen wir das Ganze mittels eines Kombinationsbegriffes:
HWND hwnd
= CreateWindow (szName, "Dies ist unser erstes kleines Fenster",
WS_OVERLAPPEDWINDOW,
0, 0, 400, 100, NULL, NULL, hI, NULL);
WS_OVERLAPPEDWINDOW
kombiniert
folgende Window-Styles:
WS_OVERLAPPED,
|
Damit macht der Kopf des Fensters einen
(gewohnten)
vollständigen Eindruck.
Unser Fenster hat ein Systemmenü und einen
Titel.
Man kann die Größe durch Ziehen am Rahmen
verändern und per Mausklick minimieren/maximieren.
Jetzt verlassen wir Kopf
und
Rahmen und bewegen uns in den Anwendungsbereich (client area),
der sich unterhalb des
Kopfbereiches
befindet. Zunächst fällt der schwarze Hintergrund auf.
Dieser wird in der
WNDCLASS-Struktur
festgelegt:
...
wc.style
= CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc
= WndProc;
wc.cbClsExtra
= 0;
wc.cbWndExtra
= 0;
wc.hInstance
= hI;
wc.hIcon
= LoadIcon (NULL, IDI_WINLOGO);
wc.hCursor
= LoadCursor (NULL, IDC_ARROW);
wc.hbrBackground
= (HBRUSH) GetStockObject (BLACK_BRUSH);
wc.lpszMenuName
= NULL;
wc.lpszClassName
= szName;
...
Die Standard-Auswahl an
"Farbpinseln"
(BRUSH) ist nicht gerade berauschend.
Farbe spielt hier wahrlich
keine große Rolle:
WHITE_BRUSH,
BLACK_BRUSH,
LTGRAY_BRUSH, GRAY_BRUSH,
DKGRAY_BRUSH,
HOLLOW_BRUSH (
identisch
mit NULL_BRUSH ).
Probieren Sie sofort alle aus. Besonders lustig
ist
die NULL_BRUSH.
Damit wird das Fenster durchsichtig(!):
Schieben Sie das Fenster mal nach links über
den Rand und holen Sie es wieder zurück.
Sieht dies bei Ihnen ähnlich aus? Das ist
Windows,
wie es keiner will. Oder doch? Es gibt fast alles.
Sie sind mit schwarz, weiß und grau nicht
zufrieden?
Dann müssen wir mit CreateSolidBrush(...)
eine
eigene HBRUSH erzeugen! Auf geht’s:
int WINAPI
WinMain (HINSTANCE hI, HINSTANCE hPrI, PSTR szCmdLine, int iCmdShow)
{
char szName[ ] = "Fensterklasse";
HBRUSH MyBrush = CreateSolidBrush( RGB( 0, 150, 255 ) );
WNDCLASS wc;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hI;
wc.hIcon = LoadIcon (NULL, IDI_WINLOGO);
wc.hCursor = LoadCursor (NULL, IDC_ARROW);
wc.hbrBackground = MyBrush;
wc.lpszMenuName = NULL;
wc.lpszClassName = szName;
RegisterClass (&wc);
HWND hwnd = CreateWindow (szName, "Dies ist unser erstes kleines
Fenster",
WS_OVERLAPPEDWINDOW, 0, 0, 400, 100, NULL, NULL, hI, NULL);
ShowWindow (hwnd, iCmdShow);
UpdateWindow (hwnd);
...
Damit stehen Ihnen mittels
RGB( rot, grün, blau ) insgesamt
256*256*256
= 16.777.216 Farbwerte (24 bit)
zur Verfügung. Die Werte
für die Einzelfarben sind jeweils von 0 bis 255 einstellbar.
Experimentieren Sie ein wenig
mit den Farben, damit Sie die Wirkung des RGB-Makros verfolgen
können.
Nachfolgend sehen Sie unser aktuelles Fenster, das den gewählten Hintergrund mit RGB( 0, 150, 255) verwendet:
Hätten Sie das vorhandene WHITE_BRUSH
gewählt,
wäre Ihnen evtl. entgangen,
dass die Funktion TextOut(...)
standardisiert
schwarze Schrift (BLACK_PEN)
auf weißem Hintergrund (WHITE_BRUSH) einsetzt.
Wenn wir gerade bei den Farben sind,
verändern
wir sowohl Schrift- als auch Hintergrundfarbe bezüglich
TextOut(...).
Zunächst der Ausgangscode, der sich im Bereich
der Nachrichtenbearbeitung für unser Fenster befindet:
case
WM_PAINT:
hdc = BeginPaint (hwnd, &ps);
TextOut (hdc, 20, 20, "Ich bin ein Fenster.", 20); //device
context, x, y, string, string-Länge
EndPaint (hwnd, &ps);
return 0;
WM steht
für Windows Message.
hdc steht für handle
to device context.
Solche Device Contexts benötigt man,
um Ausgaben auf Bildschirm oder Drucker durchzuführen.
Innerhalb der Nachricht WM_PAINT, die
ausgelöst wird, wenn das Fenster neu gezeichnet werden muß,
besorgt man sich einen hdc mittels BeginPaint(...)
und gibt diesen wieder frei durch EndPaint(...).
Außerhalb der Reaktion auf WM_PAINT
ist das entsprechende Paar übrigens GetDC(...) und ReleaseDC(...).
Wenn wir nun die Farben ändern wollen, erledigen wir dies über diesen handle hdc mittels RGB-Makro:
case
WM_PAINT:
hdc
= BeginPaint (hwnd,
&ps);
SetTextColor( hdc,
RGB( 255, 0, 0) ); //
rot
SetBkColor ( hdc,
RGB( 255, 255, 0) ); //
gelb
TextOut ( hdc,
20, 20, "Ich bin ein Fenster.", 20);
EndPaint (hwnd,
&ps);
return 0;
Machen Sie sich
bitte
die beiden Handles hwnd (handle auf ein Fenster) und hdc
(handle auf einen Gerätekontext) bewußt.
Nach dem Kompilieren / Starten
sehen wir unser Farbenspiel:
Testen Sie hier munter weiter, um ein Gefühl für den Umgang mit den Farben zu erhalten.
Übrigens können Sie mit
int SetBkMode
(
HDC
hdc,
// handle of device context
int iBkMode
//
flag specifying background mode
);
durch Wahl des
Hintergrund-Modus
TRANSPARENT die Hintergrundfarbe ignorieren.
Zurückschalten
können
Sie mit OPAQUE.
Bevor wir uns der in
Windows
sehr wichtigen Nachrichtenverarbeitung zuwenden, schauen wir uns an,
welche Fenstermerkmale und
-funktionen wir bisher betrachtet haben (rot markiert) und welche uns
noch
fehlen (blau markiert).
Die schwarz dargestellten
Befehle sind sozusagen das notwendige Gerüst:
int WINAPI
WinMain (HINSTANCE hI, HINSTANCE hPrI, PSTR szCmdLine, int iCmdShow)
{
char szName[] = "Fensterklasse";
HBRUSH MyBrush = CreateSolidBrush( RGB( 0, 150, 255 ) );
WNDCLASS wc;
wc.style
= CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc; //
Nachrichtenbearbeitungsfunktion
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hI;
wc.hIcon
= LoadIcon (NULL, IDI_WINLOGO);
wc.hCursor =
LoadCursor
(NULL, IDC_ARROW);
wc.hbrBackground = MyBrush;
wc.lpszMenuName = NULL;
wc.lpszClassName = szName;
RegisterClass (&wc);
HWND hwnd = CreateWindow
(
szName,
"Dies ist unser erstes kleines Fenster",
WS_OVERLAPPEDWINDOW,
0, 0, 400, 100,
NULL,
NULL,
hI,
NULL
);
...
Beginnen wir von oben nach unten:
Hier zur Übersicht die
Festlegung der WNDCLASS-Struktur:
typedef
struct _WNDCLASS
{ UINT style; WNDPROC lpfnWndProc; int cbClsExtra; int cbWndExtra; HANDLE hInstance; HICON hIcon; HCURSOR hCursor; HBRUSH hbrBackground; LPCTSTR lpszMenuName; LPCTSTR lpszClassName; } WNDCLASS; |
Das erste Strukturelement style
faßt verschiedene Verhaltensweisen zusammen.
Wichtig sind hier die von
uns gewählten Styles zum Neuzeichnen des Fensters
bei horizontaler bzw.
vertikaler
Größenveränderung:
wc.style = CS_HREDRAW | CS_VREDRAW;
Wenn Sie diese beiden Styles weglassen mittels
wc.style = 0;
dann beobachten Sie, dass
das
Fenster bei einfachen Größenveränderungen nicht
neugezeichnet
wird.
Weitere Styles findet man
z.B. in MSDN unter dem Suchbegriff WNDCLASS.
Gehen wir der Vollständigkeit halber zu den nächsten beiden Elementen, die uns im Moment noch nicht berühren müssen:
HCURSOR hCursor
In unserem Programm finden Sie diesbezüglich in der WNDCLASS-Struktur:
wc.hCursor = LoadCursor (NULL, IDC_ARROW);
LoadCursor bietet folgende Standard-Cursor:
IDC_APPSTARTING
Standard arrow and small hourglass
IDC_ARROW Standard arrow IDC_CROSS Crosshair IDC_HAND Windows NT 5.0 and later: Hand IDC_HELP Arrow and question mark IDC_IBEAM I-beam IDC_NO Slashed circle IDC_SIZEALL Four-pointed arrow pointing north, south, east, and west IDC_SIZENESW Double-pointed arrow pointing northeast and southwest IDC_SIZENS Double-pointed arrow pointing north and south IDC_SIZENWSE Double-pointed arrow pointing northwest and southeast IDC_SIZEWE Double-pointed arrow pointing west and east IDC_UPARROW Vertical arrow IDC_WAIT Hourglass |
Testen Sie ausführlich, damit Sie eine
geistige
Verbindung herstellen zwischen der Cursor-ID und dem Aussehen.
Da Sie momentan mit dem Cursor in unserem Fenster
nichts ausrichten können, wäre z.B. folgendes angebracht:
wc.hCursor = LoadCursor (NULL, IDC_NO);
Wie sich das Fenster ohne festgelegten Cursor verhält, können Sie mit
wc.hCursor = 0;
überprüfen. Sehr unschön! Wieder einmal: Windows, wie es keiner will.
weiter zu
Teil 3: Zwei Fenster auf Basis einer WNDCLASS-Struktur erzeugen