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
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.
@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.
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.