Session D-FLL

Verwendung und Erstellung von FLLs, ActiveX und DLLs

Volker Stamme
Wizards & Builders GmbH


Übersicht

Diese Session beschreibt die Erstellung und Verwendung externer Bibliotheken in Form von DLLs, FLLs, ActiveX-Controls und COM Servern. Da es nicht darum geht, VFP-Anwendungen als COM Server zu erstellen und zugänglich zu machen, sondern Erweiterungen zu FoxPro und anderen Sprachen zu erstellen, ist FoxPro als Programmiersprache ungeeignet und die in dieser Session beschriebenen Verfahren basieren auf der Programmierung in C++. Für das allgemeine Verständnis sind daher Kenntnisse in C / C++ empfohlen. Außerdem hat, ungeachtet der möglichen Vor- oder Nachteile gegenüber anderen C-Compilern, Visual C++ von Microsoft sicherlich die größte Verbreitung und wird deshalb bei den Code-Beispielen verwendet.

Der Vortrag gliedert sich in zwei Teilabschnitte:

Da wären als erstes einfache DLLs, die häufig dann zum Einsatz kommen, wenn Funktionen der Windows API verwendet werden sollen, die sich z.B. in FoxPro nicht direkt ansprechen lassen. Außerdem sind DLLs sehr interessant, wenn es darum geht, z.B. Berechnungen und Stringmanipulationen einer schnelleren Sprache zu überlassen. Eine Sonderform der einfachen DLLs sind sog. FLLs, die zu Ihrer Grundfunktionalität noch eine Schnittstelle zu FoxPro beinhalten. Diese Schnittstelle ermöglicht es u.a., innerhalb der FLL auf FoxPro-Funktionen zuzugreifen.

Spätestens dann, wenn externe Komponenten auch aus anderen Sprachen genutzt werden sollen, sind FLLs nicht mehr in Betracht zu ziehen, und wenn objektorientierte Funktionalitäten, wie die vollständige Einkapselung verschiedener Instanzen einer Komponente, gefordert sind, stoßen auch "normale" DLLs an ihre Grenzen. Der zweite Abschnitt beschäftigt sich deshalb mit den objektorientierten Varianten, nämlich ActiveX (OCX-) Controls und einfachen COM Servern.

Der erste Schritt – Einfache DLLs

DLLs (Dynamic Link Libraries) sind Funktionsbibliotheken, die gegenüber der statischen, also festen Einbindung von Bibliotheken drei Vorteile besitzen: Dynamisches Linken bedeutet, daß diese Bibliotheken nur bei Bedarf von einem Programm geladen und verwendet werden; werden sie nicht mehr benötigt, besteht die Möglichkeit, den entsprechenden Speicherplatz wieder freizugeben. Zum zweiten können DLLs im allgemeinen mehrfach von verschiedensten Programmen genutzt werden, was wiederum Ressourcen spart. Und drittens hat man so eine einfache Möglichkeit, bestimmte Teile einer Anwendung z.B. gegen Weiterentwicklungen oder Bugfixes auszutauschen, ohne die gesamte Anwendung zu ändern.

Das Grundgerüst für eine DLL ist sehr schnell erstellt. Hierfür stellt Microsoft einen Projektassistenten zur Verfügung, der die groben Vorarbeiten in wenigen Sekunden erledigt und die benötigten Dateien erstellt. Allerdings sind die Anforderungen hier so gering, daß man durchaus auch auf den Assistenten verzichten könnte. Der Mindestumfang sind lediglich eine C++-Sourcedatei (.CPP) und eine sogenannte Definitionsdatei  (.DEF). Im Normalfall gehört noch eine Header-Datei (.H) dazu, in der #include-Anweisungen und Konstantendefinitionen vorgenommen werden. Dies Definitionsdatei hat hierbei eine besondere Bedeutung. Zur Unterscheidung, welche der definierten Funktionen nur für den internen Gebrauch vorgesehen sind, und welche nachher tatsächlich z.B. von FoxPro genutzt werden können, werden exportierte Funktionen in dieser .DEF-Datei gesondert aufgeführt:

; Meine.def : Declares the module parameters for the DLL.

 

LIBRARY      "Meine"

DESCRIPTION  'Meine Windows Dynamic Link Library'

 

EXPORTS

   MeineErsteFunktion   @2p

   MeineZweiteFunktion  @3p

In diesem Definition File sind zwei Funktionen für den Export angegeben, MeineErsteFunktion() und MeineZweiteFunktion(). Zu beachten ist hier, daß weder Funktionsargumente noch mögliche Rückgabewerte angegeben werden. Dies muß bei der Verwendung der entsprechendenden Funktion, z.B. mit DECLARE DLL unter VFP geschehen.

Eine weitere Besonderheit der exportierten Funktionen ist die Art und Weise, in der mögliche Parameter an die Funktion übergeben werden. Z.T. aus Kompatibilitätsgründen, z. T. auch aus historischen Gründen wird bei DLLs im allgemeinen das Übergabeformat von Pascal verwendet. Daher erhalten zu exportierende Funktionen immer eine entsprechende Calling Convention, also Aufrufkonvention. In älteren Compilerversionen war die Aufrufkonvention __pascal bzw. __far __pascal zu verwenden. Inzwischen sollte stattdessen PASCAL oder noch besser WINAPI verwendet werden. Die vollständige Funktionsdeklaration sollte also lauten

<Rückgabetyp> WINAPI <Funktioname> ( <Argumentliste> )

Zulässige Variablentypen

Auf der Seite des C-Compilers gibt es keinerlei Einschränkungen, was die Rückgabe- bzw. Argumenttypen angeht. Auf FoxPro-Seite sieht es ein klein wenig anders aus. Die in der VFP-Hilfe unter dem Befehl DECLARE DLL angegebenen Typen sind wie folgt zu verstehen:

SHORT            16-bit integer

FoxPro kennt zwar keine 16-Bit-Variablen, aber die Übergabe sowohl bei den Parametern als auch bei den Rückgabewerten funktioniert dennoch reibungslos. Zu beachten ist lediglich, daß VFP genau wie immer keinerlei Prüfung vornimmt. Beispiel:

C++

short sample ( short i )

{

   return i ;

}

VFP

DECLARE SHORT sample IN meine.dll SHORT

? sample( 33333 )

Obwohl der Übergebene Wert in der C-Funktion unverändert zurückgegeben wird, lautet das Ergebnis –32203. Die Ursache hierfür liegt allerdings nicht in der C-Funktion, sondern bei FoxPro. VFP erweitert den zurückgegebenen Wert immer vorzeichenrichtig auf 32 Bit. Noch mißverständlicher wird es dann, wenn Werte außerhalb des zulässigen Bereichs übergeben werden. Da FoxPro-seitig wie gesagt keine Prüfungen durchgeführt werden, führt der Befehl

? sample( 65540 )

nicht zu einer Fehlermeldung; VFP merkt also nicht, daß die Zahl 65540 den Wertebereich einer 16-Bit-Variablen überschreitet. Die Folge ist, daß bei der Übergabe die vorderen 16 Bit einfach abgeschnitten werden und der verbleibende Teil der unteren 16 Bit zurückgegeben wird. Die Rückgabe lautet also 4.

Übrigens hat VFP sowohl in Version 5.0 als auch in Version 6.0 ein generelles Problem mit Referenzen auf 16Bit-Werte. Bei DLL-Aufrufen müssen diese FoxPro-Seitig als "INTEGER @" deklariert werden, bei ActiveX und jeder Form von COM-Server sind sie nicht zu verwenden! Die einzige Abhilfe ist hier tatsächlich eine entsprechende Änderung im Control selbst.

INTEGER          32-bit integer

LONG              32-bit long integer

Diese beiden Typen sind völlig identisch, LONG ist also auch nicht longer als INTEGER. Beide arbeiten, wie schon der Typ SHORT, vorzeichenbehaftet, unabhängig von der Deklaration in C. Beispiel:

C++

unsigned long sample2 ( unsigned long i )

{

   return i ;

}

VFP

DECLARE LONG sample2 IN meine.dll LONG

? sample2( -1 )

   -> -1

? sample2( 4000000000 )

   -> -294967296

SINGLE            32-bit floating point

DOUBLE          64-bit floating point

VFP arbeitet intern ausschließlich mit 64-Bit-Fließkommazahlen, die bei der Übergabe an die DLL ggf. automatisch in einen SINGLE umgewandelt werden. Es ist also praktisch gleichgültig, welche der beiden Möglichkeiten verwendet wird. Bei der Rückgabe ist es gewissermaßen genauso, zumindest in VFP 6.0 – gewissermaßen deshalb, weil dank eines VFP5.0-Bugs weder SINGLE noch DOUBLE Rückgabewerte FoxPro 5.0 lebend erreichen, sondern lediglich der ganzzahlige Vorkommateil.

STRING           Character string

Als STRING können nicht nur wirkliche Zeichenketten übergeben werden, sondern jede Art von Speicherbereich. Verlangt eine DLL-Funktion also z.B. die Adresse eines struct, ist dieser in VFP auch als @STRING zu deklarieren.

Einzelheiten speziell zum Aufruf von WinAPI-DLLs können Sie auch der Session D-API von Christof Lange entnehmen.

Der zweite Schritt – FLLs

Technisch gesehen ist eine FLL (FoxPro Link Library) nichts anderes als eine DLL und wird auch ähnlich gehandhabt. Wesentlicher Unterschied ist eine Schnittstelle, die es zum einen der FLL ermöglicht, auf FoxPro-Internas und Funktionen zuzugreifen, zum anderen aber auch FoxPro in die Lage versetzt, ohne gesonderte Deklarationen die enthaltenen Funktionen so zu verwenden, als seien sie fester Bestandteil von FoxPro. Für die Erstellung von FLLs werden zwei Dateien benötigt, die Datei PRO_EXT.H, die die Prototypen der Bibliotheksfunktionen und die Definitionen der verwendeten Strukturen beinhaltet und die Interface-Funktionen, die den Zugriff auf FoxPro-Internas erlauben. Diese befinden sich in der Datei WINAPIMS.LIB.

Zwei Strukturen haben hierbei besondere Bedeutung, nämlich FoxInfo und FoxTable. Aus letzterer wird eine Variable gebildet, die den Namen _FoxTable haben muß und exportiert wird.

Die Struktur FoxInfo beinhaltet eine Kurzbeschreibung aller zu exportierenden Funktionen sowie Art und Anzahl der Übergabeparameter. Der exakte Aufbau sieht in der C-Deklaration so aus:

FoxInfo

typedef struct {

   char FAR *  funcName;   // Der Funktionsname in Großbuchstaben. Das

                           // ist der Name, der in VFP verwendet wird

   FPFI        function;   // Ein Pointer auf die Funktion in C; Hier

                              // muß dementsprechend der tatsächliche Name

                              // der Funktion in C angegeben werden, der

                              // vom FoxPro-Namen abweichen kann

   short       parmCount;  // Die Anzahl der zu übergebenden Parameter.

                           // Hier wird die Maximalzahl angegeben, d.h.

                           // optionale Parameter zählen mit.

   char FAR *  parmTypes;  // Hier wird für jeden Parameter dessen Typ

                           // mit einem Großbuchstaben angegeben

} FoxInfo;

Im Normalfall wird also in dem C-Programm ein Array aus dieser Struktur erstellt. Jeder Arrayeintrag ist für eine Funktion zuständig. Die entsprechende Arrayvariable muß exportiert werden, und für den Fall, daß nicht ANSI-C sondern C++ programmiert wird, muß auch das durch die Angabe "C" angegeben werden. Ein Beispiel zur Veranschaulichung: Das Programm enthält 2 Funktionen func1 und func2. func1 erhält einen String und einen Integerwert und soll unter dem Namen StringInteger verwendet werden. func2 erhält eine logische Variable und einen optionalen Datumsausdruck und soll als "OptionalDat" erscheinen. Das Array wird dann wie folgt gebildet:

extern "C" FoxInfo MeineFunktionen[] = {

   {  "STRINGINTEGER",  (FPFI) func1,  2, "CI" },

   {  "OPTIONALDAT",    (FPFI) func2,  2, "L.D" },

} ;

Für das Element parmCount gibt es noch 3 Sonderfälle, nämlich

INTERNAL       (-1)

um anzuzeigen, daß diese Funktion in FoxPro nicht verwendet werden kann. Diese Einstellung macht allerdings praktisch nie Sinn und wird dementsprechend häufig verwendet.

CALLONLOAD     (-2)

bedeutet, daß die hier angegebene Funktion automatisch ausgeführt wird, wenn die FLL unter FoxPro mittels SET LIBRARY TO eingebunden wird.

CALLONUNLOAD   (-3)

gibt schließlich analog zu CALLONLOAD an, daß diese Funktion immer dann automatisch durchlaufen wird, wenn die FLL wieder freigegeben wird.

Das Element parmTypes läßt beliebige Kombinationen der nachfolgenden Zeichen zu:

""    Kein Parameter, nur bei parmCount = 0
"?"      Gibt an, daß der Typ des Parameters Variabel ist
"C"      Zeichenvariablen
"D"      Datum (eine 64Bit Fließkommazahl)
"I"      32 Bit Integer
"L"      Logisch
"N"      Numerisch (Fließkommazahl)
"R"      Referenz, entspricht dem "@" bei DECLARE DLL
"T"      DateTime
"Y"      Currency (Fließkommazahl)
"O"      Objekt

Optionale Parameter werden durch einen führenden Punkt gekennzeichnet.

Die zweite wichtige Struktur heißt FoxTable und enthält normalerweise nur einen Zeiger und eine Beschreibung des FoxInfo-Arrays. Normalerweise deshalb, weil auch die Verknüpfung weiterer Libraries gestattet ist. Sollte eine solche Verknüpfung verwendet werden, ist diese im ersten Element der Strukur einzutragen.

FoxTable

typedef struct _FoxTable {

   struct _FoxTable FAR *nextLibrary;

                           // Dieser Eintrag ist wie gesagt für weitere

                           // verknüpfte Listen zu verwenden und wird im

                           // Normalfall auf 0 gesetzt.

   short infoCount;        // Die Anzahl der in FoxInfo eingetragenen

                           // Funktionen. Diese Anzahl wird i.d.R. mit

                           // sizeof() Makro abgebildet

   FoxInfo FAR *infoPtr;   // Und schlußendlich ein Zeiger auf das Array

} FoxTable;

Wie bereits erwähnt erhält die Variable für diese Strukur den fest definierten Namen _FoxTable. Damit sieht, das Array "MeineFunktionen" aus dem obigen Beispiel vorausgesetzt, die Deklaration wie folgt aus:

extern "C" FoxTable _FoxTable = {

   ( FoxTable FAR * ) 0,

   sizeof( MeineFunktionen ) / sizeof( FoxInfo ),

   MeineFunktionen

};

Funktionen und Parameterübergabe

Im folgenden sind mit "FLL-Funktionen" die Funktionen gemeint, die exportiert werden. Im Unterschied dazu sind mit "interne Funktionen" alle anderen gemeint.

Damit eine FLL-Funktion Rückgabewerte an FoxPro liefern kann, stellt die FoxPro-API spezielle Funktionen zur Verfügung, die innerhalb der FLL-Funktionen verwendet werden können. D.h. die FLL-Funktionen selbst haben keinen Rückgabewert und werden deshalb als void far deklariert. Ähnlich verhält es sich mit der Parameterübergabe. Auch hier wird nicht der übliche Weg gewählt, sondern es werden alle Variablen in Form einer Struktur übergeben. Damit erhalten FLL-Funktionen nur einen einzigen Parameter, einen Zeiger auf eine Struktur vom Typ

ParamBlk

typedef struct

{

   short       pCount;  // die Anzahl Parameter, die übergeben wurden

   Parameter   p[1];    // ein Array, daß für jeden Parameter ein

                        // Element vom Typ Parameter enthält

} ParamBlk;

Damit lautet die Deklaration einer FLL-Funktion

void far MeineFunktion( ParamBlk far * fpa )

Die erwähnte Struktur Parameter wiederum hat folgenden Aufbau:

Parameter

typedef union

{

   Value    val;     // wenn der Parameter als Wert übergeben wurde

   Locator  loc;     // wenn der Parameter als Referenz übergeben wurde

} Parameter;

Da es sich hier um einen Union-Typ handelt, ist entweder ein Value oder ein Locator enthalten. Das Element Value wird verwendet, wenn der Parameter als Wert übergeben wurde, bei einer Referenz ist der Locator ausschlaggebend.

Die Strukturen Value und Locator haben folgenden Aufbau:

Die Struktur "Value"

typedef struct {

   char           ev_type;       // Typ des Parameters

   char           ev_padding;   

   short          ev_width;

   unsigned       ev_length;

   long           ev_long;

   double         ev_real;

   CCY            ev_currency;

   MHANDLE        ev_handle;

   unsigned long  ev_object;

} Value;

Je nachdem, welcher Variablentyp übergeben wurde, haben die einzelnen Elemente unterschiedliche Bedeutung:

Datentyp in ev_type Element Bedeutung

"C" Zeichen

ev_length

ev_handle

Länge der Zeichenfolge

Speicherhandle vom Typ MHANDLE

"N" Numerisch

ev_width

ev_length

ev_real

Vorkommastellen für die Anzeige

Nachkommastellen für die Anzeige

Der eigentliche Wert (double)

"I" Integer

ev_width

ev_long

Stellenzahl für die Anzeige

Der eigentliche Wert (long)

"D" Datum

ev_real

Das Datum im Vorkommateil der Zahl 1)

"T" DatumUhrzeit

ev_real

Das Datum im Vorkommateil, die Uhrzeit im Nachkommateil 2)

"Y" Währung

ev_width

ev_currency

Stellenzahl für die Anzeige

der eigentliche Wert 3)

"L" Logisch

ev_length

0 oder 1 für FALSE oder TRUE 4)

"M" Memo

ev_width

ev_long

ev_real

FCHAN (Filehandle der Memodatei)

Länge des Memofeldes

Versatz innerhalb der Memo-Datei

"G" General

ev_width

ev_long

ev_real

FCHAN (Filehandle der Datei)

Länge des Objektfeldes

Versatz innerhalb der Datei

"O" Objekt

ev_object

Zeiger auf das Objekt 5)

"0" NULL

ev_long

Enthält den Datentyp 6)

Hinweise:

  1. Laut Dokumentation ist das Datum als Julianische Tageszahl formuliert, die eine doppeltgenaue Gleitkommazahl (double) ist und mit dem Algorithmus 199 aus Collected Algorithms des ACM berechnet wurde. Der Vorkommateil sollte sinnigerweise mit einer Bezugskonstanten verglichen werden, z.B. der Entsprechung für den 1. Januar 1600, was einem Wert von 2305448 entspricht.
  2. Die Uhrzeit ist bei einem Timestamp in den Nachkommateil eincodiert. Da die Auflösung eine Sekunde beträgt und ein Tag 86.400 Sekunden hat, wird die Uhrzeit in 1/86.400 Sekunden angegeben. Beispielsweise ist 9:30 morgens = 9.5 * 3600 = 34.200 Sekunden nach Mitternacht, der Nachkommateil beträgt also 34.200 / 86.400.
  3. Währungen werden als 64 Bit Integer (LARGE_INTEGER) abgebildet, wobei die generellen 4 Nachkommastellen nicht bitcodiert, sondert auf den dezimalen Wert bezogen angegeben werden. So steht z.B. in den unteren 32 Bit des Betrages 13,2500 die Zahl 132500. Das macht diese Zahlen zwar lesbar, aber um Berechnungen durchführen zu können, muß erst in einen Double gewandelt werden.
  4. Sicherheitshalber sollte man sich nicht darauf verlassen, daß ein logisches TRUE (.T.) immer durch eine 1 repräsentiert wird. Grundsätzlich gilt die C-Definition, daß 0 den logischen Wert FALSE hat und alles andere, also z.B. auch 4711 oder -3, als TRUE zu betrachten ist.
  5. Bis einschließlich Version 5.0 von Visual FoxPro ist der Datentyp "O"bjekt schlicht unbrauchbar, da erstens nicht, wie in der Dokumentation beschrieben, ein Pointer auf ein Objekt übergeben wird, sondern irgendwelche einfachen Integerwerte wie 0 oder 1, und zweitens fehlen entsprechende Funktionen, um mit diesen Objekten zu arbeiten. In VFP 6.0 wurde dieses Problem gelöst.
  6. In VFP behält eine Variable nach der Zuweisung von NULL ihren ursprünglichen Typ. Dieser Typ kann hier abgelesen werden.

Die Struktur "Locator"

typedef struct

{

   char     l_type;     // Typ des Wertes wie in der Value Struktur

   short    l_where,    // gibt entweder den Arbeitsbereich an, wenn

                        // der Wert in einer Tabelle steht, oder -1,

                        // wenn es sich um eine Speichervariable handelt

            l_NTI,      // "Name Table Index"

            l_offset,   // Bei Tabellenfeldern die Feldnummer

            l_subs,     // 0 bei einfachen Varialben

                        // Bei Array ist hier zu erkennen, ob sie

                        // 1 - oder 2 - dimenstional sind

            l_sub1,     // Größe des Arrays in der 1. Dimension; nur

                        // gültig, wenn l_subs >= 1 ist

            l_sub2;     // Größe des Arrays in der 2. Dimension; nur

                        // gültig, wenn l_subs = 2 ist

} Locator;

Ob ein Parameter als Wert oder als Referenz übergeben wurde, kann dem jeweils ersten Element beider Strukturen Value und Locator entnommen werden. Dieses Element ist ein einzelnes Zeichen und enhält bei einer Referenz den Großbuchstaben "R". Ob also der erste Parameter ein Wert ist, kann folgendermaßen festgestellt werden:

Art des ersten Parameters:

void far MeineFunktion( ParamBlk far * fpa )

{

   if ( fpa->p[0].val.ev_type == 'R';  // Referenz

   // alternativ geht auch

   if ( fpa->p[0].loc.l_type != 'R';   // keine Referenz

}

Normalerweise wird ein Wert als Value übergeben, d.h. die FLL erhält eine Kopie. Soll der Originalwert verändert werden, muß stattdessen eine Referenz übergeben werden. In dem Moment kommt dieser ominöse Name Table Index zum tragen. Angenommen, die FLL-Funktion "Runden" soll die numerische Variable "UNRUND" bearbeiten, dann würde das etwa so aussehen:

VFP

Runden( @unrund )

C++

void far runden( ParamBlk far * fpa )

{

   // Achtung: diese Funktion muß in der FoxInfo-Struktur mit dem

   // Parametertyp "R" deklariert werden!

   Locator loc ;

   Value val ;

   if ( _FindVar( fpa->p[0].loc.l_NTI, // der Name Table Index

                  -1,                  // Speichervariable

                  &loc))               // und der Locator für's Ergebnis

   {

      if ( loc.l_type == 'N' )         // prüfen ob numerisch

      {

      _Load( &loc, &var )           // den Wert lesen

      var.ev_real = ....            // irgendeine Rundungsfunktion

      _Store( &loc, &var )          // Ergebnis zurückschreiben

      }

   }

}

Weitere Beispiele können den API-Dokumentationen zu VFP 5.0 / 6.0 entnommen werden.

Der dritte Schritt - ActiveX-Controls

Bis jetzt wurde nur von reinen Funktionsbibliotheken gesprochen. Um den objektorientierten Aspekt mit hinein zu bringen, gibt es weitere Möglichkeiten. Zuerst zum bekanntesten Vertreter dieser Gattung.

Ursprünglich hießen sie OLE-Controls oder von der Dateiendung abgeleitet einfach "OCXe", aber nachdem sie Web-tauglich gemacht wurden, brauchte das Kind einen neuen Namen: ActiveX. Der Einsatz in HTML-Seiten ist nicht Gegenstand dieses Vortrages, aber da sie bereits vielfach als weitgehend sprachunabhängige, vielseitig verwendbare Komponenten eingesetzt werden, sollen sie etwas genauer betrachtet werden.

Ein ActiveX-Control, oder genauer gesagt die dazugehörige .OCX-Datei, ist quasi als Klasse zu verstehen, von der praktisch beliebig viele, voneinander unabhängige Instanzen, also Objekte, erstellt werden können. Bei der Verwendung solcher Klassen wird im Prinzip genauso vorgegangen wie bei jedem anderen VFP Objekt auch, d.h. mittels AddObject. Da die Klassendefinition allerdings nicht von VFP stammt, muß diese Klasse vom Betriebssystem registriert sein. Diese Registrierung wird normalerweise bei der Installation eines OCX automatisch vorgenommen, kann aber auch jederzeit von Hand nachvollzogen werden. Hierfür stellen Windows 95 und NT jeweils ein Programm zur Verfügung namens REGSVR32.EXE.

Im Rahmen dieser Registrierung kommt eine sogenannte ClassID, kurz CLSID,  zum Tragen, mit der die notwendigen Einstellungen zu dem ActiveX-Control ermittelt werden können. Wie dieser Mechanismus im einzelnen funktioniert, soll am Beispiel des Toolbar-OCX erläutert werden.

CLSID

Eine Liste der registrierten Controls ist in der Windows Registry unter HKEY_CLASSES_ROOT abgelegt. Bei der Auswahl eines Controls wird der Klassenname mit übergeben, der z.B. bei AddObject zu verwenden ist. In diesem Beispiel lautet der Name

COMCTL.Toolbar

COMCTL ist hierbei der Klassenname innerhalb der .OCX-Datei, die alle enthaltenen Controls zusammenfaßt; anders gesagt kann jedes .OCX mehrere Controls enthalten. Beispielsweise ist auch das Slider-Control Bestandteil der COMCTL und kann dementsprechend über COMCTL.Slider erreicht werden. Diesen Eintrag kann man nun, wie gesagt unter HKEY_CLASSES_ROOT, in der Windows Registry suchen. Der Schlüssel enthält einen weiteren Schlüssel CLSID und möglicherweise unter CurVer einen Verweis auf ein anderes Control, falls das gerade angewählte nicht der aktuelle Stand ist. Da Folgeversionen die gleiche CLSID verwenden, ist es praktisch problemlos möglich, ein Versionsupdate des Controls einzuspielen, ohne alle Anwendungen, die das Control verwenden, explizit von dem Update in Kenntnis setzen zu müssen. Nun zu der CLSID selbst. Jede Klasse erhält in Form dieser CLSID eine eindeutige Kennung. Diese ID wird bei der Neuerstellung eines ActiveX-Controls vom Compiler erzeugt und setzt sich im wesentlichen aus Zufallswerten zusammen. Um sicherzustellen, daß keine ID von keinem Compiler weltweit zweimal vergeben wird, ist sie ein furchtbar langes Zeichenkonglomerat. Die ID des erwähnten Toolbar-Controls lautet

{612A8624-0FB3-11CE-8747-524153480004} 

Diese ID selbst ist wieder als Schlüssel in der Registry vorhanden der alle benötigten Informationen, wie z.B. den OCX-Dateinamen, enthält.

Wie man ein ActiveX-Control erstellt

In den beiden vorangegangenen Kapiteln wurden keine besonderen Funktionen des Compilers verwendet, da ein einfaches CPP-File genügt hat. Lediglich für FLLs war die Einbindung der Bibliothek WINAPIMS.LIB notwendig, der Ausschluß von MSVCRT.LIB und die Einstellung der Compileroption "Multithreaded DLL" notwendig. Mit der Verwendung von COM werden die Programme allerdings ungleich umfangreicher und man sollte zumindest das Grundgerüst tunlichst einem Assistenten überlassen. Bei der Erstellung eines neuen ActiveX-Controls gehen Sie wie folgt vor: Unter Datei/Neu erscheint ein Dialog für die Neuanlage von Dokumenten. In diesem Dialog wählen Sie in der Karteikarte "Projekte" den "MFC ActiveX Steuerelement Assistenten" aus, geben dem neuen Projekt einen passenden Namen und drücken die OK-Taste. Der folgende Dialog stellt von oben nach unten folgende Fragen zu dem Projekt:

Wie viele Steuerelemente soll Ihr Projekt haben?

Wie am Beispiel des COMCTL.OCX bereits erwähnt kann eine OCX-Datei mehrere Controls enthalten. Für unsere Zwecke belassen Sie diese Einstellung bei 1.

Sollen die Steuerelemente in diesem Projekt eine Laufzeitlizenz haben?

Die erwähnte Laufzeitlizenz findet sich in einer .LIC-Datei wieder, die zwar vorhanden sein muß, aber lediglich einen Copyright-Vermerk enthält. Die Datei stellt also keinen echten Kopierschutz dar und ist für den ersten Versuch sicherlich mehr als überflüssig.

Sollen Kommentare in Quellcodedateien erzeugt werden?

Im wesentlichen enthalten diese Kommentare nützliche Hinweise, was z.B. ein bestimmter Code-Abschnitt enthält und wo von Ihnen Code zu ergänzen ist. Zumindest in der Anfangsphase können diese Kommentare sehr hilfreich sein und bleiben deshalb eingeschaltet.

Sollen Hilfedateien erzeugt werden?

Für's erste sind online-Hilfetexte sicherlich nicht notwendig, außerdem wird ohnehin nur der erste Ansatz vorproduziert.

Im zweiten Dialog kann der Name nochmals nachbearbeitet werden, aber das sei hier mal unwichtig. Die weiteren Optionen sind interessanter:

Aktivieren wenn sichtbar

Sobald der unter dem ActiveX-Control liegende Container, z.B. die Form, sichtbar wird, soll das Control aktiviert werden. Die Option ist sinnvoll und bleibt erhalten.

Zur Laufzeit unsichtbar

Unser Beispiel soll nur als Demonstration von Eigenschaften, Methoden und Ereignissen dienen und hat vorerst keinerlei grafische Ausgabe. Das heißt, daß es zur Laufzeit nicht sichtbar sein soll und deshalb wird diese Option eingeschaltet.

Im Dialogfeld "Objekt einfügen" verfügbar

Unser Control soll genau wie alle anderen OCXe jederzeit unter VFP mittels dieses Dialoges einsetzbar sein, deshalb wird die Option eingeschaltet.

Verfügt über ein Dialogfeld "Info"

Diese Option erstellt eine Dialogressource, die in einer "AboutBox"- Methode verwendet werden kann. Alleine schon, damit man dieses Verfahren mal kennengelernt hat, sollte man die Option einschalten.

Funktion eines einfachen Frame-Steuerelements

Frame-Controls können weitere Controls beinhalten. Ein Beispiel ist die OptionGroup in VFP. Da, wie gesagt, unser Beispiel überhaupt nicht sichtbar sein soll, können wir auf solche Fähigkeiten verzichten.

Welche Fensterklasse....?

Wenn man sich beispielsweise eine eigene Combobox bauen möchte, kann man auf bereits vorhandene Klassen der MFC (Microsoft Foundation Classes) zurückgreifen und sich hier die Klasse aussuchen, die den eigenen Wünschen am nächsten kommt. Lassen Sie die Einstellung vorerst auf "(none)".

Alles notwendige wurde gemacht, clicken Sie nun auf "Fertigstellen". Sie erhalten eine Kurzinformation darüber, was der Assistent alles angestellt hat und können sich dem Control widmen. Damit Sie eine ungefähre Vorstellung darüber erhalten, was bis jetzt passiert ist, wählen Sie einfach mal im Menü "Erstellen" den Menüpunkt "<projektname>.OCX erstellen". Wenn der Erstellungsprozeß abgeschlossen ist, können Sie Ihr Control, z.B. unter VFP, bereits einsetzen, d.h. auch die Registrierung mittels REGSVR32 ist bereits erledigt. Das Control hat zwar natürlich noch keine Funktionalität außer einigen Standard-Eigenschaften, aber diesen Stand haben Sie immerhin ohne eine einzige Zeile Code erreicht.

Angenommen, Sie haben Ihr Projekt "DEMO" genannt, dann finden Sie jetzt in der Übersicht des Compilers im Reiter "Dateien" unter "Quellcodedateien" vier .CPP-Files. Da wäre zunächst eine DEMO.CPP, das ist das Grundmodul, das alle enthaltenen Controls zusammenfaßt. Im Beispiel ist das zwar nur ein Control, aber das Verfahren ist trotzdem das gleiche. Das Control selbst finden Sie dann unter DEMOCtl.CPP. Zu einem ActiveX-Control gehört normalerweise ein sog. Property Sheet, ein kleiner Dialog zum Einstellen der Eigenschaften des Controls. Den Sourcecode für diesen Eigenschaftendialog finden Sie im Programm DEMOPpg.CPP. Zum Schluß enthält das Projekt noch eine STDAFX.CPP, die normalerweise nur STDAFX.H einbindet. Diese Include-Datei wird immer dann benötigt, wenn MFC zum Einsatz kommt. Die Verwendung einer separaten Source-Datei, die nur diese Includedatei einbindet, führt dazu, daß die Includedatei vorcompiliert wird. Den Vorteil werden Sie bei jeder weiteren Compilierung merken, es geht dadurch einfach schneller.

Um dem Testcontrol etwas Leben einzuhauchen, müssen Sie sich dem Code für das Control widmen, also hier der Datei DEMOCtl.CPP.

Zu Anfang dieser Datei werden Sie, neben einigen Include-Anweisungen die Deklarationen einer Message-Map, einer Dispatch-Map, einer Event-Map und der – noch unausgefüllten – Propertypages vorfinden. Es folgen die Angaben, die Sie beim Assistenten gemacht haben, diesmal in der für das Programm korrekten Form. Blättern Sie etwas weiter nach unten, bis Sie die Definition einer Funktion vorfinden, die etwa so aussieht:

CDEMOCtrl::CDEMOCtrl()

Wie es in Klassen, die unter C++ erstellt wurden, üblich ist, hat der Default-Constructor den gleichen Namen wie die Klasse selbst. Ebenso ist der Destructor

CDEMOCtrl::~CDEMOCtrl()

handelsüblich. Es liegt also die Vermutung nahe, daß auch selbstdefinierte Methoden, Events und Eigenschaften auf diese Art deklariert und definiert werden. Und so ist es auch. Theoretisch könnte man also durch einfaches Hinzufügen von Funktionen das Control erweitern. Praktisch ist davon aber abzuraten, weil alle Elemente in insgesamt 3 Dateien korrekt vermerkt werden müssen. Zunächst wäre da die Deklaration, die in die Klassendefinition gehört und dementsprechend in DEMOCtl.H eingetragen werden muß. Zum zweiten ist natürlich die Definition in der CPP-Datei notwendig. Und drittens ist für die Programme, die das OCX einsetzen, eine Typelibrary notwendig. Für die Erstellung der Typelibrary ist eine Programmdatei zuständig, die "DEMO.odl" heißt. Da das "Ctl" im Dateinamen fehlt, ist diese Datei also für alle enthaltenen Controls gemeinsam zuständig. Da es wie gesagt für den Anfang etwas zu kompliziert ist, alle 3 Dateien korrekt zu pflegen, sollte man auch hier wieder den Assistenten verwenden. Diesmal ist der der Klassenassistent und er wird einfach mit einem rechten Mausclick im Programmcode aufgerufen.

Dieser Klassenassistent ist in 5 Karteikarten unterteilt. Die gesammelten Standardevents von Windows können in den ersten beiden dieser 5 Reiter behandelt werden, die ActiveX-Events des 4. Reiters behandeln wirklich auf die ActiveX-Technologie bezogene Events und Reiter Nummer 5 liefert ein paar Informationen zu den vorhandenen Klassen. Die für den Einstieg einzig interessante Karteikarte ist die dritte und mit "Automatisierung" übertitelt. Vielleicht fragen Sie sich jetzt, was das ganze mit Automation zu tun hat. Automation ist nichts anderes als die Verfügbarkeit eines OLE Servers (bzw. COM Servers), der von beliebigen Clients, also Anwenderprogrammen genutzt werden kann. Ein ActiveX-Control ist nichts anderes und deshalb fällt die Definition der Schnittstelle zur Außenwelt unter Automation.

In der Combobox Klassenname werden Sie zwei Klassen vorfinden, die des Controls und die des Eigenschaftsdialogs zu dem Control. Wählen Sie die Control-Klasse, also z.B. "DEMOCtl" aus.

Auf der rechten Seite des Klassenassistenten finden sich zwei Buttons "Eigenschaft hinzufügen" und "Methode hinzufügen".

Properties, Methoden und Events

Als erstes wollen wir eine Methode hinzufügen. Der Name für diese Methode sei Test und sie soll einen Long Integer Wert als Parameter erhalten. Die Rückgabe soll logisch sein, und zwar TRUE, wenn die übergebene Zahl positiv war, und ansonsten FALSE. Wenn Sie "Methode hinzufügen" auswählen, werden Sie als erstes nach dem externen Namen gefragt. Das ist der Name, unter dem diese Methode z.B. in VFP erscheint. Der interne Name im C-Code kann davon abweichen und wird deshalb gesondert erfaßt. Geben Sie dennoch in beiden Feldern "Test" ein. Da ein logischer Wert zurückgegeben werden soll, wählen Sie im Feld für den Rückgabetyp den Eintrag "BOOL" aus.

Die Parameterliste ist noch leer, aber in dem Moment, in dem Sie mit der Maus in die farblich markierte erste Zeile clicken, können Sie den Variablennamen und den Typ des ersten Parameters eingeben. Genauso wird mit weiteren Parametern verfahren, aber hier soll nur einer vom Typ "long" angelegt werden. Als Namen für die Variable wählen Sie z.B. "longvar". Nachdem Sie beide Dialoge mit "OK" geschlossen haben, befinden Sie sich wieder im Sourcecode, an dessen Ende nun eine Methode angehängt wurde, die folgendermaßen ausssieht:

BOOL CDEMOCtrl::Test(long longvar)

{

   // TODO: Add your dispatch handler code here

 

   return TRUE;

}

Ändern Sie die return-Anweisung in

return ( longvar >= 0 );

um, und die oben beschriebene Funktionalität ist bereits fertig ausformuliert. Was hat der Assistent außerdem noch gemacht? Den Funktionsrumpf haben Sie bereits gesehen und bearbeitet. Öffnen Sie nun die Datei "DEMOCtl.H" und sehen Sie sich die Klassendefinition an. Innerhalb dieser Klassendefinition finden Sie den Prototyp für die Methode

// Dispatch maps

   //{{AFX_DISPATCH(CDEMOCtrl)

   afx_msg BOOL Test(long longvar);

   //}}AFX_DISPATCH

Dieses "afx_msg" dient dem Klassenassistenten als Markierung und hat für die eigentliche Funktion keine Bedeutung. Und, wie bereits erwähnt, finden Sie auch in der für die spätere Type-Library zuständigen .ODL-Datei einen Eintrag für diese Methode:

// NOTE - ClassWizard will maintain method information here.

//    Use extreme caution when editing this section.

//{{AFX_ODL_METHOD(CDEMOCtrl)

[id(1)] boolean Test(long longvar);

//}}AFX_ODL_METHOD

Diese Warnungen sind übrigens ernst zu nehmen, fehlerhafte Änderungen an dieser Stelle werden nicht selten mit Abstürzen bei der Ausführung des Controls belohnt. Auch die Kommentarzeilen sollte man nicht unterschätzen, da sie zusammen mit den doppelten geschweiften Klammern in einer Microsoft-Umgebung besondere Bedeutung erlangen.

Ähnlich wie diese Methode können auch Properties hinzugefügt werden. Wählen Sie hierzu in der Karteikarte "Automation" des Klassenassistenten die Schaltfläche "Eigenschaft hinzufügen" an. Grundsätzlich werden zwei Eigenschaftsarten unterschieden. Die erste Art ist eine sogenannte "Member-Variable". Die Eigenschaft wird automatisch erstellt und erhält eine eigene Benachrichtigungsfunktion, die immer dann durchlaufen wird, wenn eine solche Eigenschaft von außen geändert wird. Wie der Name schon sagt, hat diese Funktion aber nur Mitteilungscharakter, es können also z.B. keine Änderungen verhindert werden. Aber das Prinzip der Assign- und Access-Methoden von VFP 6.0 ist auch hier verfügbar, nämlich dann, wenn man die zweite Art verwendet: Get-/Set-Methoden. Bei dieser Art wird kein Property angelegt, man muß also selbst ein geeignetes Property in der Klassendefinition eintragen. Falls Sie z.B. den Funktionsnamen für die Set-Funktion leer lassen, gibt es keine Möglichkeit mehr, die Eigenschaft zu beschreiben, d.h. es ist jetzt ein Read-Only-Property. Ebenso können natürlich auch Write-Only-Properties erstellt werden.

Der vierte Schritt - Einfache COM-Server

Und wieder ist es eine DLL, die dem Konzept zugrunde liegt. Ähnlich wie bei einem OCX gibt es eine Klasse, die quasi als "Träger" fungiert und im Prinzip beliebig viele untergeordnete Klassen. Die COM-Schnittstelle ist die gleiche wie auch bei ActiveX-Controls, wo liegt also der wesentliche Unterschied?

Es gibt zwar Möglichkeiten, ein ActiveX-Control so zu gestalten, daß ein freies Objekt erzeugt werden kann, d.h. statt das Control in eine Form einzubauen kann es auch per CreateObject() verwendet werden. Im allgemeinen haben OCXe aber eine grafische Ausgabe, die ein übergeordnetes Fenster voraussetzt, und deshalb ist dieses Verfahren unüblich.

Der zweite Unterschied kommt ebenfalls durch die Einbindung in eine Form zustande, und zwar die Möglichkeit, Methoden und Events mit eigenem Code zu füllen. Besonders bei Events ist das natürlich wichtig, denn was nützt ein Event, auf den nicht reagiert werden kann?

Dennoch sind solche Controls manchmal völlig ausreichend, wenn es z.B. darum geht, Berechnungen unter Verwendung von Properties und Methoden auszuführen. Die Erstellung ist wieder ausgesprochen einfach, nur verwenden Sie diesmal den "MFC Anwendungs-Assistenten für DLLs". Schritt 1 des Assistenten erwartet zwei wichtige Angaben:

  1. ob Sie die MFC Runtimebibliothek auf dem Zielsystem voraussetzen können (z.B. MFC42.DLL) oder ob sicherheitshalber der verwendete Teil in Ihr Control eingebaut werden soll oder ob das Control eine Erweiterung wird, die ohnehin nur zusammen mit MFC-Anwendungen benutzt wird. Letzteres wird hier nicht besprochen und die Variante der "verknüpften MFC-Bibliothek" funktioniert immer.
  2. Das Feld "Automatisierung" muß angewählt sein, denn das ist schließlich Sinn und Zweck des ganzen.

Im Gegensatz zu ActiveX-Controls wird das Inlet noch nicht automatisch erstellt, lediglich der Sammelcontainer für die eigentlichen Controls. Wählen Sie also in der einzigen CPP-Datei, die erstellt wurde, wieder den Klassenassistenten aus und wählen Sie, wieder unter "Automatisierung", die Funktion "Klasse hinzufügen". Klassen mit vorhandener TypeLibrary sind etwas komplexer, wählen Sie daher erst einmal "Neu".

Der Name der Klasse ist völlig frei wählbar und vorerst wird eine der einfachsten Basisklassen ausreichen, wählen Sie daher "CCmdTarget" als Basisklasse. Im Unteren Teil dieses Dialoges können Angaben zur Automatisierung gemacht werden. Da das Control mittels CreateObject() in FoxPro verwendet werden soll, muß hier die letzte Einstellung, "Erstellbar nach Type-ID" angewählt sein. In dem Dazugehörigen Eingabefeld können Sie nun den Namen festlegen, der bei CreateObject angegeben wird. Z.B. kann der vordere Teil des Vorschlags inclusive des Punktes weggelassen werden.

In der neuen Klasse werden nun nach dem gleichen Verfahren wie bei den ActiveX-Controls Methoden und Properties hinzugefügt.

Abschließende Hinweise

Bei der Erstellung eine ActiveX-Controls wird die .OCX-Datei automatisch registriert. Bei diesen einfachen COM-Servern muß das von Hand mittels REGSVR32.EXE gemacht werden.

Bei der Weitergabe muß darauf geachtet werden, daß COM-Server wie ActiveX-Controls bei der Installation registriert werden. Sinnvoll ist es auch, die Datei im Windows-System(32)-Verzeichnis abzulegen.  

Glossar

API

API ist die Abkürzung für Application Programming Interface und beschreibt - wie der Name vermuten läßt - eine zunächst einmal nicht näher spezifizierte Schnittstelle, die bei der Anwendungserstellung genutzt werden kann. APIs werden von den unterschiedlichsten Komponenten zur Verfügung gestellt und sollen deren Nutzung erleichtern und vorallem auch unter artverwanden Komponenten vereinheitlichen. Beispielsweise stellen die meisten ISDN-Karten für die Nutzung der verschiedenen ISDN-Dienste eine sog. Common ISDN API oder nochmals abgekürzt CAPI zur Verfügung. In diesem Vortrag ist mit API jedoch immer die Windows API gemeint, die im Grunde nichts weiter als eine Vielzahl häufig verwendeter Funktionen zugänglich macht.

Automation

Automation ist die Fähigkeit, die Objekte einer anderen Anwendung durch Programmcode zu steuern. Ein wichtiger Punkt hierbei ist die Fähigkeit eines Objektes, seine Eigenschaften, Methoden und Events mittels sog. Type Descriptions anderen Programmen zugänglich zu machen, was die absolute Grundlage für ActiveX-Controls ist.

ActiveX-Controls

ActiveX-Controls sind Objekte, die visuelle Elemente enthalten können, wie z.B. eine Treeview-Steuerung, und deren grundsätzliches Verhalten dem der VFP-eigenen Objekte gleichgesetzt werden kann. D.h. daß mit Instanzen eines ActiveX-Controls dessen (geänderte) Eigenschaften und Methoden ebenso gespeichert werden, wie das auch bei VFP-Objekten, z.B. in einer Form, der Fall ist.

COM

COM (Component Object Model) beschreibt eine von Microsoft spezifizierte Schnittstelle zum Austausch von Daten zwischen verschiedenen Programmen. COM ging aus OLE bzw. OLE2 hervor und bildet die Basis von ActiveX, COM Servern (auch bekannt als OLE Automation) und verwandten Komponenten.

DCOM

DCOM (distributed COM) bildet in erster Linie die Fähigkeit, z.B. einen COM Server auf einem Rechner im Netz zu installieren und ihn durch ein Programm auf einem anderen Recher zu steuern, also quasi OLE Automation per Fernsteuerung durchzuführen.

MFC

"Microsoft Foundation Classes", faßt praktisch alle von MS zur Verfügung gestellten Klassenbibliotheken zusammen.

Windows API

Unter der Windows API versteht man eine Reihe von Funktionsbibliotheken, die nach Themengebieten gruppiert sind. Beispielsweise gehören die meisten Datei-Funktionen wie Öffnen, Schließen oder auch die Ermittlung des System-TEMP-Verzeichnisses zu den KERNEL-Funktionen. Die USER-Bibliothek enthält z.B. die Standard-Messagebox. Standard-Dialoge wie Datei Öffnen oder Datei Drucken finden sich in der COMDLG-Bibliothek usw. Diese Bibliotheken liegen jeweils als DLL vor (z.B. KERNEL32.DLL) und einigen Hochsprachen wie C auch als .LIB-Datei zum direkten Einbinden in eigene Programme.

Zu den Standard-Bibliotheken gehören unter C die Dateien KERNEL32.LIB, USER32.LIB, GDI32.LIB, WINSPOOL.LIB, COMDLG32.LIB, ADVAPI32.LIB, SHELL32.LIB, OLE32.LIB, OLEAUT32.LIB, UUID.LIB, ODBC32.LIB UND ODBCCP32.LIB. VFP berücksichtigt bei dem Befehl DECLARE DLL und der Bibliotheksangabe Win32API allerdings nur die Dateien KERNEL32.DLL, GDI32.DLL, USER32.DLL, MPR.DLL, and ADVAPI32.DLL; alle weiteren DLLS müssen also explizit angegeben werden.