vb@rchiv
VB Classic
VB.NET
ADO.NET
VBA
C#
sevDataGrid - Gönnen Sie Ihrem SQL-Kommando diesen krönenden Abschluß!  
 vb@rchiv Quick-Search: Suche startenErweiterte Suche starten   RSS-Feeds  | Newsletter  | Impressum  | Datenschutz  | vb@rchiv CD Vol.6  | Shop Copyright ©2000-2017
 
zurück
Rubrik: OOP / Tools   |   VB-Versionen: VB601.02.07
"For...Each" individuell implementieren durch das IEnumVariant-Interface

In diesem Workshop zeigen wir eine Möglichkeit auf, wie man mit ein paar Tricks VB Klassen erstellen kann, die per For...Each-Schleife angesprochen werden können, ohne dass ein Collection-Objekt zum Einsatz kommt. Dadurch entfällt, dass man ein Collection-Objekt vorher komplett initialisieren muß…

Autor:  Matthias VolkBewertung:     [ Jetzt bewerten ]Views:  11.649 

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  Polymorphismus (Schnittstellenimplementierung) an.

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:

Next gibt die nächsten X Elemente der Auflistung zurück
Reset startet die Auflistung wieder von vorn
Skip überspringt die nächsten X Elemente der Auflistung
Clone erstellt eine Kopie der aktuellen Enumeration (VB verwendet dies nicht)

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  Erstellen von TypeLib's beschrieben. Wir nehmen uns die Original Definition der Schnittstelle und ändern sie wie folgt ab:

[
  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 11.649 mal aufgerufen.

Über diesen Workshop im Forum diskutieren
Haben Sie Fragen oder Anregungen zu diesem Workshop, können Sie gerne mit anderen darüber in unserem Forum diskutieren.

Aktuelle Diskussion anzeigen (2 Beiträge)

nach obenzurück


Anzeige

Kauftipp Unser Dauerbrenner!Diesen und auch alle anderen Workshops finden Sie auch auf unserer aktuellen vb@rchiv  Vol.6
(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.
 
   

Druckansicht Druckansicht Copyright ©2000-2017 vb@rchiv Dieter Otter
Alle Rechte vorbehalten.
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.

Diese Seiten wurden optimiert für eine Bildschirmauflösung von mind. 1280x1024 Pixel