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
Als Basis für die Untersuchung der
Nachrichtenverarbeitung verwenden wir unser erstes Fenster,
hier noch einmal das vollständige
Programm:
#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;
RegisterClass (&wc); HWND hwnd = CreateWindow (szName, "", WS_SYSMENU | WS_THICKFRAME, 0, 0, 200, 100, NULL, NULL, hI, NULL); ShowWindow
(hwnd, iCmdShow);
//
Nachrichten-Schleife
//
Windows-Nachrichten-Prozedur
switch
(message)
case WM_DESTROY:
return
DefWindowProc (hwnd, message, wParam, lParam);
|
Wenn wir das Programm kompilieren und starten, erscheint folgendes einfaches Fenster in der linken oberen Ecke.
Jetzt interessieren wir uns nicht
für
die Fenstermerkmale dieses Fensters, sondern wenden uns der
Nachrichtenverarbeitung
in dieser Windows-Anwendung zu. Zuständig für die
Verarbeitung
der Nachrichten innerhalb unseres Programmes ist die Windows-Prozedur,
deren Namen wir mittels wc.lpfnWndProc
= WndProc; in
der WNDCLASS-Struktur als WndProc
festgelegt haben.
Nachricht heißt innerhalb
Windows message. Die Kennung WM_... bedeutet Windows
Message.
In WndProc(...) werden die
Nachrichten
über eine switch/case-Kontrollstruktur sozusagen
ausgefiltert,
um mittels individuellem Code darauf zu
reagieren. Alle Nachrichten, für die wir keine besondere Aktion
vorgesehen
haben,
übergeben wir der allgemeinen
Windows-Nachrichten-Prozedur
DefWindowProc(...).
Analysieren wir
als
Beispiel die wichtige Nachricht WM_DESTROY:
Innerhalb von WndProc(...) haben wir
für
WM_DESTROY folgendes vorgesehen:
case
WM_DESTROY:
PostQuitMessage (0);
return 0;
Beginnen wir mit WM_DESTROY und
PostQuitMessage
(0). Was ist bzw. bewirkt das genau?
Wenn Sie herausfinden wollen, was ein
Codefragment, das Sie irgendwo übernommen haben,
genau bewirkt, dann sollten Sie
folgendermaßen
vorgehen:
"The WM_DESTROY
message
is sent when a window is being destroyed.
It is sent to the
window
procedure of the window being destroyed after the window is removed
from the screen.
This message is sent
first to the window being destroyed and then to the child windows (if
any)
as they are destroyed.
During the processing
of the message, it can be assumed that all child windows still exist.
The PostQuitMessage
function indicates to the system that a thread has made a request
to
terminate (quit).
It is typically used
in response to a WM_DESTROY message.
VOID PostQuitMessage( int nExitCode // exit code );
nExitCode:
Specifies an application exit code. This value is used as the wParam
parameter of the WM_QUIT message.
Return
Values:
This function does not return a value.
Remarks:
The PostQuitMessage function posts a WM_QUIT message to the
thread's
message queue and returns immediately;
the function simply indicates to the system that the thread is
requesting
to quit at some time in the future.
When the thread retrieves the WM_QUIT message from its message queue,
it
should exit its message loop and return
control to the system. The exit value returned to the system must be
the
wParam parameter of the WM_QUIT message."
Fazit:
WM_DESTROY wird gesendet, wenn das
Fenster zerstört wird. Daraufhin müssen wir mittels
PostQuitMessage(0) eine Nachricht
WM_QUIT
mit wParam=0 erzeugen, damit die Nachrichtenschleife (message loop)
endet.
Jetzt schauen wir schnell in MSDN noch
bei WM_QUIT nach:
"The WM_QUIT message
indicates a request to terminate an application and is generated when
the
application calls the PostQuitMessage function.
It causes the
GetMessage
function to return zero.
nExitCode:
Value of wParam. Specifies the exit code given in the
PostQuitMessage
function.
Return Values: This
message does not have a return value, because it causes the message
loop to terminate before the message is sent to the application's
window
procedure."
Jetzt haben wir diese typische
Situation,
in die Einsteiger oft geraten. MSDN oder andere Literatur beschreiben
theoretisch
die notwendigen Zusammenhänge, damit ein Windows-Programm
reibungslos
funktioniert. In der Praxis versteht man es jedoch erst wirklich, wenn
man an allem "herumschraubt". Also los: Zunächst betrachten wir
das
Experimentierfeld. Was haben wir zur Verfügung? Zunächst
unsere
Anwendung, in der wir Änderungen durchführen können. Ein
weiteres wichtiges Instrument ist der Taskmanager, der durch
STRG+ALT+Entf
gestartet wird.
Wenn wir unsere Anwendung starten, finden
wir dort einen Eintrag für unser Programm, z.B. Einfachesfenster01
oder ähnlich. Wenn wir nun die Anwendung schließen ("X",
Alt+F4,
Systemmenü), dann verschwindet dieser Eintrag im Taskmanager. Das
bedeutet, dass das Programm wirklich beendet ist. Das Verschwinden des
Fensters reicht hierzu nicht aus, wie Sie bereits ahnen.
O.k., starten wir unser erstes Experiment: Wir erzeugen einfach keine Nachricht WM_QUIT:
case
WM_DESTROY:
//PostQuitMessage (0);
return 0;
Was passiert? Das Fenster verschwindet,
aber das Programm läuft weiter, wie Sie mittels Taskmanager
beweisen
können.
Probieren Sie es mehrfach mit und ohne
PostQuitMessage(0) aus, damit Sie den Unterschied sicher erkennen.
Wo befindet sich unser Programm, wenn
es
das Fenster schließt, aber kein WM_QUIT sendet?
In der Nachrichtenschleife:
MSG msg;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg);
DispatchMessage (&msg);
}
return
msg.wParam;
Aha, in dieser while-Schleife werden
also
die "Messages" eingesammelt. Wir drücken auf "X". Das Fenster
verschwindet.
GetMessage(...) liefert WM_DESTROY.
Nachrichten
an unser Fenster werden in WndProc(...) bearbeitet.
In WndProc(...) erzeugen wir mittels
PostQuitMessage(...)
die Nachricht WM_QUIT,
die bei GetMessage(...) den
Rückgabewert
Null (FALSE) erzeugt,
der dann die while-Schleife beendet und
msg.wParam als Rückgabewert von WinMain(...) zurückgibt.
Daraufhin endet das Programm. Alles klar?
Nein?
Dann schauen wir uns die
Nachrichtenschleife
(message loop) doch noch genauer an:
Zunächst finden wir dort eine
MSG-Struktur,
die Basis für unsere Messages (Nachrichten):
typedef
struct tagMSG
{ HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG; hwnd:
Handle des Fensters, dessen Window-Prozedur die Nachricht erhält.
|
Die MSG-Struktur ist das
Transportmittel
für die Windows-Nachrichten. Innerhalb der Struktur befinden sich
alle notwendigen Informationen
zur Beschreibung der Nachricht. Wie
können
wir dies austesten? Ganz einfach, wir ergänzen unsere
switch/case-Struktur
um einen Mausklick mit der linken Taste, sodaß wir die Parameter
hwnd,
message, wParam und lParam ausgeben können:
switch
(message)
{
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps);
//TextOut (hdc, 20, 20,
"Ich
bin ein Fenster.", 20);
EndPaint (hwnd, &ps);
return 0;
case
WM_DESTROY:
PostQuitMessage (0);
return 0;
case
WM_LBUTTONDOWN:
hdc
= GetDC(hwnd);
char
str[50];
sprintf(str,"%i|%i|%i|%i",hwnd,message,wParam,lParam);
TextOut
(hdc, 20, 20, str, strlen(str));
ReleaseDC(hwnd,hdc);
return 0;
}
Wegen der Anweisung sprintf(...) müssen wir auch den zugehörigen header stdio.h einbinden:
#include
<windows.h>
#include
<stdio.h>
LRESULT
CALLBACK
WndProc (HWND, UINT, WPARAM, LPARAM);
int WINAPI
WinMain (HINSTANCE hI, HINSTANCE hPrI, PSTR szCmdLine, int iCmdShow)
{
...
Wenn Sie jetzt mit der linken Maustaste im Fenster klicken, erhalten Sie Fensterhandle, Nachricht (513 entspricht offenbar WM_LBUTTONDOWN), wParam und lParam als Integer-Ausgabe:
Bei einem Neustart des Programms
erhält
das Fenster jeweils einen neuen Handle. Die Nummer der Message bleibt
bei
513.
wParam ist 1 und lParam ändert sich
ständig. Der Hauptinformationsgehalt steckt also dort in lParam.
Verdauen Sie dies bitte in aller Ruhe!
Windows besteht fast nur aus Zahlen, die zum besseren Programmieren als
Konstanten definiert sind.
Wir werden dies prüfen, indem wir
WM_LBUTTONDOWN einfach durch die oben ermittlete Nachrichten-Nummer 513
ersetzen:
case 513:
//
WM_LBUTTONDOWN:
hdc
= GetDC(hwnd);
char
str[50];
sprintf(str,"%i|%i|%i|%i",hwnd,message,wParam,lParam);
TextOut
(hdc, 20, 20,
"
", 30); // 30 *
Leertaste
TextOut
(hdc, 20, 20, str, strlen(str));
ReleaseDC(hwnd,hdc);
return 0;
Es funktioniert! Sie glauben es noch
immer
nicht? Die Datei winuser.h liefert Ihnen die Antwort.
Schauen Sie sich dort gut um, damit Sie
das Windows-Nachrichtensystem besser verstehen :
...
#define
WM_MOUSEFIRST
0x0200
#define
WM_MOUSEMOVE
0x0200
#define
WM_LBUTTONDOWN
0x0201 // dezimal 513
#define
WM_LBUTTONUP
0x0202
#define
WM_LBUTTONDBLCLK
0x0203
#define
WM_RBUTTONDOWN
0x0204
#define
WM_RBUTTONUP
0x0205
#define
WM_RBUTTONDBLCLK
0x0206
#define
WM_MBUTTONDOWN
0x0207
#define
WM_MBUTTONUP
0x0208
#define
WM_MBUTTONDBLCLK
0x0209
...
Damit Sie mehr Freude an diesem kleinen Testprogramm haben, hier die Details von LBUTTONDOWN:
WM_LBUTTONDOWN wird bei einem Linksklick der Maus erzeugt,
während
man sich im "client area" befindet.
Das Fenster unter dem Mauscursor erhält diese Nachricht nur dann,
wenn die Maus nicht im "capture" eines anderen Fensters ist.
WM_LBUTTONDOWN
fwKeys = wParam;
xPos = LOWORD ( lParam ); //
x-Koordinate relativ zur oberen linken Ecke der "client area"
yPos = HIWORD ( lParam );
//
y-Koordinate relativ zur oberen linken Ecke der "client area"
fwKeys
Hier ist die Information enthalten, ob verschiedene "virtual keys"
(VK_...) gedrückt sind.
Folgende Werte können dort enthalten sein:
MK_CONTROL | Strg-Taste |
MK_LBUTTON | Linke Maustaste |
MK_MBUTTON | Mittlere Maustaste |
MK_RBUTTON | Rechte Maustaste |
MK_SHIFT | Shift-Taste |
Hier sehen Sie, dass wParam nicht immer
1 sein muß. Er zeigt die key-flags an. Ausprobieren! Wenn Sie
Shift
drücken, erhalten Sie 5,
bei Strg erhalten Sie 9. Drücken
Sie die linke Maus bei gedrückter rechter Maus, dann ergibt sich 3.
Jetzt noch mal zu lParam. Dieser Parameter hat ein LOWORD und ein HIWORD. Also verfeinern wir unser Programm ein wenig:
switch
(message)
{
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps);
//TextOut
(hdc, 20, 20, "Ich bin ein Fenster.", 20);
EndPaint (hwnd, &ps);
return 0;
case
WM_DESTROY:
PostQuitMessage (0);
return 0;
case
WM_LBUTTONDOWN:
hdc
= GetDC(hwnd);
char
str[50];
sprintf(str,"%i|%i|%i|%i|%i",hwnd,message,wParam,LOWORD(lParam),HIWORD(lParam));
TextOut
(hdc, 20, 20,
"
", 30); // 30 * Leertaste
TextOut
(hdc, 20, 20, str, strlen(str));
ReleaseDC(hwnd,hdc);
return 0;
}
Ein Mausklick in die linke obere Ecke (0,0) liefert entsprechend die x- und y-Koordinate:
Ich hoffe, dass Sie nun das
Nachrichtensystem
und die Verarbeitung in der Windows-Prozedur (hier: WndProc)
prinzipiell verstanden haben.
Zurück zur Nachrichtenschleife:
MSG
msg;
while (GetMessage
(&msg, NULL, 0, 0) )
{
TranslateMessage
(&msg);
DispatchMessage
(&msg);
}
GetMessage(...) | holt die Nachrichten aus der Windows-Nachrichtenschlange für unseren Thread. |
TranslateMessage(...) | übersetzt Virtual-Key-Nachrichten in Character-Nachrichten und |
DispatchMessage(...) | liefert die Nachrichten bei der Windows-Prozedur ab. |
Schauen Sie diesbezüglich auch in MSDN nach.
Die Parameterliste von GetMessage(...) ist wie folgt:
BOOL GetMessage(
LPMSG lpMsg,
HWND hWnd,
UINT
wMsgFilterMin,
// first message
UINT
wMsgFilterMax
// last message
);
Wenn hWnd auf NULL gesetzt wird,
erhält
GetMessage Nachrichten aller Fenster, die zum aufrufenden Thread
gehören.
Mit Hilfe von wMsgFilterMin und
wMsgFilterMax
kann man die eingehenden Nachrichten filtern.
Der Rückgabewert dieser Funktion
ist TRUE, solange nicht WM_QUIT als Message auftaucht.
Dieser Mechanismus wird durch die
while-Schleife
genutzt:
while (GetMessage (&msg, NULL, 0, 0) ) {/*Nachrichtenverarbeitung*/}
Wir befinden uns solange in der
"Nachrichtenschleife",
wie der Rückgabewert von GetMessage TRUE ist.
Ansonsten (bei WM_QUIT) wird diese
Schleife
verlassen.
Diesen Mechanismus der "Nachrichtenpumpe" testen wir sofort aus. Also, wir nehmen unser Mausklick-Programm von oben und streichen einfach TranslateMessage und DispatchMessage:
//
Nachrichten-Schleife
MSG msg;
while (GetMessage (&msg, NULL, 0, 0))
{
//TranslateMessage (&msg);
//DispatchMessage (&msg);
}
return
msg.wParam;
}
Oh je, oh je! Was für ein Monster
haben wir hier erzeugt. Es reagiert nicht mehr auf Mausklick und
läßt
sich auch nur noch über den Taskmanager schließen! Das liegt
daran, daß unsere Nachrichten, die wir durch unsere Aktionen
erzeugen,
einfach ignoriert werden.
Niemand reicht die von GetMessage in die
Nachrichtenstruktur msg weitergegebenen Botschaften an unsere
WndProc(...)
weiter.
Also nehmen wir zumindest DispatchMessage(...) wieder in Gebrauch:
//
Nachrichten-Schleife
MSG msg;
while (GetMessage (&msg, NULL, 0, 0))
{
//TranslateMessage (&msg);
DispatchMessage (&msg);
}
return
msg.wParam;
}
Jetzt funktioniert das System wieder. GetMessage(...) fängt die Nachrichten ein, reicht diese an die Struktur msg weiter. DispatchMessage(...) gibt diese Nachrichten geordnet an WndProc(...) weiter. Auch die Shift- und Strg-Taste werden beim Mausklick korrekt erfaßt.
Damit Sie ein Gefühl für die
Viehlzahl an Nachrichten bekommen, setzen Sie bitte das
VC-Dienstprogramm
Spy++
ein.
Nachstehend sehen Sie die anfallenden
Nachrichten zwischen Drücken (WM_LBUTTONDOWN) und Loslassen
(WM_LBUTTONUP)
der linken Maustaste. Die Nachrichten
1-57 fielen an, als die Maus in das Fenster eindrang:
Wie Sie sehen, zeigt Spy++ z.B. in
Nachricht
<00058> auch die zugehörigen Parameter der Message-Struktur
an.
Sie sehen hier auch mein "Zittern" beim
Mausklick von xpos=115 nach xpos=116, das durch WM_MOUSEMOVE angezeigt
wird.
Experimentieren Sie munter weiter. Ein
Eldorado für Message-Fans! Wenn Sie das Nachrichtensystem
begreifen,
haben Sie den Kern von Windows verstanden.