Самопроверка VB6 UDT

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

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

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

Спасибо,

Дэн


person Dan    schedule 13.02.2009    source источник
comment
Является ли этот определяемый пользователем тип классом/типом?   -  person shahkalpeshp    schedule 14.02.2009
comment
Ой, извините. Я понял это после публикации, что имя функции — PrintUDT. Итак, это должен быть тип (своего рода структура). Правильно?   -  person shahkalpeshp    schedule 14.02.2009
comment
связанные: stackoverflow.com/questions/3222080/   -  person StayOnTarget    schedule 26.11.2018


Ответы (3)


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

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

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

Этот пример состоит из трех частей:

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

Рабочий пример

Часть 1: ActiveX DLL

Как я уже упоминал, вам необходимо сделать свои UDT общедоступными, чтобы перечислять их с помощью компонента TypeLib Information. Единственный способ добиться этого — поместить 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 напрямую, потому что он просто действует как общедоступный контейнер для наших определяемых пользователем типов.

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

Часть 2. Перечисление информации о типе определяемого пользователем типа

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

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

Создайте новый модуль, назовите его 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 или что-то подобное.

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

Первое, что нужно сделать, это создать простой класс-оболочку, который скроет детали класса 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. Класс-оболочка UDTMember

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

EDIT: (15 февраля 2009 г.) я немного почистил код.

'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. Класс 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. Вспомогательная функция 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.

person Community    schedule 15.02.2009
comment
Конечно, это предполагает, что вы работаете с UDT, которые имеют информацию о типе, то есть с теми, которые объявлены в общедоступных модулях (отдельно скомпилированные DLL, OCX и т. д.). - person Bob; 15.02.2009
comment
Код также нуждается в некоторой очистке. GetWrappedmembersForUDT особенно груб. - person Bob; 15.02.2009
comment
@Bob - Можно поконкретнее? Я согласен, что это не очень элегантно, но если у вас есть какие-то конкретные предложения/идеи по очистке, чтобы он выглядел лучше, мне было бы интересно их услышать. - person Mike Spross; 16.02.2009
comment
Вау. Совсем забыл про ответы, автоматически переключающиеся в вики-режим после определенного количества правок... - person Mike Spross; 16.02.2009
comment
Спасибо за отличный ответ! Я долго пытался придумать метод для этого. Сегодня посмотрю, как пойдет реализация. - person Dan; 16.02.2009
comment
Только что закончил с быстрой и грязной версией реализации. Все работает как шарм! Как сказал MarkJ, по возможности дал бы +2. Хотя это решение также может иметь непреднамеренные последствия продления существования этого компонента в VB6... - person Dan; 16.02.2009
comment
Кроме того, комментарий Боба о отдельной компиляции заставил меня нервничать, но это прекрасно работало как новый класс в существующей dll ActiveX. - person Dan; 16.02.2009
comment
Привет еще раз. Можете ли вы изменить свой код, чтобы он обрабатывал вложенные UDT? У меня есть изменения для этого, но я хотел бы задокументировать их здесь для потомков. Спасибо, Дэн - person Dan; 16.02.2009
comment
@ Дэн, я должен был быть более четким в своем ответе относительно публикации UDT. Как вы обнаружили, UDT не обязательно должны находиться в отдельной библиотеке DLL. Важно сделать их публичными. Я просто предложил поместить их в отдельную DLL, чтобы показать, что она работает, даже когда они находятся в другом файле. - person Mike Spross; 17.02.2009
comment
@Mike Spross, отличный и очень подробный ответ! Вы, кажется, гуру VB6. Может быть, вы можете помочь мне с этим: stackoverflow.com/questions/37261070/. До сих пор я не добился большого прогресса в этом, хотя и предложил награду. Вы можете сделать снимок? - person AllSolutions; 26.05.2016

@Дэн,

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

Понимание UDT
Из-за отсутствия возможности отражения. Я бы создал свой собственный RTTI для своих UDT.

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

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

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

RTTI будет примерно таким:

"Строка:Длинная:Длинная:Длинная:Целое"

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

person Gutzofter    schedule 14.02.2009
comment
Закрученный, но гениальный (имею в виду комплимент!). Думаю получить доступ к памяти RTTI будет нетривиально? Кроме того, это решает только часть проблемы - он также хочет регистрировать имена участников. Думаю, вы могли бы хранить их и в своем члене RTTI. Я думаю, вы сказали, что это базовый уровень - person MarkJ; 15.02.2009

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

Затем вы можете использовать вариант ключевого слова или 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 как New, довольно интересно и значительно минимизирует ошибки в этом преобразовании. Ты видишь это

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 не добавляет этого поведения.

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

person RS Conley    schedule 14.02.2009
comment
Будьте осторожны с Dim As New. Например, если вы делаете Dim acct As New BankAccount, а затем позже хотите сделать If acct Is Nothing, это не будет работать должным образом. VB6 автоматически создаст экземпляр acct, если это Nothing, поэтому проверка на Nothing будет всегда возвращать False. Иногда может доставить тебе неприятности... - person Mike Spross; 16.02.2009
comment
... Тем не менее, я согласен с вашей точкой зрения здесь. Для типа, который был преобразован в класс, не будет существующих проверок на Ничего для переменных этого типа, поэтому проблема возникает только в том случае, если такая проверка будет добавлена ​​позже. - person Mike Spross; 16.02.2009
comment
Я исправил нотоносец, который не форматировался как код, теперь он должен быть понятнее. - person RS Conley; 16.02.2009