Осмотр собственной определяемых пользователем типов данных в VB6

У меня такое чувство, что ответ на это будет "невозможно", но я дам ему шанс... Я нахожусь в незавидном положении изменения устаревшего приложения VB6 с некоторыми улучшениями. Преобразование в более умный язык не вариант. Приложение использует большую коллекцию пользовательских типов для перемещения данных. Я хотел бы определить общую функцию, которая может ссылаться на любой из этих типов и извлекать содержащиеся в них данные.
В псевдо-коде, вот что я ищу. для:

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

похоже, что эта информация должна быть доступна для COM где-то... Любые гуру VB6 там хотят сделать снимок?

спасибо,

Дэн

3 ответов


вопреки тому, что говорили другие, можно получить информацию о типе времени выполнения для UDT в VB6 (хотя это не встроенная языковая функция). Microsoft Библиотека Информационных Объектов TypeLib (tlbinf32.dll) позволяет программно проверять информацию о типе COM во время выполнения. У вас уже должен быть этот компонент, если у вас установлена Visual Studio: чтобы добавить его в существующий проект VB6, перейдите в -51-->Ссылки и проверить запись надпись " информация о типе."Обратите внимание, что вам придется распространять и регистрировать tlbinf32.dll в программе установки вашего приложения.

вы можете проверить экземпляры UDT с помощью информационного компонента TypeLib во время выполнения, если ваши UDT объявлены Public и определена в Public класса. Это необходимо для того, чтобы VB6 генерировал COM-совместимые данные типа для ваших UDT (которые затем могут быть перечислены с различными классами в информации TypeLib деталь.) Самый простой способ удовлетворить это требование - поместить все ваши UDT в публичное UserTypes класс, который будет скомпилирован в ActiveX DLL или ActiveX EXE.

резюме рабочего примера

этот пример содержит три части:

  • Часть 1: создание проекта библиотеки DLL ActiveX, который будет содержать все публичные объявления UDT
  • Часть 2: создание пример PrintUDT метод продемонстрируйте, как можно перечислять поля экземпляра UDT
  • Часть 3: создание пользовательского класса итератора, который позволяет легко перебирать поля любого общедоступного UDT и получать имена и значения полей.

пример работающего

Часть 1: библиотека ActiveX

как я уже упоминал, вам нужно сделать общедоступными ваши UDT, чтобы перечислить их с помощью TypeLib Информационная составляющая. Единственный способ сделать это-поместить UDT в открытый класс внутри проекта ActiveX DLL или ActiveX EXE. Другие проекты в приложении, которым необходимо получить доступ к UDT, будут ссылаться на этот новый компонент.

чтобы следовать этому примеру, начните с создания нового проекта ActiveX DLL и назовите его UDTLibrary.

далее, переименовать Class1 модуль класса (по умолчанию добавляется IDE) в UserTypes и добавить два определяемые пользователем типы для класса,Person и 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

листинг 1: UserTypes.cls действует как контейнер для наших UDT

далее изменить Instancing свойство UserTypes класс до "2-PublicNotCreatable". Нет причин для кого-либо создавать экземпляр UserTypes класс напрямую, потому что он просто действует как публичный контейнер для наших UDT.

наконец, убедитесь, что Project Startup Object (под -51-->Свойства) имеет значение "(нет) " и компилирует проект. Теперь у вас должен быть новый файл с именем UDTLibrary.dll.

Часть 2: перечисление информации типа UDT

теперь пришло время продемонстрировать, как мы можем использовать библиотеку объектов TypeLib для реализации PrintUDT метод.

во-первых, начните с создания нового стандартного проекта EXE и назовите его как угодно. Добавьте ссылку на файл UDTLibrary.dll это было создано в части 1. С тех пор, как я ... чтобы продемонстрировать, как это работает, мы будем использовать немедленное окно для проверки кода, который мы напишем.

создайте новый модуль, назовите его UDTUtils и добавьте к нему следующий код:

'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

листинг 2: пример PrintUDT метод и простой метод тестирования

Часть 3: сделать его объектно-ориентированным

приведенные выше примеры предоставляют "быструю и грязную" демонстрацию того, как использовать библиотеку информационных объектов TypeLib для перечислите поля UDT. В реальном сценарии я, вероятно, создал бы UDTMemberIterator класс, который позволит вам более легко перебирать поля UDT вместе с функцией утилиты в модуле, который создает UDTMemberIterator для данного экземпляра UDT. Это позволит вам сделать что-то вроде следующего в вашем коде, что намного ближе к псевдо-коду, который вы разместили в своем вопросе:

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

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

это на самом деле не слишком сложно сделать, и мы можем повторно использовать большую часть код PrintUDT процедура, созданная в части 2.

сначала создайте новый проект ActiveX и назовите его UDTTypeInformation или что-то подобное.

затем убедитесь, что для объекта запуска нового проекта установлено значение "(None)".

первое, что нужно сделать, это создать простой класс-оболочку, которая скрывает детали TLI.MemberInfo класс из вызывающего кода и позволяет легко получить имя и значение поля UDT. Я назвал этот класс UDTMember. Этот Instancing свойство для этого класса должна быть 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

Листинг 3: The UDTMember класса

теперь нам нужно создать класс итератора,UDTMemberIterator, это позволит нам использовать vb's For Each...In синтаксис для итерации полей экземпляра UDT. The Instancing свойство для этого класса должно быть установлено в PublicNotCreatable (позже мы определим метод утилиты, который будет создавать экземпляры от имени вызова код.)

EDIT: (2/15/09) я очистил код немного больше.

'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

Листинг 4: The UDTMemberIterator класса.

обратите внимание, что для того, чтобы сделать этот класс итератор, так что For Each можно использовать с ним, вам нужно будет установить определенные атрибуты процедуры на Item и _NewEnum методы (как отмечено в комментариях к коду). Атрибуты процедуры можно изменить в меню Сервис (сервис - >процедура Атрибуты.)

наконец, нам нужна функция полезности (UDTMemberIteratorFor в самом первом примере кода в этом разделе), который создаст UDTMemberIterator для экземпляра UDT, который мы можем затем повторить с For Each. Создайте новый модуль Utils и добавьте следующий код:

'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

Листинг 5: The UDTMemberIteratorFor функции полезности.

наконец, скомпилируйте проект и создайте новый проект для его тестирования.

в вашем тесте проект, добавить ссылку на вновь созданный UDTTypeInformation.dll и UDTLibrary.dll создано в части 1 и попробуйте следующий код в новом модуле:

'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

листинг 6: тестирование UDTMemberIterator класса.


@Dan,

похоже, что вы пытаетесь использовать RTTI UDT. Я не думаю, что вы действительно можете получить эту информацию, не зная о типа до времени выполнения. Чтобы начать работу, попробуйте:

Понимание UDTs
Из-за отсутствия способности к отражению. Я бы создал свой собственный RTTI для моего UDTs.

чтобы дать вам опору. Попробуйте это:

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

вы можете написать утилиту, которая откроет каждый исходный файл и добавит RTTI с именем типа для UDT. Вероятно, было бы лучше поместить все UDTs в общий файл.

RTTI было бы что-то вроде этого:

"String: Long:Long:Long: Integer"

используя память UDT, вы можете извлечь значения.


если вы измените все свои типы на классы. У тебя есть выбор. Большая ловушка перехода от типа к классу заключается в том, что вам нужно использовать новый keyworld. Каждый раз, когда появляется объявление переменной типа add new.

затем вы можете использовать ключевое слово variant или CallByName. VB6 не имеет никакого типа отражения, но вы можете сделать списки допустимых полей и проверить, присутствуют ли они, например

тест класса имеет следующее

Public Key As String
Public Data As String

вы затем можно сделать следующее

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

если вы попытаетесь использовать поле, которое не существует, будет вызвана ошибка 438. CallByName позволяет использовать строки для вызова поля и методы класса.

что VB6 делает, когда вы объявляете Dim как новый, довольно интересно и значительно минимизирует ошибки в этом преобразовании. Вы видите это

Dim T as New Test

не обрабатывается точно так же, как

Dim T as Test
Set T = new Test

например это работа

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

это даст ошибку

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

причина этого заключается в том, что в первом примере VB6 флаги T так, что в любое время, когда к члену обращаются, он проверяет, является ли T ничем. Если это так, он автоматически создаст новый экземпляр тестового класса, а затем назначит переменную.

во втором примере VB не добавляет Это поведение.

в большинств проекте мы rigorously убеждаемся что мы идем тусклый T как испытание, комплект T = новое испытание. Но в твоем случай, так как вы хотите преобразовать типы в классы с наименьшим количеством побочных эффектов, используя Dim T в качестве нового теста. Это связано с тем, что Dim как New заставляет переменную имитировать способ работы типов более тесно.