Spyworks 6.0

Veröffentlicht: 27. Mai 2001 | Aktualisiert: 15. Jun 2004

Von Torsten Zimmermann

* * *

Auf dieser Seite

  

Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:

Bild01

Abbildung 1: Mit den SpyWorks COM-Hooks können Sie den Inhalt von Eigenschaften-Comboboxen dynamisch festlegen.

Es gibt Tools, die zehren von den Nachteilen einer bestimmten Programmiersprache. Dann wieder gibt es Tools, die sind vollkommen unabhängig davon eine große Unterstützung. Seit den Anfangstagen von Visual Basic gehört SpyWorks von Desaware zu beiden Kategorien. Ein Blick auf die aktuelle Version soll herausfinden, ob das auch heute noch gilt.

Vor einigen Jahren habe ich an dieser Stelle bereits über SpyWorks geschrieben [1]. Damals war, vielleicht erinnern Sie sich noch daran, Visual Basic sehr weit entfernt von dem, was wir heute darunter verstehen. SpyWorks war die ideale Ergänzung; es eröffnete dem VB-Entwickler Welten, die zuvor ausschließlich C-Programmierern vorbehalten waren. Mit SpyWorks konnte man Fensternachrichten abfangen, Hooks programmieren und vieles mehr, was mit systemnaher Programmierung zu tun hatte. Darüber hinaus war SpyWorks eine wahre Fundgrube für Lernbegierige, die sich über die Tiefen der API-Programmierung informieren wollten. Schließlich kam SpyWorks aus dem Hause Desaware, dessen Chef Dan Appleman wir inzwischen, sozusagen persönlich, durch seine Vorträge auf der BASTA! kennen.

Mit der weiteren Entwicklung von Visual Basic, namentlich der Einführung des AddressOf-Operators, rückte die Kernfunktionalität von SpyWorks jedoch immer mehr in den Hintergrund. Zwar war es in Fragen Stabilität und Komfort kaum zu schlagen, aber zumindest theoretisch kann sich heute jeder VB-Entwickler sein eigenes Subclassing-Steuerelement schreiben. Bedeutet das aber das Ende von SpyWorks? Mit dieser Frage konfrontiert, nahm ich die aktuelle Version unter die Lupe.

Das Wichtigste vorab: SpyWorks besteht noch immer zu einem wesentlichen Teil aus sicherem Subclassing und der einfachen Entwicklung von Hooks. In diesem Bereich hat sich nur wenig geändert, wenn man einmal davon absieht, dass es natürlich an die aktuellen Visual Basic-Versionen angepasst wurde. Viel wichtiger schien mir daher ein Blick auf die wirklichen Neuerungen im SpyWorks-Paket. Sie beschäftigen sich ausführlich mit COM, aber auch mit Dingen, die auf der Wunschliste der VB-Entwickler schon immer ganz oben standen. Aus diesem Bereich habe ich mir zwei Leistungsmerkmale herausgesucht, die ich persönlich besonders interessant fand: COM-Hooks und die Möglichkeit, Funktionen aus ActiveX-DLLs zu exportieren, d.h. mit VB "echte C-API-DLLs" zu schreiben.

COM-Hooks

Die Hilfestellungen, die SpyWorks unter dem Begriff COM-Hooks zusammenfasst, eröffnen Visual Basic Türen in die COM-Welt, die ihm bisher verschlossen waren. Im Kern geht es dabei um die Bedienung von COM-Interfaces, die VB nicht ohne Weiteres implementieren kann.

Hilfe für Collection-Objekte
Wer sich bereits intensiv mit VB-Collections beschäftigt hat, wird recht schnell zu der Erkenntnis gekommen sein, dass diese nach dem Alles-oder-Nichts-Prinzip funktionieren. Das wird besonders deutlich, wenn in Collection-Objekten z.B. Datenbankzugriffe gekapselt und die Datensätze in einer For Each-Schleife durchlaufen werden sollen.

For Each ruft intern die Methode _NewEnum auf, wobei nicht der Name, sondern die DispatchID ausschlaggebend ist (sie muss auf -4 gesetzt sein). Normalerweise bzw. in einfachen Fällen liefert _NewEnum dann eine Instanz eines "echten" Collection-Objekts zurück. Genauer: _NewEnum muss ein Objekt zurückliefern, das das COM-Interface IEnumVariant implementiert [2]. Über dieses Interface greift dann For Each auf die einzelnen Elemente des Collection-Objekts zu.

Ist das zurückgelieferte Collection-Objekt tatsächlich eine Collection, so funktioniert For Each natürlich nur, wenn dieser Collection zuvor die zu durchlaufenden Daten hinzugefügt wurden. Mitunter ein recht aufwendiges Verfahren, das auch viel Zeit und damit Performance kostet. Andererseits bietet ein For Each-Aufruf aber einigen Komfort, weil er generisch ist und man sich nicht um die vorherige Abfrage einer Count-Eigenschaft zu kümmern braucht.

Einen Ausweg aus dieser Situation bietet eigentlich nur die Implementierung von IEnum-Variant in Ihrer eigenen VB-Collection-Klasse. Das funktioniert jedoch nicht so einfach, wie [2] erklärt, da das IEnumVariant-Interface einige Eigenheiten hat, die es VB unmöglich machen, es zu implementieren. Das hat auch Desaware erkannt und bietet mit SpyWorks eine einfache Lösung:

Der erste Schritt besteht darin, dem Projekt einen Verweis auf die "Desaware ActiveX Extension Library" hinzuzufügen. Danach implementiert man in der eigenen Collection-Klasse (z.B. die, die den DB-Zugriff kapseln soll) die SpyWorks-eigene Schnittstelle IdwEnumCallback und mit ihr die Methoden Clone, GetNext, Reset und Skip. Folgender Programmcode zeigt das veränderte Verhalten der neuen _NewEnum-Methode:

Public Function NewEnum() As IUnknown 
   Dim enumobj As New dwEnumerator 
   Set NewEnum = enumobj.GetEnumerator(Me) 
   enumobj.Cookie = 0 
End Function

Es muss ein neues Objekt der Klasse dwEnumerator erstellt werden, mit der eigenen Klasseninstanz initialisiert und anschließend zurückgeliefert werden.

Interessant ist dabei die Eigenschaft Cookie. Da ja für jeden Eintrag in der For Each-Schleife ein Aufruf an die Klasse erfolgt, ist es wichtig, dass der Index des aktuell abzufragenden Elements bekannt ist. Deshalb wird bei der Initialisierung dieser Wert auf 0 gesetzt. Das folgende Programmfragment zeigt die Methode des Collection-Objekts, die innerhalb der For Each-Schleife für jedes Element aufgerufen wird:

Private Sub IdwEnumCallback_GetNext(NextItem As Variant, ByVal ThisEnum As 
 DWAXEXT.IdwEnumerator) 
   Dim curval As Long 
   curval = ThisEnum.Cookie 
   If curval < m_Elements Then 
  NextItem = m_LongArray(curval) 
  curval = curval + 1 
  ThisEnum.Cookie = curval 
   End If 
End Sub

Als Parameter werden eine ByRef-Variable (NextItem) für das gewünschte Collection-Element und das in _NewEnum erzeugte dwEnumerator-Objekt hereingereicht. Dessen Eigenschaft Cookie wird als Index in die Collection-Daten interpretiert. Die liegen im Beispiel in einem Array (m_LongArray), könnten aber auch aus anderen Datenstrukturen gelesen werden.

Dass SpyWorks die Eigenschaft Cookie genannt hat, macht deutlich, wie man das Verhältnis zwischen For Each-Code und Collection-Objekt sieht: For Each ist ein "Client", der Daten von einem "Server" anfordert. Dieser Server nun muss ein "Gedächtnis" (Zustand) dafür haben, welche Daten er zuletzt geliefert hat, denn der Client fordert immer nur die "nächsten" Daten an. In einem Web-Szenario würde dieser Zustand in einem clientseitigen Cookie gespeichert.

Die weitere Gestaltung Ihres Collection-Objekts liegt ganz in Ihrer Hand. Sie können also wie gewohnt eine Item-Eigenschaft oder auch eine Add-Methode implementieren. Die Funktionalität bei Aufruf durch For Each ist davon unberührt.

Mit dieser Hilfe von SpyWorks wird es überflüssig, die Daten in eine "echte" Collection zu kopieren. Gerade Entwickler, die komplexe hierarchische Datenstrukturen in Objektmodellen kapseln, werden das zu schätzen wissen. Sie können die Element-Objekte ihrer Collection-Objekte sozusagen "virtuell" machen.

Property Browsing

Ein weiteres Beispiel aus dem Bereich COM-Hooks ist das Property Browsing (Abbildung 1). Damit bezeichnet man einen Mechanismus der ActiveX-Steuerelemente, die Bezeichnungen von Eigenschaftswerten zur Laufzeit zu erzeugen. Standardmäßig werden die Bezeichnungen über einen Aufzähler (Enum) definiert; in manchen Situationen ist es jedoch erforderlich, diese Bezeichnungen nicht statisch festzulegen. Das könnte z.B. eine Anpassung an die eingestellte Sprache sein oder aber eine Reaktion auf Zustände der Steuerelement-Umgebung.

Abbildung 1: Mit den SpyWorks COM-Hooks können Sie den Inhalt von Eigenschaften-Comboboxen dynamisch festlegen.

Um diese Dynamik auf der Seite des Steuerelements zu realisieren, setzt man zuerst einen Verweis auf die "Desaware ActiveX Extension Library". Danach implementiert man in dem Steuerelement die Schnittstelle IdwPerPropertyBrowsing und ihre Methoden und deklariert eine Variable vom Typ dwControlHook. Der Programmcode sieht danach folgendermaßen aus:

Implements IdwPerPropertyBrowsing 
Dim ControlHook As dwControlHook 
Dim Prop2Dispid As Long 
Dim m_Prop2 As Long 
Public Property Get Prop2() As Long 
Prop2 = m_Prop2 
End Property 
Public Property Let Prop2(ByVal New_Prop2 As Long) 
m_Prop2 = New_Prop2 
...  
PropertyChanged "Prop2" 
End Property 
Private Sub IdwPerPropertyBrowsing_GetDisplayString(ByVal dispID As Long, DisplayName As String) 
' 
End Sub 
Private Sub IdwPerPropertyBrowsing_GetPredefinedStrings(ByVal dispID As Long, PropertyStrings() As String) 
' 
End Sub 
Private Sub IdwPerPropertyBrowsing_ReadProperties(ByVal dispID As Long, ByVal 
StringNumber As Long, PropertyValue As Variant) 
' 
End Sub

Um die Funktionsweise des Property Browsing-Mechanismus' zu verstehen, ist es wichtig, dass die Abfrage der Eigenschaftswerte immer über die DispatchID erfolgt. Diese ID muss daher bei der Initialisierung festgehalten werden:

Private Sub UserControl_Initialize() 
Set ControlHook = New dwControlHook 
ControlHook.Initialize Me 
Prop2Dispid = ControlHook.GetDispID("Prop2", Me) 
End Sub

In diesem Beispiel sollen für die Eigenschaft Prop2 die möglichen Eigenschaftswerte dynamisch erzeugt werden.

Die weitere Kommunikation zur Realisierung der dynamischen Eigenschaftswerte erfolgt danach ausschließlich über die implementierte Schnittstelle IdwPerPropertyBrowsing. Beim ersten Schritt fragt der Container, d.h. in der Regel Visual Basic, das Steuerelement, welche Werte angezeigt werden sollen. Dafür gibt es die Interface-Methode GetPredefinedStrings:

Private Sub IdwPerPropertyBrowsing_GetPredefinedStrings(ByVal dispID As Long, PropertyStrings() As String) 
Select Case dispID 
Case Prop2Dispid 
ReDim PropertyStrings(4) 
PropertyStrings(0) = "First custom value" 
PropertyStrings(1) = "Second custom value" 
PropertyStrings(2) = "Third custom value" 
PropertyStrings(3) = "Fourth custom value" 
PropertyStrings(4) = "Fifth custom value" 
End Select 
End Sub

Zunächst wird nach der DispatchID unterschieden; wenn diese der gewünschten Eigenschaft entspricht, werden die entsprechenden Werte zurückgeliefert. Dabei darf natürlich nicht vergessen werden, das Feld im Parameter PropertyStrings entsprechend groß anzulegen. Die gelieferten Werte werden danach im Eigenschaftsfenster von Visual Basic angezeigt und dem Anwender zur Auswahl angeboten.

Wenn der Anwender aus der Liste einen Wert auswählt, fragt der Container das Steuerelement, welchem Wert diese Auswahl entspricht. Dies geschieht über die Methode ReadProperties:

Private Sub IdwPerPropertyBrowsing_ReadProperties(ByVal dispID As Long, ByVal 
 StringNumber As Long, PropertyValue As Variant) 
   Select Case Prop2Dispid 
  Case Prop2Dispid 
 Select Case StringNumber 
Case 0 
PropertyValue = 1 
Case 1 
PropertyValue = 2 
Case 2 
PropertyValue = 4 
Case 3 
PropertyValue = 8 
Case 4 
PropertyValue = 16 
 End Select 
   End Select 
End Sub

Dieser Methode wird neben der DispatchID noch der Index der Auswahl im Parameter StringNumber übergeben. Im letzten Parameter, PropertyValue, liefert das Steuerelement den eigentlichen Wert der Auswahl zurück. Dieser Wert wird dem Steuerelement dann wieder über den normalen Property Let-Aufruf zugeordnet. Der letzte Aufruf besteht darin, dass der Container das Steuerelement fragt, wie dieser Wert angezeigt werden soll:

Private Sub IdwPerPropertyBrowsing_GetDisplayString(ByVal dispID As Long, DisplayName As String) 
   If Prop2Dispid = dispID Then 
  DisplayName = "(" & m_Prop2 & ") My custom display" 
   End If 
End Sub

Es ist also möglich, zu unterscheiden zwischen den Texten, die dem Anwender zur Auswahl angeboten werden, und dem Text, der im Eigenschaftsfenster als definierter Wert erscheint.

Funktionen exportieren

Ein bislang unerfüllter Wunsch vieler VB-Entwickler: DLLs schreiben zu können, die Funktionen direkt und nicht über eine COM-Schnittstelle exportieren. Diese Fähigkeit wird benötigt, wenn Systemerweiterungen entwickelt werden müssen, z.B. für die Systemsteuerung oder als ISAPI-Erweiterungen. Für die reine Anwendungsentwicklung ist die Entwicklung solcher Bibliotheken jedoch nicht zu empfehlen, da COM-Schnittstellen deutlich mehr Sicherheit bieten und dem objektorientierten Ansatz folgen.

SpyWorks enthält nun eine Technologie, die es erlaubt, Funktionen tatsächlich aus einer ActiveX-DLL zu exportieren, so dass sie später per Declare ansprechbar sind. Doch natürlich kann auch SpyWorks keine Wunder vollbringen, und so sind die Schritte dahin ein wenig gewöhnungsbedürftig (vgl. dazu auch [3]). Das nimmt man jedoch gerne in Kauf, wenn man danach das gewünschte Ergebnis erzielt.

Am wichtigsten bei der SpyWorks-Lösung ist der Umstand, dass die Funktionen nicht von der eigentlichen VB-DLL exportiert werden, sondern von einer Alias-DLL, die die Definitionen bereitstellt. Möchte eine Anwendung eine der exportierten Funktionen nutzen, ruft sie nicht die VB-DLL auf, sondern diese Alias-DLL. Die Alias-DLL leitet dann den Aufruf entsprechend ihrer Definition weiter (s. Kasten 1).

Auch wenn das ein wenig umständlich klingt, in der Praxis ist es recht einfach umzusetzen:

  • Erstellen Sie ein neues Projekt vom Typ "ActiveX DLL".

  • Aktivieren Sie für dieses Projekt die Referenz auf "Desaware Dynamic Export Technology Interface". In dieser Typelib (dwexport.tlb) sind alle Schnittstellen enthalten, die für den Export von Funktionen notwendig sind.

  • Fügen Sie diesem Projekt ein Modul hinzu, in das Sie die Funktionen schreiben, die Sie exportieren möchten. Diese Funktionen müssen als Public deklariert sein.
    Achtung: Wenn Sie z.B. eine Systemerweiterung entwickeln, müssen Sie sich bezüglich der Parameterdefinition natürlich an das Win32-SDK halten.

  • Fügen Sie dem Projekt eine Klasse hinzu und nennen Sie sie dwExporter. Die Eigenschaft "Instancing" der Klasse muss auf "5 - MultiUse" eingestellt werden.

  • Implementieren Sie in dieser Klasse die Schnittstelle IdwDynamicExport. Über diese Schnittstelle werden Informationen über die zu exportierenden Funktionen abgefragt. Folgendes Beispiel zeigt den Programmcode für eine DLL, die eine Funktion DoSomething exportiert.

Die Alias-DLL
Mit VB erzeugte DLLs sind immer ActiveX-DLLs, die keine API-Funktionen exportieren. Wie macht es SpyWorks also, dass eine Funktion dennoch via Declare erreichbar ist?

SpyWorks erzeugt für Ihre ActiveX-DLL, die Sie gemäß den Anweisungen im Text erzeugt haben, eine kleine Proxy- oder Alias-DLL. Diese Alias-DLL binden Sie z.B. über Declare-Anweisungen in ein anderes VB-Projekt ein:

Private Function DoSomething Lib "mydllalias.dll" (ByVal msg As String) As 
 Integer

Die Alias-DLL exportiert also tatsächlich Funktionen, so wie Sie es sich wünschen. Nur tun diese Funktionen selbst nichts. Sie delegieren lediglich ihren Aufruf an Ihre ActiveX-DLL weiter.

Intern erzeugt sich die Alias-DLL beim ersten Aufruf einer Funktion eine Instanz der dwExporter-Klasse Ihrer ActiveX-DLL. Über das Objekt bekommt sie die Adressen Ihrer Funktionen, die sich nun durch die geladene DLL im Arbeitsspeicher befinden, und kann dann die Aufrufe der Alias-DLL-Funktionen dorthin weiterleiten. Jeder weitere Aufruf einer Funktion erfolgt direkt, weil dann ja die Adresse bekannt ist.

Die Desaware Export Utility ist also ein kleiner Compiler, der aus einer Funktionsdefinition, die er durch Befragen einer dwExporter-Instanz erhält, eine Alias-DLL direkt als Binärcode erzeugt. In sie kompiliert er den Namen Ihrer ActiveX-Komponente (ProgId von dwExporter darin), die Erzeugung einer Instanz und Proxyfunktionen, die ihre Aufrufe weiterleiten.

Das ist sicherlich nicht ganz einfach - aber SpyWorks vollbringt auch keine Wunder. In jedem Fall eröffnet uns diese Funktion neue Anwendungsgebiete für VB-Programme. Das gilt wahrscheinlich auch noch in Zeiten von VB7, da bisher keine Aussage darüber existiert, ob die nächste Version von Visual Basic "echte" DLLs erzeugen können wird.

Kasten 1: Wie die Alias-DLL einen Aufruf an Ihre Funktion weiterleitet.

Implements IdwDynamicExport 
Private Sub IdwDynamicExport_GetFunctionCount(FunctionCount As Long) 
FunctionCount = 1 ' es wird nur eine Funktion exportiert 
End Sub 
Private Sub IdwDynamicExport_GetFunctionInfo(ByVal FunctionNumber As Long,  
Ordinal As Integer, FunctionName As String, FunctionAddress As Long) 
Select Case FunctionNumber 
Case 1 
Ordinal = 5 
FunctionName = "DoSomething" 
FunctionAddress = GetAddress(AddressOf DoSomething) 
End Select 
End Sub 
Private Sub IdwDynamicExport_GetModuleHandle(ModuleHandle As Long) 
ModuleHandle = App.hInstance 
End Sub

Danach können Sie die DLL kompilieren und die Alias-DLL erstellen. Das dafür vorgesehene Tool heißt "Desaware Export Utility" und befindet sich im Lieferumfang von SpyWorks (Abbildung 2). Sie brauchen lediglich den Namen des VB-Projekts und den Namen der Alias-DLL angeben und können dann die DLL erstellen lassen.

Bild02

Abbildung 2: Mit der Desaware Export Utility erzeugen Sie eine Alias-DLL, über die die Funktionen in einem BAS-Modul Ihrer ActiveX-DLL aufgerufen werden.

Im Lieferumfang von SpyWorks befindet sich ein kleines Beispielprogramm, das von dieser Funktionalität Gebrauch macht. Es ist eine kleine Erweiterung der Systemsteuerung von Windows. Da zwischen Windows und der Erweiterung an dieser Stelle ausschließlich über Funktionsaufrufe kommuniziert wird (im Gegensatz zu der moderneren Variante über COM-Schnittstellen), bietet SpyWorks an dieser Stelle eine sehr schöne Möglichkeit, von einem bisher verschlossenen Mechanismus Gebrauch zu machen.

Beim Vertrieb Ihrer Komponente dürfen Sie natürlich nicht vergessen, die Alias-DLL mit auszuliefern. Bei der Installation muss dann darauf geachtet werden, dass die Alias-DLL über die standardmäßige Pfadlogik zugänglich ist, sie sich also entweder im Anwendungsverzeichnis oder in einem über die Pfad-Systemvariable befindet. Weitere DLLs brauchen nicht ausgeliefert werden, um exportierte Funktionen zu nutzen.

Fazit

SpyWorks ist auch heute noch das, was es schon immer war: eine Fundgrube für professionelle Softwareentwickler. Dies spiegelt sich nicht nur in einer ausgereiften Dokumentation wider, sondern ebenso in zahlreichen Beispielprogrammen. Nicht nur die beiden hier vorgestellten Ansätze stellen interessante Möglichkeiten des Tools dar, sondern ebenso mitgelieferte Anwendungen zum Überprüfen von Speicher und Prozesszuständen.

Wer die Funktionsweise von Visual Basic verstehen und erweitern will, hat mit SpyWorks qualifizierte Unterstützung in der Hand.

Ressourcen

[1] Torsten Zimmermann: SpyWorks-VB; BasicPro 6/94, S. 72
[2] Matthew Curland,William Storage: Extending Implements; Visual Basic Programmer's Journal, April 1997, S. 121
[3] John Chamberlain: Take Control of the Compile Process; Visual Basic Programmer's Journal, November 1999, S.

*Torsten Zimmermann ist Technologieberater bei der DataDesign AG. Nebenbei schreibt er Artikel für verschiedene Zeitschriften mit den Schwerpunktthemen VB, COM, API- und Datenbankprogrammierung. Für Fragen und Anregungen erreichen Sie ihn per E-Mail: TorstenZ@basicpro.de.*