Auto-Inspection des UDTs VB6

j'ai le sentiment que la réponse à cette question sera "pas possible", mais je vais tenter ma chance... Je suis dans la position peu enviable de modifier une ancienne application VB6 avec quelques améliorations. La conversion à une langue plus intelligente n'est pas une option. L'application repose sur une grande collection de types définis par l'utilisateur pour déplacer les données. Je voudrais définir une fonction commune qui peut prendre une référence à l'un de ces types et d'extraire les données contenues.

En pseudo code, Voici ce que je cherche pour:

Public Sub PrintUDT ( vData As Variant )
  for each vDataMember in vData
    print vDataMember.Name & ": " & vDataMember.value 
  next vDataMember 
End Sub

il semble que cette information doit être disponible pour COM quelque part... Un gurus VB6 dehors veut tenter sa chance?

Merci,

Dan

18
demandé sur Dan 2009-02-14 01:13:22

3 réponses

contrairement à ce que d'autres ont dit, il est possible d'obtenir des informations de type run-time pour les UDT dans VB6 (bien que ce ne soit pas une fonctionnalité de langage intégrée). Microsoft Bibliothèque De Types D'Informations Bibliothèque D'Objets (tlbinf32.dll) vous permet d'inspecter programmatiquement les informations de type COM à l'exécution. Vous devriez déjà avoir ce composant si vous avez Visual Studio installé: pour l'ajouter à un projet VB6 existant, allez à Projet->Références et vérifiez l'entrée étiquetés "Bibliothèque De Types D'Informations."Notez que vous devrez distribuer et enregistrer tlbinf32.dll dans le programme d'installation de votre application.

vous pouvez inspecter les instances UDT en utilisant le composant D'Information TypeLib à l'exécution, aussi longtemps que vos UDT sont déclarés Public et sont définis dans un Public classe. Cela est nécessaire pour que VB6 génère des informations de type COM-compatible pour vos UDT (qui peuvent ensuite être énumérées avec diverses classes dans le composant D'Information TypeLib). La façon la plus facile de répondre à cette exigence serait de mettre tous vos UDT dans un public UserTypes classe qui sera compilé dans un ActiveX DLL ou EXE ActiveX.

le Résumé d'un travail exemple

cet exemple contient trois parties:

  • Partie 1: création D'un projet DLL ActiveX qui contiendra toutes les déclarations publiques UDT
  • Partie 2: Création d'un exemple PrintUDT méthode pour démontrer comment vous pouvez énumérer les champs D'une instance UDT
  • Partie 3: création d'une classe iterator personnalisée qui vous permet d'itérer facilement à travers les champs de N'importe quel UDT public et d'obtenir des noms de champs et des valeurs.

exemple

Partie 1: La DLL ActiveX

comme je l'ai déjà mentionné, vous devez rendre vos UDT accessibles au public afin de les énumérer en utilisant les informations TypeLib composant. La seule façon d'accomplir ceci est de mettre vos UDT dans une classe publique à l'intérieur D'un projet ActiveX DLL ou ActiveX EXE. D'autres projets de votre application qui ont besoin d'accéder à vos UDT feront alors référence à ce nouveau composant.

pour suivre cet exemple, commencez par créer un nouveau projet DLL ActiveX et nommez-le UDTLibrary.

suivant, renommer le Class1 class module (ajouté par défaut par L'IDE) à UserTypes et ajouter deux types définis par l'utilisateur de la classe, Person et Animal:

' UserTypes.cls '

Option Explicit

Public Type Person
    FirstName As String
    LastName As String
    BirthDate As Date
End Type

Public Type Animal
    Genus As String
    Species As String
    NumberOfLegs As Long
End Type

Liste 1:UserTypes.cls agit comme un conteneur pour nos UDT

ensuite, changez le Instanciation propriété UserTypes classe à "2-PublicNotCreatable". Il n'y a aucune raison pour quiconque d'instancier le UserTypes classe directement, parce que c'est tout simplement agir comme un public conteneur pour notre UDT.

Enfin, assurez-vous que le Project Startup Object (moins de Projet->Propriétés) est défini à" (None) " et compile le projet. Vous devriez maintenant avoir un nouveau fichier appelé UDTLibrary.dll.

Partie 2: l'Énumération de type défini par l'utilisateur des Informations de Type

maintenant il est temps de montrer comment nous pouvons utiliser la bibliothèque D'objets TypeLib pour implémenter un PrintUDT méthode.

tout d'abord, commencez par créer un nouveau projet EXE Standard et appelez-le comme vous voulez. Ajouter une référence vers le fichier UDTLibrary.dll qui a été créé dans la Partie 1. Depuis que j'ai juste pour montrer comment cela fonctionne, nous utiliserons la fenêtre immédiate pour tester le code que nous écrirons.

Créer un nouveau Module, le nom UDTUtils et ajoutez-y le code suivant:

'UDTUtils.bas'
Option Explicit    

Public Sub PrintUDT(ByVal someUDT As Variant)

    ' Make sure we have a UDT and not something else... '
    If VarType(someUDT) <> vbUserDefinedType Then
        Err.Raise 5, , "Parameter passed to PrintUDT is not an instance of a user-defined type."
    End If

    ' Get the type information for the UDT '
    ' (in COM parlance, a VB6 UDT is also known as VT_RECORD, Record, or struct...) '

    Dim ri As RecordInfo
    Set ri = TLI.TypeInfoFromRecordVariant(someUDT)

    'If something went wrong, ri will be Nothing'

    If ri Is Nothing Then
        Err.Raise 5, , "Error retrieving RecordInfo for type '" & TypeName(someUDT) & "'"
    Else

        ' Iterate through each field (member) of the UDT '
        ' and print the out the field name and value     '

        Dim member As MemberInfo
        For Each member In ri.Members

            'TLI.RecordField allows us to get/set UDT fields:                 '
            '                                                                 '
            ' * to get a fied: myVar = TLI.RecordField(someUDT, fieldName)    '
            ' * to set a field TLI.RecordField(someUDT, fieldName) = newValue ' 
            '                                                                 '
            Dim memberVal As Variant
            memberVal = TLI.RecordField(someUDT, member.Name)

            Debug.Print member.Name & " : " & memberVal

        Next

    End If

End Sub

Public Sub TestPrintUDT()

    'Create a person instance and print it out...'

    Dim p As Person

    p.FirstName = "John"
    p.LastName = "Doe"
    p.BirthDate = #1/1/1950#

    PrintUDT p

    'Create an animal instance and print it out...'

    Dim a As Animal

    a.Genus = "Canus"
    a.Species = "Familiaris"
    a.NumberOfLegs = 4

    PrintUDT a

End Sub

Liste 2: Un exemple PrintUDT méthode et une méthode d'essai simple

Partie 3: Faire de l'Orienté Objet

les exemples ci-dessus fournissent une démonstration "rapide et sale" de la façon d'utiliser la bibliothèque D'objets D'Information TypeLib pour énumérer les champs d'un type défini par l'utilisateur. Dans un scénario réel, je créerais probablement un UDTMemberIterator classe qui vous permettrait d'itérer plus facilement à travers les champs de UDT, avec une fonction utilitaire dans un module qui crée un UDTMemberIterator pour une instance UDT donnée. Cela vous permettrait de faire quelque chose comme ce qui suit dans votre code, qui est beaucoup plus proche du pseudo-code que vous avez posté dans votre question:

Dim member As UDTMember 'UDTMember wraps a TLI.MemberInfo instance'

For Each member In UDTMemberIteratorFor(someUDT)
   Debug.Print member.Name & " : " & member.Value
Next

ce n'est en fait pas trop difficile à faire, et nous pouvons réutiliser la plupart des le code de la PrintUDT routine créée dans la partie 2.

tout d'abord, créez un nouveau projet ActiveX et nommez-le UDTTypeInformation ou quelque chose de similaire.

Ensuite, assurez-vous que l'Objet de Démarrage pour le nouveau projet est "(Aucun)".

la première chose à faire est de créer une classe wrapper simple qui cache les détails du TLI.MemberInfo classe de code d'appel et de le rendre facile d'obtenir un type du nom du champ et la valeur. J'ai appelé cette classe UDTMember. Le Instanciation propriété de cette classe doit être PublicNotCreatable.

'UDTMember.cls'
Option Explicit

Private m_value As Variant
Private m_name As String

Public Property Get Value() As Variant
    Value = m_value
End Property

'Declared Friend because calling code should not be able to modify the value'
Friend Property Let Value(rhs As Variant)
    m_value = rhs
End Property

Public Property Get Name() As String
    Name = m_name
End Property

'Declared Friend because calling code should not be able to modify the value'
Friend Property Let Name(ByVal rhs As String)
    m_name = rhs
End Property

Liste 3: Le UDTMember classe d'emballage

maintenant nous devons créer une classe iterator,UDTMemberIterator, qui nous permettra d'utiliser VB For Each...In syntaxe pour itérer les champs d'une instance UDT. Instancing propriété de cette classe doit être définie à PublicNotCreatable (nous définirons plus tard une méthode utilitaire qui créera des instances au nom de l'appelant code.)

EDIT: (2/15/09) j'ai nettoyé le code un peu plus.

'UDTMemberIterator.cls'

Option Explicit

Private m_members As Collection ' Collection of UDTMember objects '


' Meant to be called only by Utils.UDTMemberIteratorFor '
'                                                       '
' Sets up the iterator by reading the type info for     '
' the passed-in UDT instance and wrapping the fields in '
' UDTMember objects                                     '

Friend Sub Initialize(ByVal someUDT As Variant)

    Set m_members = GetWrappedMembersForUDT(someUDT)

End Sub

Public Function Count() As Long

    Count = m_members.Count

End Function

' This is the default method for this class [See Tools->Procedure Attributes]   '
'                                                                               '
Public Function Item(Index As Variant) As UDTMember

    Set Item = GetWrappedUDTMember(m_members.Item(Index))

End Function

' This function returns the enumerator for this                                     '
' collection in order to support For...Each syntax.                                 '
' Its procedure ID is (-4) and marked "Hidden" [See Tools->Procedure Attributes]    '
'                                                                                   '
Public Function NewEnum() As stdole.IUnknown

    Set NewEnum = m_members.[_NewEnum]

End Function

' Returns a collection of UDTMember objects, where each element                 '
' holds the name and current value of one field from the passed-in UDT          '
'                                                                               '
Private Function GetWrappedMembersForUDT(ByVal someUDT As Variant) As Collection

    Dim collWrappedMembers As New Collection
    Dim ri As RecordInfo
    Dim member As MemberInfo
    Dim memberVal As Variant
    Dim wrappedMember As UDTMember

    ' Try to get type information for the UDT... '

    If VarType(someUDT) <> vbUserDefinedType Then
        Fail "Parameter passed to GetWrappedMembersForUDT is not an instance of a user-defined type."
    End If

    Set ri = tli.TypeInfoFromRecordVariant(someUDT)

    If ri Is Nothing Then
        Fail "Error retrieving RecordInfo for type '" & TypeName(someUDT) & "'"
    End If

    ' Wrap each UDT member in a UDTMember object... '

    For Each member In ri.Members

        Set wrappedMember = CreateWrappedUDTMember(someUDT, member)
        collWrappedMembers.Add wrappedMember, member.Name

    Next

    Set GetWrappedMembersForUDT = collWrappedMembers

End Function

' Creates a UDTMember instance from a UDT instance and a MemberInfo object  '
'                                                                           '
Private Function CreateWrappedUDTMember(ByVal someUDT As Variant, ByVal member As MemberInfo) As UDTMember

    Dim wrappedMember As UDTMember
    Set wrappedMember = New UDTMember

    With wrappedMember
        .Name = member.Name
        .Value = tli.RecordField(someUDT, member.Name)
    End With

    Set CreateWrappedUDTMember = wrappedMember

End Function

' Just a convenience method
'
Private Function Fail(ByVal message As String)

    Err.Raise 5, TypeName(Me), message

End Function

Liste 4: Le UDTMemberIterator classe.

notez que pour rendre cette classe itérable de sorte que For Each peut être utilisé avec, vous devrez définir certains attributs de procédure sur le Item et _NewEnum méthodes (tel que noté dans les commentaires du code). Vous pouvez modifier les attributs de procédure à partir du Menu Outils (Outils - > procédure Attribut.)

Enfin, nous avons besoin d'une fonction d'utilité (UDTMemberIteratorFor dans le tout premier exemple de code de cette section) qui va créer un UDTMemberIterator pour une instance UDT, que nous pouvons ensuite itérer avec For Each. Créez un nouveau module appelé Utils et ajouter le code suivant:

'Utils.bas'

Option Explicit

' Returns a UDTMemberIterator for the given UDT    '
'                                                  '
' Example Usage:                                   '
'                                                  '
' Dim member As UDTMember                          '
'                                                  '        
' For Each member In UDTMemberIteratorFor(someUDT) '
'    Debug.Print member.Name & ":" & member.Value  '
' Next                                             '
Public Function UDTMemberIteratorFor(ByVal udt As Variant) As UDTMemberIterator

    Dim iterator As New UDTMemberIterator
    iterator.Initialize udt

    Set UDTMemberIteratorFor = iterator

End Function

Liste 5: Le UDTMemberIteratorFor fonction d'utilité.

Enfin, compilez le projet et créer un nouveau projet pour le tester.

dans votre test projet, ajouter une référence au nouveau UDTTypeInformation.dll et UDTLibrary.dll créé dans la Partie 1 et à essayer le code suivant dans un nouveau module:

'Module1.bas'

Option Explicit

Public Sub TestUDTMemberIterator()

    Dim member As UDTMember

    Dim p As Person

    p.FirstName = "John"
    p.LastName = "Doe"
    p.BirthDate = #1/1/1950#

    For Each member In UDTMemberIteratorFor(p)
        Debug.Print member.Name & " : " & member.Value
    Next

    Dim a As Animal

    a.Genus = "Canus"
    a.Species = "Canine"
    a.NumberOfLegs = 4

    For Each member In UDTMemberIteratorFor(a)
        Debug.Print member.Name & " : " & member.Value
    Next

End Sub

Liste 6: Test UDTMemberIterator classe.

38
répondu Mike Spross 2009-10-31 11:57:39

@Dan,

on dirait que vous essayez D'utiliser RTTI d'un UDT. Je ne pense pas que vous pouvez vraiment obtenir cette information sans savoir à propos de L'UDT avant la course-time. Pour obtenir vous avez commencé à essayer:

Comprendre Les UDTs

Parce que de ne pas avoir cette capacité de réflexion. Je créerais mon propre RTTI pour mes UDTs.

Pour vous donner une base de référence. Essayez ceci:

Type test
    RTTI as String
    a as Long
    b as Long 
    c as Long
    d as Integer
end type

Vous pouvez écrire un utilitaire qui ouvrira chaque fichier source et ajouter le RTTI avec le nom du type à L'UDT. Il serait probablement préférable de mettre tous les UDTs dans un fichier commun.

Le RTTI serait quelque chose comme ceci:

"De La Chaîne:Long:Long:Long:Entier"

en utilisant la mémoire de L'UDT vous pouvez extraire les valeurs.

1
répondu Gutzofter 2009-02-14 19:14:27

si vous changez tous vos Types en Classes. Vous avez des options. Le grand piège de passer d'un type à une classe est que vous devez utiliser le nouveau keyworld. Chaque fois là une déclaration d'une variable de type Ajouter nouveau.

alors vous pouvez utiliser la variante mot clé ou CallByName. VB6 n'a aucun type de réflexion mais vous pouvez faire des listes de champs valides et tester pour voir s'ils sont présents par exemple

le Test de classe A les

Public Key As String
Public Data As String

vous pouvez procéder de la façon suivante

Private Sub Command1_Click()
    Dim T As New Test 'This is NOT A MISTAKE read on as to why I did this.
    T.Key = "Key"
    T.Data = "One"
    DoTest T
End Sub

Private Sub DoTest(V As Variant)
    On Error Resume Next
    Print V.Key
    Print V.Data
    Print V.DoesNotExist
    If Err.Number = 438 Then Print "Does Not Exist"
    Print CallByName(V, "Key", VbGet)
    Print CallByName(V, "Data", VbGet)
    Print CallByName(V, "DoesNotExist", VbGet)
    If Err.Number = 438 Then Print "Does Not Exist"
End Sub

Si vous tentez d'utiliser un champ qui n'existe pas erreur 438 sera soulevée. CallByName vous permet d'utiliser des chaînes d'appeler le champ et les méthodes d'une classe.

ce que VB6 fait quand vous déclarez Dim comme nouveau est très intéressant et minimisera grandement les bogues dans cette conversion. Vous voyez ce

Dim T as New Test

n'est pas traitée exactement le même que

Dim T as Test
Set T = new Test

Par exemple, ce sera

Dim T as New Test
T.Key = "A Key"
Set T = Nothing
T.Key = "A New Key"

Cela vous donnera une erreur

Dim T as Test
Set T = New Test
T.Key = "A Key"
Set T = Nothing
T.Key = "A New Key"

la raison en est que dans le premier exemple, VB6 signale T de façon à ce que chaque fois qu'un membre y accède, il vérifie si le T n'est rien. Si c'est le cas, il créera automatiquement une nouvelle instance de la classe Test et assignera ensuite la variable.

dans le second exemple, VB n'ajoute pas ce comportement.

dans la plupart des projets, nous nous assurons rigoureusement que nous faisons le Test de Dim t, mis T = nouveau Test. Mais dans votre cas puisque vous voulez convertir les Types en Classes avec le moins d'effets secondaires en utilisant faible comme nouveau Test est la voie à suivre. C'est parce que la Dim comme nouvelle cause la variable imite la façon dont les types fonctionnent plus étroitement.

1
répondu RS Conley 2009-02-16 13:02:20