Inspeção auto de VB6 UDTs
-
23-08-2019 - |
Pergunta
Eu tenho um sentimento a resposta a esta vai ser "não é possível", mas vou dar-lhe um tiro ...
Estou na posição nada invejável de modificar um aplicativo legado VB6 com algumas melhorias. Convertendo para uma linguagem mais inteligente não é uma opção.
O aplicativo conta com uma grande coleção de tipos definidos pelo usuário para mover os dados. Gostaria de definir uma função comum que pode ter uma referência para qualquer um desses tipos e extrair os dados contidos.
No código pseudo, aqui está o que eu estou procurando:
Public Sub PrintUDT ( vData As Variant )
for each vDataMember in vData
print vDataMember.Name & ": " & vDataMember.value
next vDataMember
End Sub
Parece que esta informação deve estar disponível para algum lugar COM ... Qualquer VB6 gurus lá fora, o cuidado de tomar um tiro?
Obrigado,
Dan
Solução
Ao contrário do que já foi dito, é possível obter informações sobre o tipo de tempo de execução para a UDT está em VB6 (embora não seja um built-in recurso de linguagem). TypeLib Informação de Microsoft Object Library (tlbinf32.dll) permite-lhe inspeccionar programaticamente COM informações de tipo em tempo de execução. Você já deve ter este componente se você tiver Visual Studio instalado: para adicioná-lo a um projeto VB6 existente, vá para Project-> referências e verificar a entrada rotulado "Informações TypeLib." Note que você terá que distribuir e registrar tlbinf32.dll no programa de configuração do aplicativo.
Você pode inspecionar as instâncias da UDT usando o componente de informação TypeLib em tempo de execução, desde que os seus da UDT são declarados Public
e são definidos dentro de uma classe Public
. Isto é necessário, a fim de fazer VB6 gerar informações de tipo COM-compatível para o seu UDT de (que pode então ser enumerados com várias classes no componente de informação TypeLib). A maneira mais fácil de cumprir este requisito seria colocar todo o seu UDT de em uma classe UserTypes
público que serão compilados em uma DLL ActiveX ou EXE ActiveX.
Resumo de um exemplo de trabalho
Este exemplo contém três partes:
- Parte 1 : Criar um projeto ActiveX DLL que irá conter todas as declarações a UDT pública
- Parte 2 : Criando um método exemplo
PrintUDT
para demonstrar como você pode enumerar os campos de uma instância de UDT - Parte 3 :. Criar uma classe iterator personalizado que permite que você facilmente percorrer os campos de qualquer UDT pública e obter nomes e valores de campo
O exemplo de trabalho
Parte 1: O ActiveX DLL
Como já mencionado, você precisa fazer do seu UDT pública acessível, a fim de enumerá-las usando o componente de informação TypeLib. A única maneira de alcançar este objetivo é colocar o seu UDT de em uma classe pública dentro de um projeto ActiveX DLL ou EXE ActiveX. Outros projetos em seu aplicativo que precisa acessar seu UDT de então fazem referência a este novo componente.
Para acompanhar este exemplo, começar por criar um novo projeto ActiveX DLL e nomeá-la UDTLibrary
.
Em seguida, mudar o nome do módulo de classe Class1
(este é adicionado por padrão pelo IDE) para UserTypes
e adicionar dois tipos definidos pelo usuário para a classe, Person
e 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
Listagem 1: UserTypes.cls
atua como um recipiente para o nosso
Em seguida, mudar o Instancing propriedade para a classe UserTypes
para "2-PublicNotCreatable". Não há nenhuma razão para qualquer um para instanciar a classe UserTypes
diretamente, porque ele está simplesmente agindo como um recipiente pública para o nosso UDT de.
Finalmente, certifique-se a Project Startup Object
(em Projeto-> Propriedades ) é definida como a "(nenhum)" e compilar o projeto. Agora você deve ter um novo arquivo chamado UDTLibrary.dll
.
Parte 2: Enumerando UDT Type Information
Agora é hora de demonstrar como podemos usar Object Library TypeLib para implementar um método PrintUDT
.
Em primeiro lugar, começar por criar um novo projecto EXE padrão e chamá-lo o que quiser. Adicione uma referência para o UDTLibrary.dll
arquivo que foi criado na Parte 1. Desde que eu só quero demonstrar como isso funciona, vamos utilizar a janela Immediate para testar o código vamos escrever.
Crie um novo módulo, nomeá-lo UDTUtils
e adicione o seguinte código a ele:
'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
PrintUDT
e um método simples teste
Parte 3: Tornando-Object-Oriented
Os exemplos acima fornecem uma demonstração "rápida e suja" de como usar o TyInformações peLib Object Library para enumerar os campos de um UDT. Em um cenário do mundo real, eu provavelmente iria criar uma classe UDTMemberIterator
que lhe permitiria mais facilmente percorrer os campos da UDT, juntamente com uma função de utilidade em um módulo que cria um UDTMemberIterator
de uma determinada ocorrência UDT. Isso permitirá que você a fazer algo como o seguinte em seu código, que é muito mais próximo do pseudo-código que você postou sua pergunta:
Dim member As UDTMember 'UDTMember wraps a TLI.MemberInfo instance'
For Each member In UDTMemberIteratorFor(someUDT)
Debug.Print member.Name & " : " & member.Value
Next
Na verdade não é muito difícil de fazer isso, e podemos voltar a usar a maior parte do código da rotina PrintUDT
criado na Parte 2.
Primeiro, crie um novo projeto ActiveX e nomeie-UDTTypeInformation
ou algo similar.
Em seguida, certifique-se de que o objecto de arranque para o novo projeto é definido como "(Nenhum)".
A primeira coisa a fazer é criar uma classe de invólucro simples que irá esconder os detalhes da classe TLI.MemberInfo
de chamar código e torná-lo fácil de obter o nome e o valor do campo de um UDT. Eu chamei este UDTMember
classe. O Instancing propriedade para esta classe deve ser 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
Listagem 3: A classe UDTMember
invólucro
Agora precisamos criar uma classe de iterador, UDTMemberIterator
, que nos permitirá usar sintaxe For Each...In
do VB para iterate os campos de uma instância de UDT. A propriedade Instancing
para esta classe deve ser definida para PublicNotCreatable
(vamos definir um método de utilidade mais tarde que irá criar instâncias em nome de código de chamada).
EDIT:. (2/15/09) Eu limpo o código-se um pouco mais
'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
Listagem 4:. A classe UDTMemberIterator
Note que para fazer esta classe iterable para que For Each
pode ser usado com ele, você terá que definir certos atributos de procedimento sobre os métodos Item
e _NewEnum
(como observado nos comentários de código). Você pode alterar o procedimento atributos a partir do menu Tools (Ferramentas-> atributos de procedimento).
Finalmente, precisamos de uma função de utilidade (UDTMemberIteratorFor
no primeiro exemplo de código nesta seção) que irá criar um UDTMemberIterator
para uma instância de UDT, que pode, então, iterate com For Each
. Criar um novo módulo chamado Utils
e adicione o código a seguir:
'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
Listagem 5:. A função de utilidade UDTMemberIteratorFor
Finalmente, compilar o projeto e criar um novo projeto para testá-lo.
Em seu projet teste, adicione uma referência para o UDTTypeInformation.dll
recém-criado eo UDTLibrary.dll
criado na Parte 1 e experimentar o seguinte código em um novo módulo:
'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
Listagem 6:. Testando a classe UDTMemberIterator
Outras dicas
@ Dan,
Parece que a sua tentativa de usar RTTI de um UDT. Eu não acho que você pode realmente obter essa informação sem saber sobre o UDT antes de run-time. Para começar tente:
UDTs Entendendo
Por causa de não ter essa capacidade de reflexão. Gostaria de criar minha própria RTTI aos meus UDTs.
Para dar-lhe uma linha de base. Tente isto:
Type test
RTTI as String
a as Long
b as Long
c as Long
d as Integer
end type
Você pode escrever um utilitário que irá abrir todos os arquivos fonte e adicionar o RTTI com o nome do tipo para o UDT. Provavelmente seria melhor colocar todos os UDTs em um arquivo comum.
O RTTI seria algo como isto:
"String: Longo: Longo: Longo: Integer"
Usando a memória da UDT você pode extrair os valores.
Se você alterar todos os seus tipos de Classes. Você tem opções. A grande armadilha de mudar de um tipo para uma classe é que você tem que usar o novo Keyworld. Toda vez que há uma declaração de uma variável do tipo adicionar novos.
Em seguida, você pode usar a palavra-chave variante ou CallByName. não VB6 não tem anytype de reflexão, mas você pode fazer listas de campos válidos e teste para ver se eles estão presentes, por exemplo,
O Teste de classe tem o seguinte
Public Key As String
Public Data As String
Você pode então fazer o seguinte
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
Se você tentar usar um campo que não existe, em seguida, erro 438 será gerado. CallByName permite usar cordas para ligar para o campo e métodos de uma classe.
O que VB6 faz quando você declarar Dim como New é bastante interessante e irá minimizar consideravelmente bugs no essa conversão. Você vê este
Dim T as New Test
não for tratada exatamente o mesmo que
Dim T as Test
Set T = new Test
Por exemplo Este trabalho irá
Dim T as New Test
T.Key = "A Key"
Set T = Nothing
T.Key = "A New Key"
Isso dará um erro
Dim T as Test
Set T = New Test
T.Key = "A Key"
Set T = Nothing
T.Key = "A New Key"
A razão para isso é que nas primeiras bandeiras exemplo VB6 T de modo que a qualquer momento um membro é acessado ele verificar se o T não é nada. Se é que vai criar automaticamente uma nova instância da classe de teste e, em seguida, atribuir a variável.
No segundo exemplo VB não adiciona esse comportamento.
Na maioria projecto que rigorosamente se certificar de que vamos Dim T como teste, aparelho de T = New Test. Mas no seu caso desde que você deseja converter tipos em Classes com o mínimo de efeitos colaterais usando Dim T como New Test é o caminho a percorrer. Isso ocorre porque o Dim como New causa a variável para imitar a forma como os tipos de trabalhos mais de perto.