Zur Hauptseite ... Zum Onlinearchiv ... Zum Abonnement ... Zum Newsletter ... Zu den Tools ... Zum Impressum ... Zum Login ...

Achtung: Dies ist nicht der vollständige Artikel, sondern nur ein paar Seiten davon. Wenn Sie hier nicht erfahren, was Sie wissen möchten, finden Sie am Ende Informationen darüber, wie Sie den ganzen Artikel lesen können.

Kompletten Artikel lesen?

Einfach für den Newsletter anmelden, dann lesen Sie schon in einer Minute den kompletten Artikel und erhalten die Beispieldatenbanken.

Bitte teilen Sie uns Ihre Anrede, Ihren Namen und Ihre E-Mail-Adresse mit:

Anrede:
Vorname:
Nachname:
E-Mail:

Gedrucktes Heft

Diesen Beitrag finden Sie in Ausgabe 4/2018.

Unser Angebot für Sie!

Lesen Sie diesen Beitrag und 500 andere sofort im Onlinearchiv, und erhalten Sie alle zwei Monate brandheißes Access-Know-how auf 72 gedruckten Seiten! Plus attraktive Präsente, zum Beispiel das bald erscheinende Buch 'Access 2010 - Das Grundlagenbuch für Entwickler'!

Diesen Beitrag twittern

ActiveX und COM ohne Registrierung verwenden

ActiveX-Komponenten müssen, so sie unter VBA ansprechbar sein sollen, im System registriert werden. Dies erfordert administrative Rechte unter Windows, weshalb viele Administratoren solche externen Komponenten leider gar nicht gern sehen. Dass Sie manche ActiveX-Dateien jedoch auch ganz ohne Registrierung verwenden können, zeigen die Techniken dieses Beitrags.

VBA und die COM-Registry

Es gibt zunächst unter VBA zwei Möglichkeiten, um ein Objekt einer ActiveX-Komponente zu erzeugen. Die eine nutzt die Methode CreateObject, der Sie die sogenannte ProgID übergeben und das Resultat einer Objektvariablen zuweisen. Bei dieser kann es sich um den allgemeinen Typ Object handeln. Dennoch lassen sich die Funktionen des Objekts namentlich über den Punkt-Operator ansprechen, obwohl Object selbst ja keinerlei Methoden aufweist. Hier verwenden Sie Late Binding.

Late deshalb, weil Windows hier den Methodennamen erst beim Aufruf auswerten muss, um an den Funktionszeiger zu gelangen, der schließlich angesprungen wird. Dieser Vorgang benötigt einige Zeit, weshalb Late Binding die Performance erheblich herabsetzt. Dafür aber brauchen Sie immerhin keinen Verweis auf die Komponente zu setzen!

Anders bei der zweiten Methode, die sich des Operators New bedient. Nach dem Schreiben dieses Ausdrucks in eine VBA-Routine listet IntelliSense gleich alle Klassen auf, die sich damit instanziieren lassen. Dabei kann VBA aber nur auf jene zugreifen, die es in den Verweisen des aktuellen VBA-Projekts findet. Demzufolge muss auf die gefragte ActiveX-Komponente ein Verweis gesetzt sein. VBA kennt dann aus dieser Type Library den genauen Aufbau des Objekts, weshalb Sie es es einer Objektvariablen des dezidierten Klassentyps zuweisen können. Nun haben Sie Early Binding vor sich. Technisch bedeutet dies, dass VBA nun bereits beim Kompilieren die Funktionszeiger ermitteln kann und eben diese in das PCode-Kompilat einsetzt. Das steigert die Performance ungemein.

Doch beiden Methoden zum Erzeugen des Objekts ist eines gemein: Sie funktionieren nicht ohne die Registrierung der ActiveX-Komponente unter Windows. Der Verweis auf eine Komponente macht es nötig, dass deren Type Library (Bibliothek) und deren Interfaces (Klassen) in den Zweigen HKEY_CLASSES_ROOT und den Unterschlüsseln typelib, interface und clsid eingetragen sind. Für CreateObject braucht es zusätzlich Einträge unter HKEY_CLASSES_ROOT direkt, wobei die ProgID selbst den Schlüssel stellt und wiederum auf eine zugehörige clsid verweist.

Beispiel: Sie möchten den Ordnerpfad zum Windows-Desktop ermitteln. Dafür können Sie etwa die Shell-Bibliothek verwenden. Sie wählen diese in der Liste der Verweise unter dem Eintrag Microsoft Shell Controls And Automation aus. Die Bibliothek zeigt sich dann im Objektkatalog unter der Rubrik Shell32. Mit diesem Code-Schnipsel erhalten Sie dann den Pfad:

Dim oShell As Shell32.Shell
Set oShell = New Shell32.Shell
Debug.Print oShell.Namespace(0&).Self.Path

Ohne VBA-Verweis auf die Shell-Bibliothek geht es mit Late Binding auch so:

Dim oShell As Object
Set oShell = CreateObject("Shell.Application")
Debug.Print oShell.Namespace(0&).Self.Path

Was passiert hier? VBA sucht in der Registry unter HKEY_CLASSES_ROOT und dem Schlüssel Shell.Application. Dort wertet es den Unterschlüssel CLSID aus, der die GUID {13709620-C279-11CE-A49E-444553540000} zurückgibt. Im Folgenden schreitet es zum Zweig HKEY_CLASSES_ROOT\clsid und sucht dort nach dieser GUID. Es wird fündig und liest nun den Unterschlüssel InProcServer32 aus, der auf die ActiveX-Datei %SystemRoot%\system32\shell32.dll verweist. Die kann VBA nun theoretisch schon laden und das Objekt über die GUID und die allen COM-Komponenten eigene API-Funktion DLLGetClassObject erzeugen. Allerdings weiß es dann noch nichts über das Objekt selbst. Dazu muss es einen zweiten Unterschlüssel Typelib auswerten, der die GUID zur Shell-Bibliothek zurückgibt. Diese Bibliothek muss es nun ebenfalls laden, um dort nach der Klasse mit der zuvor ermittelten CLSID zu suchen. Aus HKCR\TypeLib\\1.0\0\win32 erfährt es, dass die Bibliothek sich konkret unter C:\Windows\SysWOW64\shell32.dll befindet. In der geladenen Bibliothek klappert es nun die sogenannten CoClasses ab, die in ihr enthalten sind. Das sind all jene, die sich als Objekt neu erzeugen lassen.

Es findet die zur CLSID gehörige Klasse und erfährt nun erst, welches Standard-Interface ihr eigen ist. Dabei sollte es sich ab Windows 7 um IShellDispatch5 oder IShellDispatch6 handeln. Nun nimmt es sich die Definition dieses Interfaces vor und kennt damit dessen Methoden. Das Interface selbst holt es sich über einen Aufruf der Standard-COM-Methode QueryInterface auf das zuvor per DLLGetClassObject erzeugte Objekt. Erst jetzt kann VBA das Objekt einer Variablen zuweisen.

Sie sehen, dass VBA unter der Haube allerlei zu bewältigen hat, wenn es um Late Binding geht. Dabei ist der Vorgang in Wirklichkeit noch deutlich komplizierter, als geschildert. Nachvollziehbar: die Performance geht dabei in die Knie.

Doch die beiden Methoden zum Erzeugen von Objekten sind ja eigentlich nicht Thema dieses Beitrags. Die Erläuterungen sind aber für das Verständnis der weiteren Ausführungen nützlich.

ActiveX-Objekte per API erstellen

Der Sachverhalt ist jener: Es gibt nur wenig, was Sie unter C++ realisieren können, was sich nicht auch unter Visual Basic 6 oder VBA umsetzen ließe! Der Aufwand dafür allerdings ist ungleich höher. Denn mit normalem VBA-Code kommen Sie nicht weiter. Gerade dann, wenn es um COM geht, benötigen Sie in der Regel wenigstens eine Bibliothek, wie die oleexp.tlb, welche die Definition der grundlegenden Interfaces enthält. Wir stellten diese Bibliothek bereits vor (Shortlink 1096) und werden weitere Beiträge zum Umgang mit ihr veröffentlichen. Hier aber geht es ja gerade darum, die Zahl der Verweise im VBA-Projekt minimal zu halten, weshalb in der vorliegenden Lösung auf externe Bibliotheken komplett verzichtet wurde.

Zum Einsatz kommt dafür eine Handvoll standardisierter Windows-API-Funktionen und ... etwas Assembler-Code! Ja, Sie hören richtig! Tatsächlich kann mit einigen Tricks auch unter VBA Maschinen-Code ausgeführt werden. Sehr komfortabel ist das indessen nicht, aber diese Code-Teile sind in der Lösung auch nicht sonderlich umfangreich.

Untergebracht ist alles im Modul mdlCOMLoaderAsm der Demodatenbank Regless.accdb. Die Routine zum Erzeugen eines Objekts nennt sich darin GetCOMInstance. Sie übergeben ihr als Parameter einerseits den Pfad zur ActiveX-Datei – die nicht registriert sein muss! – und andererseits die ProgID des gewünschten Objekts. Alternativ können Sie statt der ProgID auch direkt die CLSID der Komponente angeben, wenn Sie diese kennen.

Wollten wir hier die genaue Funktionsweise der komplexen Prozedur erläutern, so könnte damit sicher die gesamte Ausgabe gefüllt werden. Deshalb beschränken wir uns nur auf die prinzipiellen Techniken. Ansonsten verwenden Sie das Modul einfach unbesehen und importieren es bei Bedarf in Ihre eigenen Datenbankprojekte. Es weist keine weiteren Abhängigkeiten auf und lief bisher in der vorliegenden Version in unseren eigenen Projekten anstandslos. Die Anwendung erfolgt also nach diesem Schema:

Dim O As Object
Dim sFile As String
Dim sProgIDCLSID As String
sFile = "c:\Windows\SysWOW64\shell32.dll"
sProgIDCLSID = "Shell"
Set O = GetCOMInstance(sFile, sProgIDCLSID)
Debug.Print Typename(O)   '-> IShellDispatch6
O.TueDiesUndDas ...

Die Objektvariable O soll das Objekt aufnehmen, welches wir instanziieren möchten. In sFile steht der volle Pfad zur ActiveX-Datei die das Objekt beherbergt. sProgIDCLSID speichert die ProgID oder wahlweise die CLSID der Komponente. Hier verwenden wir die shell32.dll und in ihr die Objektklasse Shell. Damit erhalten wir über den Aufruf von GetCOMInstance dasselbe Objekt, wie mit dem eingangs vorgestellten Code. Typename gibt korrekt die Interface-Klasse IShellDispatch6 zurück. Statt TueDiesUndDas könnten Sie nun eben den Pfad zum Desktop ermitteln:

Debug.Print O.Namespace(0&).Self.Path
Set O = Nothing

Die Routine läuft schneller ab, wenn Sie statt der ProgID die CLSID einsetzen:

sProgIDCLSID = "{13709620-C279-11CE-A49E-444553540000}"

Sie finden diese in der Registry im Zweig HKCR\Shell.Application\CLSID. Die Funktion muss dann nicht erst umständlich die ProgID in die benötigte CLSID umwandeln. Des Weiteren können Sie den vollen Pfad zur Datei auch abkürzen:

sFile = "shell32.dll"

Das funktioniert in folgenden Fällen: Die Datei befindet sich entweder im Verzeichnis der Datenbank, oder im Suchpfad von Windows. Der besteht aus den Windows-Verzeichnissen und allen, die in der Umgebungsvariablen PATH abgelegt sind.

Dass das Ganze funktioniert, können Sie übrigens mit der Funktion GetDesktopPath im Modul mdlTest der Demodatenbank nachvollziehen.

Einen dritten optionalen Parameter von GetCOMInstance haben wir noch unterschlagen: Steht ClearCache auf True, dann ermittelt die Routine aus einer ProgID die CLSID immer neu.

Wird False übergeben (Standard), so speichert das Modul die ermittelte CLSID beim ersten Mal ab und verwendet sie fortan bei jedem Aufruf der Funktion mit derselben ProgID.

Das beschleunigt die Sache dann deutlich. (Allerdings werden Sie die Instanziierung von Objekten auch nicht etwa fortlaufend in einer Schleife ausführen, so dass der Performance-Gewinn wohl nicht sonderlich zu Buche schlägt.)

GetCOMInstance-Funktion im Detail

Die erste Aktion besteht darin, die ActiveX-Datei zu laden. Das übernimmt an sich die API-Funktion LoadLibrary, welcher Sie den Pfad übergeben und ein Handle auf das Modul erhalten. Das aber ist nicht immer notwendig, denn die Datei kann sich bereits im Prozessraum von Access befinden. Bei der shell32.dll etwa ist dies der Fall. Access benötigt sie selbst für seine internen Funktionen. Dann aber reicht die API-Funktion GetModuleHandle aus:

hModule = GetModuleHandle(sFile)

Bei API-Funktionen ereignet sich in der Regel kein VBA-Fehler, wenn falsche Parameter übergeben wurden. Stattdessen definiert der Rückgabewert den Erfolg. Hier verhält es sich so, dass die in hModule (Long-Wert) gespeicherte Rückgabe 0 ist, wenn die Funktion fehlschlug. Dann kommt LoadLibrary zum Zug:

If hModule = 0 Then hModule = LoadLibrary(sFile)

Findet die die Funktion auch hier die Datei nicht, weil deren Pfad nicht stimmt oder sie sich nicht im Windows-Suchpfad befindet, so kommt es zum letzten Versuch:

If hModule = 0 Then 
   hModule = LoadLibrary(CurrentProject.Path & "\" & sFile)
End if

Hier wird das Datenbankverzeichnis auf Existenz der Datei befragt. Schlagen alle Methoden fehl, so löst die Routine einen VBA-Fehler aus und verlässt die Prozedur:

If hModule = 0 Then
   Err.Raise vbObjectError, , sFile & " module not loaded"
   Exit Function
End If

Wir gehen davon aus, dass die Datei erfolgreich geladen wurde. Nun geht es an den nächsten Test, der untersucht, ob die Datei ActiveX-kompatibel ist und die die API-Funktion DllGetClassObject exportiert:

pFunc = GetProcAddress(hModule, "DllGetClassObject")
If pFunc = 0 Then
     Err.Raise vbObjectError, , _
         sFile & " does not export DllGetClassObject"
     FreeLibrary hModule
     Exit Function
End If

Der API-Funktion GetProcAddress übergeben Sie das zuvor erhalten Handle (hModule) und den Namen einer Funktion. In der Long-Variablen pFunc ist nun der Funktionszeiger gespeichert. Steht hier eine Null, so unterstützt die Datei diese Funktion nicht und es kommt ebenfalls zu einer Fehlermeldung und zum Verlassen der Routine.

Anhand des Funktionszeigers kann im Folgenden die Funktion DllGetClassObject im Prinzip aufgerufen werden. Das allerdings kann VBA im Unterschied zu C++ keinesfalls! Hier kommt eine Hilfsfunktion CallPointerASM zum Einsatz, die Ihnen garantiert kryptisch vorkommen wird, weil sie auf Umwegen Assembler-Code generiert, der von der API-Funktion CallWindowProc angesprungen wird. Sie ist so deklariert:

Private Function CallPointerASM( _
     ByVal fnc As Long, ParamArray Params()) As Long
     ...
End Function

Mit dem Parameter fnc setzen Sie den Funktionszeiger und in Params() eine beliebige Anzahl von zu übergebenden Parametern, da diese Variable vom Typ ParamArray ist. Die Prozedur erzeugt nun mit API-Funktionen, wie VirtualAlloc, einen Speicherblock im RAM und bringt dort die Parameter unter. Genauer pusht es diese auf den virtuellen Stack. Abschließend gibt es diesen Speicherblock auch gleich wieder frei (VirtualFree). Zuvor jedoch wird die API-Funktion CallWindowProc zweckentfremdet. Eigentlich ist sie dafür gedacht, um die Standard-Routine eines Windows-Fensters anzuspringen. Möglicherweise kennen Sie sie deshalb schon von Subclassing-Routinen, wo Windows-Messages an eine per AddressOf erhaltene VBA-Modulfunktion geleitet werden und von dort aus weiter an die ursprüngliche Funktionsadresse über CallWindowProc, damit das Windows-Fenster weiter funktioniert, wie gedacht. Tatsächlich aber weiß Windows nicht, ob die Funktionsadresse die eines Fensters ist, oder irgendetwas Anderes. Wir verwenden stattdessen nun den eben erzeugten Speicherblock als Adresse, welcher den Assembler-Code enthält. Windows führt nun unmittelbar etwa die Funktion DllGetClassObject aus!

Es kann sein, dass Sie ähnlichen Code im Netz finden, wenn Sie nach CallPointer VB googeln. Die meisten Lösungen verwenden dabei einfach ein Byte-Array, in dem der Assembler-Code abgespeichert wird. An CallWindowProc wird dann die Adresse des Byte-Arrays geleitet. Das hat früher auch funktioniert, seit VBA 7 (Office 2010) jedoch nicht, weil nun aus Sicherheitsgründen der allen Variablen zugrundeliegende RAM-Speicher nicht mehr mit dem Attribut PAGE_EXECUTE_READWRITE ausgestattet ist, um Ungemach zu verhindern. Deshalb wurde die Routine CallPointerASM komplett neu entwickelt.

Zurück zur Prozedur GetCOMInstance! Bevor die API-Funktion DllGetClassObject in ihr angesprungen werden kann, muss erst noch die CLSID bereitstehen. Denn die API-Funktion hat folgende prototypische Definition:

Declare Function DllGetClassObject( _
	Byval CLSID As GUID, _
	ByVal IID As GUID, _
	pResultingInterface As Long) As Long

Sie benötigt also die CLSID des Objekts, welches man erhalten möchte. Außerdem muss die IID eine GUID aufweisen, die zu einem Interface gehört, welches die Funktion zurückgeben soll. Für ein Objekt reicht hier das Standard-Interface IUnknown aus, für die Instanziierung muss aber das Interface IClassFactory her, weil erst dieses die endgültige Funktion CreateInstance beherbergt.

Die Aufgabe, aus einer ProgID eine CLSID zu machen ohne dass die Klasse registriert ist (!), ist allerdings alles andere, als trivial. Die Hilfsroutine ScanCoClasses übernimmt sie. Ihre Darstellung sprengte trotz der nur etwa 40 Zeilen endgültig den Rahmen dieses Beitrags. Sie simuliert über benutzerdefinierte Typen Interfaces der Abteilung ITypelib und ITypeInfo, deren Methoden sie abermals über CallPointerASM aufruft. Dabei ermittelt sie alle CoClasses der ActiveX-Datei und deren zugehörigen CLSIDs, sowie ProgIDs. Nachdem diese in einem Array abgespeichert sind, kann die Hauptroutine dieses durchlaufen und mit der übergebenen ProgID vergleichen. Trifft ein Element zu, so haben Sie die CLSID zur ProgID. DllGetClassObject kann nun ausgeführt und eine Instanz des gewünschten Objekts über die CreateInstance-Methode von IClassFactory erhalten werden.

Das ist alles nicht so einfach. Glückwunsch, wenn Sie das Kapitel bis zu diesem Punkt durchgehalten haben! Es ist tatsächlich nur für die Hartgesottenen unter Ihnen gedacht. Für den praktischen Einsatz des Moduls ist dieses Wissen nicht notwendig.

Beispiel ActiveX-Datei zum Ähnlichkeitsvergleich

Im Download zum Beitrag finden Sie eine ActiveX-Datei mSimilarity.dll, die unter anderem Strings auf Ähnlichkeit untersuchen kann und aus eigener Feder stammt. Das ist ein Erfordernis, das gerade in Datenbanken oft benötigt wird. Aus einer Adressentabelle filtern Sie etwa jene Namen heraus, die bei falscher Schreibweise eines Suchbegriffs die ähnlichsten Treffer enthalten. Eine ähnliche Komponente kam im Beitrag Fehlertolerantes Suchen (Shortlink 733) bereits zum Einsatz. Dort ist auch der Umgang mit ihren Funktionen erschöpfend beschrieben. Die aktuelle DLL ist eine Weiterentwicklung mit mehr Methoden und verbesserter Performance. Eine bessere Alternative werden Sie wahrscheinlich vergeblich im Netz suchen... Und, bevor es wieder zu Nachfragen per E-Mail kommt: Ja, die DLL ist komplett Freeware und darf ohne weitere Hinweise in eigenen Projekten eingesetzt und weitergegeben werden, ob privat oder kommerziell. Eine kleine Angabe zum Autor mossSOFT an der einen oder anderen Stelle kann jedoch nicht schaden.

Sie registrieren die DLL über die begleitende Datei register_mSimilarity.bat, welche Sie im Administrator-Kontext ausführen. Bei Weitergabe Ihres Datenbankprojekts entfiele dieser Schritt, weil wir Klassen der DLL regless instanziieren werden. Für die Demodatenbank ist die Registrierung allerdings nötig, weil deren Routinen teilweise ihren Bibliotheksverweis ansprechen.

Hier ein Code-Schnipsel, welches die Ähnlichkeitsmethode JaroWinkler demonstriert:

Dim C As CSimilarity
Dim S1 As String, S2 As String
Set C = New CSimilarity
S1 = "Trowitzsch"
S2 = "Trowitsch"
Debug.Print C.JaroWinkler(S1,S2)   '-> 92

Die gefragte Klasse in der DLL nennt sich CSimilarity. Eine Objektinstanz auf sie landet in der Variablen C. Zwei Begriffe in S1 und S2 weisen kleine Unterschiede in der Schreibweise auf. (Die erste ist korrekt!) JaroWinkler vergleicht beide und gibt 92 als Ähnlichkeitsfaktor aus. 100 wäre der Wert für komplette Übereinstimmung. Als Limit für hinreichende Ähnlichkeit nehmen Sie übrigens einen Wert zwischen 70 und 80. Alles darunter ist eher fragwürdig.

Die Routine in Listing 1 testet nun die Performance der Methoden. Neben JaroWinkler zieht sie zusätzlich die Alternative Ratcliff heran. Die Variable n speichert jeweils den Ähnlichkeitswert. Beide Methoden werden in einer Schleife 1 Million Mal aufgerufen. Die dafür benötigte Zeit messen der VBA.Timer und die Variable T. Das Ergebnis auf meinem I7-5820-Rechner:

Sub TestSimilarity1()
     Dim C As New CSimilarity
     Dim I As Long, n As Integer
     Dim S1 As String, S2 As String
     Dim T As Single
     S1 = "Trowitzsch": S2 = "Trowitsch"
     n = C.JaroWinkler(S1, S2)
     n = C.Ratcliff(S1, S2)
     DoEvents
     T = VBA.Timer
     For I = 1 To 1000000
         n = C.JaroWinkler(S1, S2)
     Next I
     Debug.Print VBA.Timer - T, n
     T = VBA.Timer
     For I = 1 To 1000000
         n = C.Ratcliff(S1, S2)
     Next I
     Debug.Print VBA.Timer - T, n
     Set C = Nothing
End Sub

Listing 1: Dies ist der Standard-Code bei registrierter ActiveX-DLL

0,84375 s       92 
0,9414063 s     91

JaroWinkler ist also geringfügig schneller, als Ratcliff. Die zurückgegebenen Ähnlichkeitswerte sind bei beiden fast identisch. Die Performance ist auch nicht schlecht: Jede Schleife auf die Funktionen wird in weniger als einer Sekunde durchlaufen. Sie können damit also eine Million Strings pro Sekunde vergleichen. Die Dauer, die VBA für die Schleife selbst benötigt, spielt übrigens keine Rolle. Ein Test auf eine Quasi-Null-Schleife in der folgenden Form gibt hier 0,0078 ms für die Ausführungszeit aus. Das sind eine Milliarde Durchläufe pro Sekunde! VBA ist also alles andere als langsam:

Sie haben das Ende des frei verfügbaren Teils des Artikels erreicht. Lesen Sie weiter, um zu erfahren, wie Sie den vollständigen Artikel lesen und auf viele hundert weitere Artikel zugreifen können.

Sind Sie Abonnent?Jetzt einloggen ...
 

Kompletten Artikel lesen?

Einfach für den Newsletter anmelden, dann lesen Sie schon in einer Minute den kompletten Artikel und erhalten die Beispieldatenbanken.

Bitte teilen Sie uns Ihre Anrede, Ihren Namen und Ihre E-Mail-Adresse mit:

Anrede:
Vorname:
Nachname:
E-Mail:

© 2003-2018 André Minhorst Alle Rechte vorbehalten.