Die meisten von Ihnen kennen sicherlich die Möglichkeit, in VB eine Eigenschaft innerhalb einer Klasse zu deklarieren, um sie per "For… Each" Schleife zu enumerieren. In Visual Basic sieht das bisher so aus: ' Über Prozedurattribute wert "-4" zuweisen Public Function NewEnum() As IUnknown Set NewEnum = MyCollection.[_NewEnum] End Function Es ist ja für einen Programmierer auch sehr komfortabel, keine extra Integer- oder Long-Variable deklarieren zu müssen, um den nächsten Wert der Klassen-Auflistung zu erhalten. Das Ganze hat nur ein riesiges Problem: es muss vorher ein Collection-Objekt initialisiert sein, sonst funktioniert es nicht. Warum? Nur das Collection-Objekt ist in der Lage, das benötigte IEnumVariant-Interface zurück zu geben. In manchen Fällen ist das auch vollkommen ok. Aber oft ist es so, dass man mit dieser Methode auf große Probleme stößt. Und zwar dann, wenn man entweder sehr große Datenmengen in das Collection-Objekt speichern muss, sehr zeitaufwendige Operationen durchführen muss, um das Collection Objekt zu füllen, oder viele Objekte in einem Collection-Objekt abgelegt werden, die wiederum Objekte (ggf. NewEnum-Eigenschaften) enthalten. Stellen Sie sich einfach mal vor, Sie programmieren eine Klasse zum Auflisten aller Dateien in einem Ordner und möchten in einer For…Each-Schleife alle Dateien durchlaufen. Dann müssen Sie bei jedem Initialisieren der Klasse und jedem Verzeichniswechsel sehr zeitaufwendige Operationen durchführen, um dieses Collection-Objekt zu füllen. Eine bessere Alternative wäre es, dynamisch das nächste benötigte For…Each-Element zu erstellen, ohne das sehr ressourcenfressende Collection-Objekt verwenden zu müssen. Und diese Methode wollen wir Ihnen heute vorstellen. Damit Sie das Verfahren besser verstehen, will ich Ihnen die Schematik erst etwas deutlicher machen. Das IEnumVariant-Interface ist eine Schnittstelleninformation des Systems und in der stdole.tlb integriert. Visual Basic implementiert diese Schnittstelle standardmäßig, die wiederum vom Collection-Objekt verwendet wird. Wenn sie "ausgeblendete Mitglieder anzeigen" in Ihrem Objektbrowser aktivieren, findet sich auch die IEnumVariant-Schnittstelle. Jetzt fragen Sie sich bestimmt, warum da oben im Code IUnknown steht und nicht IEnumVariant. Das liegt daran, dass die Schnittstelle sich von der IUnknown-Schnittstelle ableitet,
wie fast alle COM-Schnittstellen. Diese dient dazu, Objektreferenzen per Laufzeit zu erstellen. Immer wenn es in VB heißt "Set xx = New YY" wird die IUnknown-Schnittstelle aufgerufen.
Um das genauer zu verstehen, wie eine Klasse (Schnittstelle) sich von einer anderen ableiten kann, schauen Sie sich doch mal den Workshop
Um das Ganze etwas einfacher auszudrücken: VB holt sich unter vorgehaltener Hand die Schnittstelle IEnumVariant aus meiner Klasse. Da ich sie selbst nicht beeinflussen oder erstellen kann, gebe ich VB die Schnittstelle meines Collection-Objektes. Wenn Sie sich im Objektbrowser nun die Schnittstelle (bei VB heißt das Klassen) IEnumVariant anschauen, sehen Sie die vier Prozeduren:
Nun, ein findiger Programmierer denkt nicht lange nach, erstellt eine neue Klasse und leitet diese Schnittstelle über "Implements IEnumVariant" ab. Ist eigentlich auch der einzige Weg um System-Schnittstellen in VB zu implementieren. Wer dies bereits versucht hat, wird aber mit der Fehlermeldung bestraft "Diese Schnittstelle implementiert Datentypen die In Visual Basic nicht unterstützt werden". Wenn wir uns die Definition der Schnittstelle mal anschauen, wie sie in der StdOle.tlb definiert ist (z.B. in einerm C++ Header), können wir schnell herausfinden warum. In der Schnittstelle sind dort die 32-Bit Zahlen als "unsigned Long" deklariert, ganz klar, die gibt es in VB leider nicht. Diese Longwerte sind 32-Bit Ganzzahlen ohne Vorzeichen, VB unterstützt aber nur "signed Longs", also mit Vorzeichen. Hoffnungslos ? Noch lange nicht! Wie einige es schon mit dem Umgang bei den Windows-APIs kennen, kann man Parameter auch abändern, so lang man sich bei dem Datentyp an die vorgeschrieben Größe hält. So kann man statt einem Longwert auch ein 2 Felder großes Integer-Array definieren oder statt einer 16 Byte großen Struktur auch ein 16-Byte großes Array. Wichtig ist immer nur, dass man einen Datentyp definiert, der groß genug ist und die Daten, die übergeben werden, dem gewollten Datentyp gerecht entsprechen. Dann erstellen wir mal unsere eigene Definition der Schnittstelle. Wie Sie das genau machen können ist unter anderem im Workshop
[ uuid(AE5B6874-EC3E-4C16-A6E9-DFB8A2F2C580) ] library IEnumVariantVB { importlib("stdole2.tlb"); [ uuid(00020404-0000-0000-C000-000000000046), odl ] interface IEnumVARIANT : IUnknown { typedef enum NextRet { S_OK = 0, S_FALSE } NextRet; HRESULT Next([in] long celt, [in, out] VARIANT* rgvar, [in] long pceltFetched, [out, retval] long * lngRetval); HRESULT Skip([in] long celt); HRESULT Reset(); HRESULT Clone([in, out] IEnumVARIANT** ppenum); HRESULT NextReDef([in] long celt, [in, out] VARIANT* rgvar, [in] long pceltFetched, [out, retval] NextRet * lngRetval); //Added vor VB compatibility }; } Eben haben Sie gelesen, dass es nur die Prozeduren Next, Skip, Reset und Clone gibt. Warum ich einfach noch eine Prozedur eingefügt habe, die gar nicht dahin gehört, erkläre ich Ihnen später. Wenn wir nun diese Definition mit dem MDIL Compiler zu einer TypeLib umwandeln, können wir diese in unser Projekt einbinden und ohne weiteres in einer Klasse implementieren. Wir erstellen zu diesem Zweck einfach eine neue Klasse ("IEnumVariant") und fügen folgenden Code ein: Implements IEnumVariantVB.IEnumVariant Private Sub IEnumVariant_Clone(ppenum As IEnumVariantVB.IEnumVariant) ' VB benutzt diese Prozedur nicht End Sub ' celt = Anzahl der Items die angefordert werden ' rgvar = muss mit einem Variant-Array gefüllt werden mit der Anzahl ' von "celt" Feldern ' pceltFetched = muss mit der tatsächlichen Anzahl der Array-Elemente ' gefüllt werden, die verfügbar sind Private Function IEnumVariant_Next(ByVal celt As Long, _ rgvar As Variant, ByVal pceltFetched As Long) As Long Static I as Integer I = I + 1 If I < 10 then rgvar = "Element " & I IEnumVariant_Next = 0 ' Auflistung enthält weitere Elemente Else I = 0 IEnumVariant_Next = 1 ' Auflistung Ende End If End Function Private Sub IEnumVARIANT_Reset() ' Wird diese Funktion aufgerufen, wird erwartet dass beim nächsten ' Aufruf von "Next" die Auflistung von vorn ausgegeben wird.</font> End Sub ' celt = Anzahl der Elemente die übersprungen werden sollen Private Sub IEnumVARIANT_Skip(ByVal celt As Long) ' Wird diese Funktion aufgerufen, wird erwartet dass beim nächsten ' Aufruf von "Next" die Auflistung von vorn ausgegeben wird. End Sub Klar, diese Version ist recht simpel und nicht 100 % konform. Aber sie erfüllt erstmal ihren Zweck. Mit dieser Klasse können Sie schon eine For…Each Schleife erstellen, die 1 - 9 Elemente ausgibt und zwar mit folgendem Code: Klasse "IEnumTest" Option Explicit Private WithEvents INewEnum As IEnumVariant Private Sub Class_Initialize() Set INewEnum = New IEnumVariant End Sub ' Prozedur ID -4 über Prozedur-Eigenschaften Public Property Get NewEnum() As IUnknown Set NewEnum = INewEnum End Property Irgendwo im Code, z. B. im Formular: Private Sub Main() Dim ITest As IEnumTest, TmpVar As Variant Set ITest = New IEnumTest For Each TmpVar In ITest Debug.Print TmpVar Next End Sub Toll, wir können For…Each ohne Collection nutzen. Aber sicher erhalten Sie so wie ich, immer nach dem 2ten Schleifendurchlauf die Fehlermeldung "For Schleife konnte nicht initialisiert werden". Warum das so ist, liegt an VB und der Art und Weise, wie VB COM-Objekte anspricht. Aber das ist noch kein Grund aufzugeben. Nachdem wir festgestellt haben, dass es wohl an unserer Next-Implementation liegt, müssen wir uns eine Alternative überlegen. Da fällt mir das VTable-Mapping ein. Kurze Erklärung zu COM und VTables. COM-Objekte sind eine Ansammlung von öffentlichen Funktionen und Eigenschaften. Wird so ein COM-Objekt erstellt, so werden alle Funktionen / Propertys in eine feste Reihenfolge gebracht. Das legt unter anderem die TypeLib eines COM-Objekts fest. Wird ein COM-Objekt nun in den Speicher geladen, so wird eine Tabelle angelegt (VTable) in der die Adressen aller Funktionen / Propertys abgelegt werden und zwar genau in der Reihenfolge, wie sie definiert sind. VB macht das mit unseren Klassenmodulen genau so, leider wird dies aber erst beim kompilieren festgelegt. Und wenn man seine Komponente, ohne vorher eine Binär-kompatible Version festzulegen, neu kompiliert, kann die Reihenfolge auch wieder anders sein. Es sei denn, man benutzt eine TypeLib um die Reihenfolge festzulegen. Wir wollen nun also einen VTable Eintrag mappen. Das bedeutet, nachdem unser Objekt erstellt worden ist, überschreiben wir einen Eintrag in der bereits von VB erstellten VTable, durch eine andere Prozeduradresse. Dadurch erreichen wir, dass jedes Mal, wenn VB versucht eine Prozedur unserer Klasse anzusprechen, eine andere, von uns festgelegte, Prozedur aufgerufen wird. Ziel ist es, durch dieses Verfahren, die Probleme die VB mit unserer IEnumVariant-Implementation hat zu beseitigen. Nun, wohin mappen wir die Next-Prozedur. Das geht nur in ein öffentliches Modul. Damit wir den VB Aufruf aber wieder an unsere Klasse leiten können, müssen wir in unserer Klasse eine zusätzliche Prozedur schreiben. Damit wir aber nicht in Schwierigkeiten kommen, dass VB unsere VTable nicht so anlegt wie wir es wollen, dürfen wir nicht einfach eine Prozedur hineinschreiben. Aus diesem Grund habe ich in der Definition des IEnumVariant-Interfaces auch die erwähnte NextReDef-Prozedur eingefügt. Sie gehört eigentlich nicht dazu, aber wir erreichen dadurch, dass diese Prozedur von VB in der VTable an Position 5 abgelegt wird. Das mappen von VTable-Einträgen lässt sich mit folgendem Code bewerkstelligen: Public Declare Sub CopyMemory Lib "kernel32.dll" _ Alias "RtlMoveMemory" ( _ ByRef Destination As Any, _ ByRef Source As Any, _ ByVal Length As Long) Private Declare Function VirtualProtect Lib "kernel32.dll" ( _ ByVal lpAddress As Long, _ ByVal dwSize As Long, _ ByVal flNewProtect As VirtualAllocPageFlags, _ ByRef lpflOldProtect As VirtualAllocPageFlags) As Long Private Enum VirtualAllocPageFlags PAGE_EXECUTE = &H10 PAGE_EXECUTE_READ = &H20 PAGE_EXECUTE_READWRITE = &H40 PAGE_EXECUTE_WRITECOPY = &H80 PAGE_NOACCESS = &H1 PAGE_READONLY = &H2 PAGE_READWRITE = &H4 PAGE_WRITECOPY = &H8 PAGE_GUARD = &H100 PAGE_NOCACHE = &H200 PAGE_WRITECOMBINE = &H400 End Enum Public Function MapVTable(ByVal ptrObject As Long, _ ByVal vIndex As Long, ByVal ptrNewProc As Long) As Long Dim VTblOffset As Long Dim VTblPtr As Long DIm OldPageProtect As VirtualAllocPageFlags ' VTable Pointer ermitteln CopyMemory VTblPtr, ByVal ptrObject, 4 ' Offset der gewünschten Prozedur ermitteln VTblOffset = VTblPtr + (vIndex * 4) ' Lese/Schreibzugriff sichern VirtualProtect VTblOffset, 4, PAGE_EXECUTE_READWRITE, OldPageProtect ' Original Eintrag zurückgeben CopyMemory MapVTable, ByVal VTblOffset, 4 ' Eintrag ersetzen CopyMemory ByVal VTblOffset, ptrNewProc, 4 ' Originalsicherung wiederherstellen VirtualProtect VTblOffset, 4, OldPageProtect, OldPageProtect End Function ' VB gibt uns zusätzlich einen Pointer zu dem IEnumVariant-Objekt, ' wird in Klassenmodulen verborgen, da die Klasse selbst über "Me" ' angesprochen werden kann. Public Function IEnumVariantNext_Mapped(ByVal ptrMe As Long, _ ByVal celt As Long, _ rgvar As Variant, _ ByVal pceltFetched As Long) As Long Dim objMe As IEnumVariantVB.IEnumVariant ' Referenz des IEnumVariant-Objektes kopieren und NextReDef aufrufen CopyMemory objMe, ptrMe, 4 IEnumVariantNext_Mapped = objMe.NextReDef(celt, rgvar, pceltFetched) CopyMemory objMe, 0&, 4 End Function Da wir nun die Next-Prozedur auf die Prozedur "NextReDef" unserer Klasse mappen möchten, ändert sich der Code unserer Klasse "IEnumVariant" wie folgt: Option Explicit Implements IEnumVariantVB.IEnumVARIANT Private ptrOldVTableEntry As Long Private Sub Class_Initialize() ptrOldVTableEntry = MapVTable(ObjtPtrMe, 3, AddressOf IEnumVariantNext_Mapped) End Sub Private Sub Class_Terminate() Call MapVTable(ObjtPtrMe, 3, ptrOldVTableEntry) End Sub Private Function ObjtPtrMe() As Long Dim TmpMeObj As IEnumVariantVB.IEnumVARIANT Set TmpMeObj = Me ObjtPtrMe = ObjPtr(TmpMeObj) End Function Private Sub IEnumVariant_Clone(ppenum As IEnumVariantVB.IEnumVARIANT) ' VB nutzt diese Prozedur nicht End Sub Private Function IEnumVariant_Next(ByVal celt As Long, _ rgvar As Variant, _ ByVal pceltFetched As Long) As Long ' Diese Funktion wird umgeleitet End Function Private Function IEnumVariant_NextReDef( _ ByVal celt As Long, _ rgvar As Variant, _ ByVal pceltFetched As Long) As IEnumVariantVB.NextRet Static I As Integer I = I + 1 If I < 10 Then rgvar = "Element " & I IEnumVariant_Next = 0 ' Auflistung enthält weitere Elemente Else I = 0 IEnumVariant_Next = 1 ' Auflistung Ende End If End Function Private Sub IEnumVARIANT_Reset() ' Wird diese Funktion aufgerufen, wird erwartet, dass beim ' nächsten Aufruf von "Next" die Auflistung von vorn ausgegeben wird. End Sub Private Sub IEnumVARIANT_Skip(ByVal celt As Long) ' Wird diese Funktion aufgerufen, wird erwartet, dass beim nächsten ' Aufruf von "Next" die Auflistung von "celt"-Einträge übersprungen wird. End Sub Nun funktioniert unsere For…Each-Schleife auch perfekt, selbst ohne Collection. Dass man nicht nur Integers von 1 bis 9 in der Schleife durchlaufen kann, zeigt Ihnen dazu unser Beispielprojekt. Außerdem wird Ihnen dort noch gezeigt, wie es durch das Hinzufügen eines Ereignisses in der Klasse IEnumVariant unglaublich vereinfacht wird, For...Each-Elemente vorzubereiten. Dieser Workshop wurde bereits 15.501 mal aufgerufen.
Anzeige
![]() ![]() ![]() (einschl. Beispielprojekt!) Ein absolutes Muss - Geballtes Wissen aus mehr als 8 Jahren vb@rchiv! - nahezu alle Tipps & Tricks und Workshops mit Beispielprojekten - Symbol-Galerie mit mehr als 3.200 Icons im modernen Look Weitere Infos - 4 Entwickler-Vollversionen (u.a. sevFTP für .NET), Online-Update-Funktion u.v.m. |
sevAniGif (VB/VBA) ![]() Anzeigen von animierten GIF-Dateien Ab sofort lassen sich auch unter VB6 und VBA (Access ab Version 2000) animierte GIF-Grafiken anzeigen und abspielen, die entweder lokal auf dem System oder auf einem Webserver gespeichert sind. Tipp des Monats ![]() Dieter Otter sevTabStrip: Rechtsklick auf Reiter erkennen Eine Funktion, mit der sich prüfen lässt, auf welchen Tab-Reiter ein Mausklick erfolgte Access-Tools Vol.1 ![]() Über 400 MByte Inhalt Mehr als 250 Access-Beispiele, 25 Add-Ins und ActiveX-Komponenten, 16 VB-Projekt inkl. Source, mehr als 320 Tipps & Tricks für Access und VB |
|||||||||||||||||||||
Microsoft, Windows und Visual Basic sind entweder eingetragene Marken oder Marken der Microsoft Corporation in den USA und/oder anderen Ländern. Weitere auf dieser Homepage aufgeführten Produkt- und Firmennamen können geschützte Marken ihrer jeweiligen Inhaber sein. |