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
DLL steht für Dynamic
Link Libraries.
Darunter versteht man im
Betriebssystem Windows Routinen, die durch Prozeduren aufgerufen werden
und zur Laufzeit in die Anwendung geladen und mit dieser verknüpft
werden. DLLs enthalten typischerweise spezielle Funktionen, die nicht
im Windows-Betriebssystem enthalten sind. Das Windows-Betriebssystem
verwendet eine Vielzahl von
DLL's, z.B. KERNEL32.DLL, USER32.DLL und GDI32.DLL, als elementare
Bibliotheken, die alle auf dem Betriebssystem aufsetzenden Programme
nutzen können.
Am besten versteht man die
grundsätzlichen Zusammenhänge, wenn man selbst ein ganz
einfaches Beispiel erstellt.
Erstellen Sie ein Verzeichnis mit
Namen "DLL" und erstellen Sie zunächst bitte folgende drei Dateien
mit den entsprechenden Sourcecodes:
//lib.h #ifdef
__cplusplus EXPORT
CALLBACK Funktion();
|
//lib.c #include
<windows.h> int
WINAPI DllMain (HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved) EXPORT
CALLBACK Funktion() |
//test.c #include
<windows.h> int
WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR
szCmdLine, int iCmdShow) |
Sie verfügen nun über diese drei Dateien lib.h, lib.c und test.c im Verzeichnis "DLL".
Nun erstellen wir mit VC++ ein
Arbeitsverzeichnis namens "DLL_Test":
Man wählt hierzu Datei-Neu-(Register)Arbeitsbereiche
aus. Als Namen wählen Sie bitte "DLL_Test".
Sie haben nun ein leeres
Arbeitsverzeichnis erstellt.
Innerhalb dieses Arbeitsbereiches
erstellen wir nun zwei Projekte "LIB" und "TEST":
Man wählt hierzu Datei-Neu-(Register)Projekte
aus. Achten Sie darauf, daß die Option "Hinzufügen zu
akt. Arbeitsbereich" ausgewählt ist. Dann wird automatisch
unser Arbeitsbereich-Ordner "...\DLL_Test" als Pfad eingestellt.
Als Projekttyp wählen Sie Win32
Dynamic-Link Library, und als Projekt-Namen wählen Sie
nun bitte "LIB".
Wiederholen Sie die Projekterstellung mit "TEST". Hierbei wählen Sie Win32-Anwendung.
Jetzt haben wir ein leeres Arbeitsverzeichnis mit zwei leeren Projekten erzeugt.
Damit nichts schief geht, kopieren
Sie bitte lib.h und lib.c in das Unterverzeichnis LIB und entsprechend
lib.h und test.c in das Unterverzeichnis TEST. Fügen Sie nun zu
dem Projekt "LIB" die Dateien lib.h und lib.c hinzu. Zum Projekt "TEST"
fügen Sie bitte lib.h und test.c hinzu.
Hinzufügen erfolgt z.B. mit
Rechtsklick auf den Projektnamen und Auswahl von "Dateien zu Projekt
hinzufügen...".
Nun wählen wir LIB als
aktives Projekt
und erzeugen die DLL mittels Funktionstaste "F7":
Im Unterverzeichnis DEBUG finden
Sie nun unsere DLL mit Namen lib.dll.
Wählen Sie nun TEST als
aktives Projekt und betätigen Sie "F7":
test.obj : error LNK2001:
Nichtaufgeloestes externes Symbol _Funktion@0
Debug/TEST.exe : fatal error
LNK1120: 1 unaufgeloeste externe Verweise
Fehler beim Ausführen von
link.exe.
Offenbar liegt beim Linken ein
Fehler vor.
Mit TEST als aktives Projekt
wählen Sie im Menü die Einstellung Projekt-Abhängigkeiten
und machen TEST abhängig von LIB.
Nun versagt der Linker nicht mehr
seinen Dienst! Im Verzeichnis DEBUG finden Sie nun test.exe.
Jetzt führen wir test.exe aus:
Die erforderliche DLL-Datei
LIB.DLL wurde nicht gefunden.
Sie kopieren lib.dll nach ...\TEST\DEBUG, damit sich exe und dll treffen können.
Hoffentlich grüßt Sie
nun ein "Hallo Welt !" als "Info aus der DLL". Dann haben Sie es
geschafft!
Wenn nein, sollten Sie sich intensiv
mit dem Thema Arbeitsverzeichnisse/Projekte befassen.
Nun zum Verständnis:
Am besten fügen wir geistig
lib.h und test.c zusammen:
//test.c
mit lib.h #include
<windows.h> #ifdef __cplusplus
EXPORT CALLBACK
Funktion(); int
WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR
szCmdLine, int iCmdShow) |
Auf diese Weise sehen Sie genau,
daß hier EXPORT als Stellvertreter für spezielle
Compileranweisungen steht:
__declspec
(dllexport) im Falle von
C und
extern
"C" __declspec (dllexport) im
Falle von C++.
Dies ermöglicht die Nutzung
der DLL sowohl in C als auch in C++.
Damit ist klar, daß unsere Funktion() sich in der entsprechenden DLL befindet.
In lib.c wird nun die DLL realisiert. Als Einstieg findet man hier DllMain(...) anstelle von WinMain(...).
BOOL
WINAPI DllMain(
HINSTANCE hinstDLL, //
handle to DLL module
DWORD fdwReason, //
reason for calling function
LPVOID lpvReserved //
reserved
);
In unserem Fall reicht es, zu
wissen, daß die Funktion ein TRUE zurück geben sollte.
Bevor wir uns weiter mit
der Erstellung von DLL’s befassen, wenden wir uns dem dynamischen
Zugriff auf Funktionen innerhalb einer DLL zu. Beginnen wir mit
einem einfachen
Beispiel:
#include <windows.h> LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM
wParam, LPARAM lParam) { HDC
hdc; PAINTSTRUCT
ps; switch
(message) {
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
Ellipse (hdc, 40, 20, 140, 60);
EndPaint (hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage (0);
return 0; } return
DefWindowProc(hwnd, message, wParam, lParam); } int WINAPI WinMain(HINSTANCE hI, HINSTANCE, PSTR, int
iCmdShow) {
char szName[] =
"Fensterklasse"; 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
= (HBRUSH)3; wc.lpszMenuName = NULL; wc.lpszClassName
= szName; RegisterClass(&wc);
HWND
hwnd = CreateWindow(szName, "DLL - Demo", WS_SYSMENU, 0, 0, 200, 140,
NULL, NULL, hI, NULL); ShowWindow(hwnd, iCmdShow); UpdateWindow(hwnd);
MSG
msg; while
(GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg); } return
msg.wParam; } |
Das ist ein einfaches WinAPI-Programm, das in einem kleinen Fenster
eine Ellipse zeichnet.
Die Frage ist nur, wo
findet unser Programm eigentlich diese Funktion Ellipse(…)? Sie wissen
sicher, dass die grundlegenden WinAPI-Funktionen in drei Modulen
verteilt sind,
die sich Kernel, User und GDI nennen. Bei den heutigen
Windows-Betriebssystemen (bei Windows 2000 z.B. in
...:\WINNT\system32\... nach) findet man diese Module als kernel32.dll,
user32.dll und gdi32.dll. Damit unser Programm
die Funktion Ellipse findet, binden wir mittels Linker die Bibliothek
gdi32.lib ein. Das Betriebssystem sorgt dafür, dass unser Programm
die Speicheradresse findet, an dem diese Funktion beim Laden der
gdi32.dll im Speicher positioniert wurde.
Sie glauben das nicht? O.k., machen wir einen Versuch. Unter Projekt – Einstellungen – Linker finden Sie folgende einzubindenden Objekt-/Bibliothek-Module:
Sie sehen gleich an erster Stelle die grundlegenden Windows-Module
kernel32.lib user32.lib und gdi32.lib. Entfernen wir doch einfach den
Eintrag gdi32.lib und kompilieren das Ganze. Der Compiler, genauer
gesagt der Linker, schüttelt sich sofort:
DynDLL.obj : error LNK2001: Nichtaufgeloestes externes Symbol __imp__Ellipse@20
Release/DynDLL.exe : fatal error LNK1120: 1 unaufgeloeste externe Verweise
Fehler beim Ausführen von link.exe.
Sie sehen, der Linker sucht nach
dem externen Symbol __imp__Ellipse@20, das unser Programm
später zu der Funktion Ellipse in gdi32.dll führen
soll. Daraus lernen wir, dass unsere „normalen“ WinAPI-Programme
ständig auf Funktionen in DLL’s zugreifen. Wir müssen hier
nur keinen besonderen Aufwand betreiben, um dorthin zu gelangen. Das
Einbinden der xxx.lib reicht aus, um auf eine Funktion in xxx.dll
zuzugreifen.
Machen Sie sich doch einmal die
Freude und schauen Sie einfach mit einem Editor in der Datei …\Program Files\Microsoft Visual
Studio\VC98\Lib\gdi32.lib
nach. Dort finden Sie u.a. unser externes Symbol __imp__Ellipse@20.
Nun könnten wir einfach die
gdi32.lib wieder einbinden, und schon würde unser Programm sauber
„gelinkt“ werden. Nehmen wir aber an, wir hätten gar keine
gdi32.lib, was machen wir
nun? Dazu ändern wir die Funktion WndProc(…) wie folgt ab:
#include <windows.h> LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM
wParam, LPARAM lParam) { HDC
hdc; PAINTSTRUCT
ps; HINSTANCE hLib = LoadLibrary
("GDI32.DLL"); typedef
BOOL (WINAPI *PFN_ELLIPSE) (HDC, int, int, int, int); PFN_ELLIPSE
pfn_Ellipse = (PFN_ELLIPSE) GetProcAddress(hLib, "Ellipse"); switch
(message) {
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
pfn_Ellipse (hdc, 60, 50, 160, 90);
EndPaint (hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage (0);
return 0; } FreeLibrary (hLib); return
DefWindowProc(hwnd, message, wParam, lParam); } int WINAPI WinMain(HINSTANCE hI, HINSTANCE, PSTR, int
iCmdShow) { /* wie oben */ } |
Mit diesem Programm
können
Sie ebenfalls auf die gleiche Funktion Ellipse in gdi32.dll zugreifen,
nur
etwas umständlicher:
Schauen wir uns alle Schritte in
Ruhe an. Da wird zunächst die Bibliothek gdi32.dll mit LoadLibrary(…) geladen. Die Information an das
Betriebssystem, dass unser Programm die Bibliothek nicht mehr braucht,
lautet FreeLibrary(…). Zur
Ermittlung der Zieladresse verwendet man GetProcAddress(hLib, "Ellipse"). Wir
erhalten einen Zeiger zurück, mit dem man die Funktion in der DLL
aufruft. Da während der Kompilierung keinerlei
Typüberprüfung erfolgt, muss man als Programmierer selbst
dafür sorgen, dass die Parameter für die Funktion korrekt
sind. Ansonsten kann die Funktion den auf dem Stack reservierten
Speicherbereich überschreiten mit der Folge einer
Zugriffsverletzung. Daher muss man den Prototyp der DLL-Funktion kennen
und mit typedef eine Typdefinitionen des speziellen Funktionszeigers
erzeugen:
typedef BOOL (WINAPI *PFN_ELLIPSE) (HDC, int, int, int,
int);
PFN_ELLIPSE pfn_Ellipse = (PFN_ELLIPSE)
GetProcAddress(hLib, "Ellipse");
Wenn Sie diese beiden Zeilen z.B.
einfach durch
FARPROC pfn_Ellipse = GetProcAddress(hLib, "Ellipse");
ersetzen, erhalten Sie in der
Zeile des
Funktionsaufrufs folgende Fehlermeldung:
'int (__stdcall *)(void)' : Zu viele Parameter uebergeben
Auf diese Weise könnte man
also höchstens eine Funktion ohne Parameter starten, wie z.B.
folgende Funktion aus gdi32.dll:
FARPROC
pfn_GdiGetBatchLimit = GetProcAddress(hLib, "GdiGetBatchLimit");
pfn_GdiGetBatchLimit();
Nun fügen wir die Bibliothek
gdi32.lib im Linker wieder hinzu, damit wir direkt die
WinAPI-Funktionen für GDI aufrufen können. Das sind in
unserem Beispiel folgende Funktionen:
SelectObject(…)
GetStockObject(…) Ellipse(…)
Wir schauen uns hier vergleichend
beide Methoden des Zugriffs auf die Funktion Ellipse(…) an:
#include <windows.h> LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM
wParam, LPARAM lParam) {
HDC hdc; PAINTSTRUCT ps; /*
Vorbereitung der Methode 2 */ HINSTANCE hLib = LoadLibrary
("GDI32.DLL"); typedef
BOOL (WINAPI *PFN_ELLIPSE) (HDC, int, int, int, int); //
wichtig für Funktionen mit Parameter PFN_ELLIPSE
pfn_Ellipse = (PFN_ELLIPSE) GetProcAddress(hLib, "Ellipse"); switch (message) {
case WM_PAINT:
hdc
= BeginPaint(hwnd, &ps);
/*
Methode 1 - GDI32.LIB ist mittels Linker mit dem Programm verbunden.*/
SelectObject(hdc, GetStockObject(DKGRAY_BRUSH));
Ellipse (hdc, 40, 20, 140, 60);
/* Methode 2 */
SelectObject(hdc, GetStockObject(LTGRAY_BRUSH));
pfn_Ellipse (hdc, 60, 50, 160, 90);
EndPaint (hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage (0);
return 0; }
/* Nachbereitung der
Methode 2 */ FreeLibrary (hLib); return DefWindowProc(hwnd, message, wParam, lParam); } int WINAPI WinMain(HINSTANCE hI, HINSTANCE, PSTR, int
iCmdShow) { /* wie oben */ } |
Ich hoffe, dass das bei Ihnen
alles geklappt
hat. Da man aber nie sicher weiß, ob eine DLL oder eine Funktion
auch
wirklich geladen ist, sollte man bei DLL’s eine entsprechende
Fehlerabfrage und -behandlung durchführen, die oben aus
Gründen der Übersichtlichkeit weggelassen wurde. Das
könnte allgemein dargestellt wie folgt aussehen:
hLib = LoadLibrary("xxx.dll");
if (hLib != NULL)
{
pfnFunction = (PFNFUNCTION)GetProcAddress(hLib,"Function");
if (!pfnFunction)
{
// Fehlerbehandlung Funktion nicht gefunden
FreeLibrary(hLib);
return SOME_ERROR_CODE;
}
else
{
// Funktion starten
RetVal = pfnFunction(Param1, Param2, ...);
}
}
else
{
// Fehlerbehandlung DLL nicht gefunden
}
…
FreeLibrary(hLib);
Wie wichtig dies
ist, erkennt man, wenn man unser obiges Beispiel mit Fehlern behaftet:
Fehler 1: Wir
vergessen beim Namen der DLL die „32“:
HINSTANCE hLib =
LoadLibrary ("GDI.DLL");
Unser Programm wird
kompiliert und gelinkt, jedoch beim Ausführen erfolgt ein
Lesefehler auf dem Speicher:
Beheben Sie Fehler
1, damit wir den nächsten Fehler testen können:
Fehler 2: Wir
verwechseln den Namen der Funktion:
PFN_ELLIPSE pfn_Ellipse = (PFN_ELLIPSE)
GetProcAddress(hLib, "Elipse");
Wieder wird
kompiliert und gelinkt, jedoch beim Ausführen kracht es erneut.
Die Fehlermeldung kennen Sie bereits von oben.
Es ist also sehr
wichtig bei Sprüngen im Speicher von einem Modul zum anderen die
richtigen Wegweiser aufzustellen. Daher sollten Sie bei Experimenten
mit eigenen DLL’s unbedingt Fehlerabfragen implementieren, damit Sie
aufgrund eigener Fehlermeldungen sofort erkennen, wo genau der
misslungene Sprung lokalisiert ist.
Betrachten wir die drei grundlegenden
Windows-DLL’s kernel32.dll, user32.dll und gdi32.dll genauer.
Welche Funktionen
sind in welcher DLL verpackt? Wie erfährt man eigentlich die
Funktionsnamen und die Anzahl und den Speicherbedarf der notwendigen
Parameter?
Im Rahmen von MS
VC++ finden Sie ein Programm namens dumpbin.exe. Ein analoges
Programm von Borland nennt sich tdump.exe. Am besten legen Sie
ein Verzeichnis
C:\Dump
an und kopieren dieses File und die
DLL’s, die
Sie untersuchen wollen dorthin, in unserem Fall bitte die drei o.g.
Windows-DLL’s, die man bei MS Windows 2000 z.B. im Verzeichnis
C:\Winnt\system32 findet.
Dann erstellen Sie
bitte eine batch-Datei namens dump.bat, die folgende Anweisung
enthält:
dumpbin.exe
/exports gdi32.dll >gdi32dll.txt
Im Text-File
gdi32dll.txt finden Sie nun die gewünschten Informationen:
Dump of file gdi32.dll
543 number of names
ordinal hint RVA
name
1 0 00028FEB
AbortDoc
2 1 000294F1
AbortPath
...
90 59 00016C95
Ellipse
...
525 20C 00005DB1
TextOutA
526 20D 00004234
TextOutW
...
543 21E 0002812A gdiPlaySpoolStream
Das ist eine gute
Methode, um sich schnell einen Überblick über die einem DLL-Modul zusammengefassten Funktionen
zu verschaffen. In den mit MS Windows 2000 mitgelieferten DLL’s finden
Sie folgende Anzahl:
Kernel32.dll: 823
user32.dll: 695 gdi32.dll: 543
Das sind insgesamt
2061 WinAPI-Funktionen. Wir müssen jedoch einige abziehen, da z.B.
TextOut(…) sowohl als ANSI-Version TextOutA(…) als auch als
Unicode-Version TextOutW(…) – das W steht für wide character - vorhanden ist.
Nun können Sie die gleiche
Vorgehensweise auch auf weitere DLL’s anwenden.