Session D-OCX

Erstellung von DLLs, ActiveX und einfachen COM-Servern

Volker Stamme
Wizards & Builders GmbH


Übersicht

Diese Session beschreibt die Erstellung und Verwendung externer Bibliotheken in Form von DLLs, 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.

Spätestens dann, 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 - 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 dritte Schritt – schon wieder ActiveX

Die Erstellung von FLLs (FoxPro Link Library) ist nicht mehr Bestandteil dieser Session. Trotzdem soll die Anbindung an die Internas von FoxPro nicht völlig außen vor bleiben. Grundsätzlich kann man nämlich auch eine Mischung der klassischen FLL und eines ActiveX-Controls bauen. Die Vorteile sind klar. Von Seite des ActiveX-Controls besteht die Möglichkeit, die Innereien von FoxPro direkt zu bearbeiten, z.B. Tabellenwerte schreiben, FoxPro-Variablen Werte zuweisen oder komplexe Berechnungen aus Geschwindigkeitsgründen in C vorzunehmen, das Ergebnis aber in einem FoxPro-Array abzulegen. Dafür könnte man eine dieser ominösen FLLs konstruieren, an sich eine einfache DLL mit ein paar zusätzlichen Strukturen. Aber dieser Form fehlt eine ganz entscheidene Fähigkeit - sie ist, zumindest aus Sicht von FoxPro, nicht objektorientiert. Deshalb beschreibt dieser Teil, was innerhalb des ActiveX-Controls benötigt wird, damit dieses etwas enger als gewöhnlich mit FoxPro zusammenarbeiten kann.

Für die Erstellung solcher Controls 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 OCXAPI.LIB.

Funktionen und Parameterübergabe

Im Gegensatz zu FLLs ist es nicht notwendig, die Funktionen eines ActiveX-Controls, die FoxPro nutzen darf, gesondert zu deklarieren. Das ist durch die COM-Schnittstelle ohnehin schon gewährleistet. Wichtiger ist also, daß das ActiveX-Control an FoxPro-Internas kommt. Alles, was die Funktionen in der OCXAPI..LIB dazu benötigen, ist ein "Instanz-Handle" des aufrufenden Prozesses, in unserem Fall FoxPro.

Das geschieht auf recht einfache Art und Weise. Ein ActiveX-Control hat, wie jedes Objekt in C++, genau einen Destructor und einen Constructor. Letztere entsprechen dem Init-Event eines VFP-Objektes, ersterer analog dem Destroy. Zur Herstellung und Trennung einer Verbindung zu FoxPro stellt die OCXAPI eine Funktion zur Verfügung:

_OCXAPI( )

Diese Funktion erwartet zwei Parameter. Der erste gibt das eben erwähnte Instanz-Handle von FoxPro an und der zweite klärt, ob die Verbindung auf- oder abgebaut werden soll. Woher kriegt man jetzt dieses vermaledeite Handle? Dafür stellen die Klassenbibliotheken von Microsoft eine Funktion zur Verfügung:

AfxGetInstanceHandle( )

Parameter bekommt diese Funktion nicht, und der Rückgabewert ist entweder

Da ein OCX alleine nicht überlebensfähig ist, tritt hier normalerweise der 2. Fall ein - Handle des Aufrufers, also der VFP-Anwendung. Bleibt noch zu klären, ob die Verbindung auf- oder abgebaut werden soll. Dafür gibt es zwei entsprechende Konstanten

DLL_PROCESS_ATTACH

zum Aufbauen und

DLL_PROCESS_DETACH

zum Abbauen der Verbindung. Im Constructor müßte demnach stehen

_OCXAPI( AfxGetInstanceHandle(), DLL_PROCESS_ATTACH );

was soviel heißt wie "habt Euch lieb, ob Ihr wollt oder nicht".  Das würde reichen, aber es geht noch weiter. Denn _OCXAPI() hat auch noch einen Rückgabewert. Dieser ist logisch und false, wenn die OCXAPI.LIB unter dem angegebenen Handle irgendwas, bloß nicht VFP gefunden hat. Der Aufruf im Constructor könnte demnach auch so aussehen:

if( ! _OCXAPI( AfxGetInstanceHandle(), DLL_PROCESS_ATTACH ))

{

   MessageBox( "Da versucht doch wieder einer, mit Visual Basic " \

               "mein schönes OCX zu nutzen. Ts, ts, ts." );

   delete this;   // soooo nicht.

}

Schutz vor Mißbrauch ist also eingebaut. Im Destructor ist der Rückgabewert allerdings belanglos, dort kann man sich diesen if- Konstrukt schenken.

Wie gesagt, will man aus VFP heraus Methoden oder Eigenschaften des OCX ansprechen, werden diese auf OCX-Seite dafür nicht besonders vorbereitet - das ist Standard-COM-Funktionalität. Wenn man aus dem OCX Zugriff auf eine in FoxPro definierte Variable benötigt, sind 2 Strukturen wichtig, nämlich Value und Locator. Der Grund dafür ist hauptsächlich, daß Variablen in VFP jeden beliebigen Typ annehmen können, ohne entsprechend deklariert worden zu sein. Deshalb muß es zu jeder Variablen Strukturen geben, die über den momentanen Inhalt, den Typ etc. Auskunft geben. Außerdem muß bei VFP immer separat geklärt werden, ob es eine Speichervariable oder z.B. ein Tabellenfeld ist. Letzteres wird über die Locator-Struktur abgebildet, die Typabhängigen Beschreibungen finden sich in Value.

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, um welchen Variablentyp es sich handelt, 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;

In den Begleitdateien können Sie einige Beispiele finden, wie diese Strukturen zum Einsatz kommen.

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.