Auto-inspection de VB6 UDT
-
23-08-2019 - |
Question
Je sens la réponse à cela va être « pas possible », mais je vais donner un coup de feu ...
Je suis dans la position peu enviable de modifier une application VB6 héritée avec quelques améliorations. La conversion en une langue plus intelligente est pas une option.
L'application repose sur une grande collection de types définis par l'utilisateur pour déplacer des données. Je voudrais définir une fonction commune qui peut prendre une référence à l'un de ces types et extraire les données contenues.
Dans le code pseudo, voici ce que je cherche:
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 ... Tous les gourous VB6 soins là-bas pour prendre une photo?
Merci,
Dan
La solution
Contrairement à ce que d'autres ont dit, il est possible d'obtenir la gestion du temps des informations de type pour son UDT dans VB6 (bien qu'il ne soit pas une caractéristique de langage intégré). TypeLib Informations objet bibliothèque (tlbinf32.dll) vous permet d'inspecter programme COM informations de type à l'exécution. Vous devriez déjà avoir cette composante si vous avez installé Visual Studio: pour l'ajouter à un projet VB6 existant, allez à Projet-> Références et vérifiez l'entrée intitulée « TypeLib l'information. » Notez que vous devrez distribuer et inscrire tlbinf32.dll dans le programme d'installation de votre application.
Vous pouvez consulter les instances UDT utilisant le composant d'information TypeLib au moment de l'exécution, aussi longtemps que vos UDT sont déclarés Public
et sont définies dans une classe Public
. Ceci est nécessaire afin de rendre VB6 générer des informations de type COM compatible pour votre UDT (qui peut ensuite être dénombrées à différentes classes dans le composant d'information TypeLib). La meilleure façon de répondre à cette exigence serait de mettre tous vos UDT dans une classe publique UserTypes
qui sera compilé dans un DLL ActiveX ou EXE ActiveX.
Résumé d'un exemple de travail
Cet exemple contient trois parties:
- Partie 1 : Création d'un projet DLL ActiveX qui contiendra toutes les déclarations de UDT publique
- Partie 2 : Création d'un exemple de méthode
PrintUDT
pour montrer comment vous pouvez énumérer les champs d'une instance UDT - Partie 3 :. Création d'une classe itérateur personnalisée qui vous permet d'itérer facilement à travers les champs de tout UDT public et obtenir les noms des champs et des valeurs
L'exemple de travail
Partie 1: La DLL ActiveX
Comme je l'ai déjà mentionné, vous devez rendre public accessible de votre UDT afin de les énumérer en utilisant le composant d'information TypeLib. La seule façon d'y arriver est de mettre vos UDT dans une classe publique dans un projet DLL ActiveX ou EXE ActiveX. D'autres projets dans votre application qui ont besoin d'accéder à vos UDT renverront alors ce nouveau composant.
Pour suivre cet exemple, commencez par créer un nouveau projet DLL ActiveX et nommez-le UDTLibrary
.
Ensuite, renommer le module de classe Class1
(ce qui est ajouté par défaut par l'EDI) pour UserTypes
et ajouter deux types définis par l'utilisateur à 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: agit UserTypes.cls
comme un conteneur pour notre UDT de
Ensuite, changer le instanciation propriété pour la classe UserTypes
à "2-PublicNotCreatable". Il n'y a aucune raison pour quiconque de instancier la classe UserTypes
directement, parce qu'il est tout simplement agir comme contenant du public pour nos années UDT.
Enfin, assurez-vous que le Project Startup Object
(sous Projet-> Propriétés ) est réglé sur "(Aucun)" et compiler le projet. Vous devriez maintenant avoir un nouveau fichier appelé UDTLibrary.dll
.
Partie 2: UDT Énumération Type d'information
Maintenant, il est temps de montrer comment nous pouvons utiliser TypeLib bibliothèque d'objets pour mettre en œuvre une méthode PrintUDT
.
Tout d'abord, commencez par créer un nouveau projet EXE standard et appeler ce que vous voulez. Ajoutez une référence au fichier UDTLibrary.dll
qui a été créé dans la partie 1. Comme je veux juste montrer comment cela fonctionne, nous utiliserons la fenêtre immédiate pour tester le code que nous allons écrire.
Créer un nouveau module, nommez-UDTUtils
et ajoutez le code suivant à elle:
'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 de méthode PrintUDT
et une méthode de test simple
Partie 3: Rendre Object-Oriented
Les exemples ci-dessus fournissent une démonstration « rapide et sale » de la façon d'utiliser le TypeLib information bibliothèque d'objets d'énumérer les champs d'un UDT. Dans un scénario réel, je serais probablement créer une classe UDTMemberIterator
qui vous permettra d'itérer plus facilement à travers les champs de UDT, ainsi que d'une fonction d'utilité dans un module qui crée un UDTMemberIterator
pour une instance donnée UDT. Cela vous permettra de faire quelque chose comme ce qui suit dans votre code, ce qui est beaucoup plus proche de la pseudo-code affiché 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
Il est en fait pas trop difficile de le faire, et nous pouvons réutiliser la plupart du code de la routine PrintUDT
créé 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 réglé sur « (Aucun) ».
La première chose à faire est de créer une classe simple wrapper qui permet de masquer les détails de la classe TLI.MemberInfo
d'appeler le code et le rendre facile d'obtenir le nom et la valeur de champ d'un UDT. J'ai appelé cette UDTMember
de classe. instanciation propriété pour 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: La classe wrapper UDTMember
Maintenant, nous devons créer une classe iterator, UDTMemberIterator
, qui nous permettra d'utiliser la syntaxe de For Each...In
de VB pour itérer les champs d'une instance UDT. La propriété Instancing
pour cette classe doit être réglé sur PublicNotCreatable
(nous allons définir une méthode d'utilité plus tard qui va créer des instances au nom de code d'appel).
EDIT:. (15/02/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:. La classe UDTMemberIterator
Notez que pour rendre cette classe itérables afin que For Each
peut être utilisé avec, vous devrez définir certains attributs de procédure sur les méthodes de Item
et _NewEnum
(comme il est indiqué dans les commentaires de code). Vous pouvez modifier la procédure Attributs dans le menu Outils (Outils-> Procédure Attributs).
Enfin, nous avons besoin d'une fonction d'utilité (UDTMemberIteratorFor
dans le premier exemple de code dans cette section) qui va créer un UDTMemberIterator
pour une instance UDT, que nous pouvons itérer avec For Each
. Créer un nouveau module appelé Utils
et ajoutez 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
Listing 5:. La fonction utilitaire UDTMemberIteratorFor
Enfin, compiler le projet et créer un nouveau projet pour le tester.
Dans votre projet de test, ajoutez une référence au UDTTypeInformation.dll
nouvellement créé et le UDTLibrary.dll
créé dans la partie 1 et essayez 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:. Tester la classe UDTMemberIterator
Autres conseils
@ 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 au sujet de l'UDT avant l'exécution. Pour vous aider à démarrer essayer:
Comprendre UDT
En raison de ne pas avoir cette capacité de réflexion. Je voudrais créer mon propre RTTI à mes UDT.
Pour vous donner une ligne de base. 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 tous les fichiers source et ajoutez RTTI avec le nom du type à l'UDT. Probablement serait préférable de mettre tous les UDT dans un fichier commun.
Le RTTI serait quelque chose comme ceci:
"String: Long: Long: Long: Entier"
Utilisation de la mémoire de l'UDT, vous pouvez extraire les valeurs.
Si vous changez tous vos types de classes. Vous avez des options. Le grand écueil de passer d'un type à une classe est que vous devez utiliser la nouvelle Keyworld. Chaque fois qu'il ya une déclaration d'un add variable de type nouveau.
Ensuite, vous pouvez utiliser le mot-clé de variante ou CallByName. VB6 n'a pas anyType de réflexion, mais vous pouvez faire des listes de champs valides et tester pour voir si elles sont présentes par exemple
Le test de classe a les éléments suivants
Public Key As String
Public Data As String
Vous pouvez alors effectuer les opérations suivantes
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 essayez d'utiliser un champ qui ne en erreur existe pas 438 ressusciteront. CallByName vous permet d'utiliser des chaînes pour appeler le champ et les méthodes d'une classe.
Qu'est-ce que VB6 fait quand vous déclarez Dim comme nouveau est tout à fait intéressant et réduira considérablement les bugs dans cette conversion. Vous voyez ce
Dim T as New Test
ne sont pas traités exactement le même que
Dim T as Test
Set T = new Test
Par exemple, cela fonctionne
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 est que, dans les premiers drapeaux VB6 exemple T de telle sorte que chaque fois qu'un membre est accédaient vérifier si le T est rien. Si elle est elle crée automatiquement une nouvelle instance de la classe de test, puis assigner la variable.
Dans le second exemple VB n'ajoute pas ce comportement.
Dans la plupart des projets que nous faisons avec rigueur que nous allons Dim T comme test, Set T = Nouveau test. Mais dans votre cas puisque vous voulez convertir les types en classes avec le moins d'effets secondaires en utilisant Dim T comme un nouveau test est le chemin à parcourir. En effet, le Dim comme une nouvelle cause la variable d'imiter les types de fonctionnement de plus près.