Как получить имя процедуры или функции во время выполнения?

Есть ли способ вернуть имя функции или процедуры во время выполнения?

В настоящее время я обрабатываю ошибки следующим образом:

Sub foo()
Const proc_name as string = "foo"
On Error GoTo ErrHandler

    ' do stuff

ExitSub:
    Exit Sub
ErrHandler:
    ErrModule.ShowMessageBox "ModuleName",proc_name
    Resume ExitSub
End Sub

Недавно я испытал одну из моих констант, лежащих ко мне после того, как я обновил имя функции, но не постоянное значение. Я хочу вернуть имя процедуры в обработчик ошибок.

Я знаю, что мне придется взаимодействовать с объектом VBIDE.CodeModule, чтобы найти его. Я немного выполнил метапрограммирование с помощью библиотеки Microsoft Visual Basic для расширения возможностей приложений, но я не добился успеха в этом во время выполнения. У меня нет моих предыдущих попыток, и прежде чем я выкапываю каблуки, чтобы попробовать это снова, я хочу знать, возможно ли это даже отдаленно.

Вещи, которые не будут работать

  • Использование встроенной библиотеки VBA для доступа к стеку вызовов. Этого не существует.
  • Реализация моего собственного стека вызовов путем нажатия и вызова имен процедур из массива при входе и выходе из каждого. Это по-прежнему требует, чтобы я передавал имя proc где-то еще как строку.
  • Третий инструмент, например vbWatchDog. Это действительно работает, но я не могу использовать сторонний инструмент для этого проекта.

Примечание

vbWatchdog, похоже, делает это, напрямую обращаясь к памяти ядра через вызовы API.

Ответ 1

Я не совсем уверен, насколько это полезно...

Хорошо, что вам не придется беспокоиться о имени суб/функции - вы можете изменить его. Все, о чем вам нужно заботиться, это уникальность имени имени метки обработчика ошибок.

Например

если вы можете избегать дублирования меток обработчика ошибок в разных subs/functions

не выполнять ⇩⇩⇩⇩⇩

Sub Main()
    On Error GoTo ErrHandler
    Debug.Print 1 / 0

ErrHandler:
    Debug.Print "handling error in Main"
    SubMain
End Sub

Sub SubMain()
    On Error GoTo ErrHandler
    Debug.Print 1 / 0

ErrHandler:
    Debug.Print "handling error in SubMain"
End Sub

то приведенный ниже код должен работать.

Примечание. Я не смог проверить его полностью, но я уверен, что вы можете настроить его и заставить его работать, если это поможет.

Примечание. Добавьте ссылки на Visual Basic for Applications Extensibility 5.3 через Инструменты → Ссылки в VBE

Sub Main()

    ' additionally, this is what else you should do:
    ' write a Boolean function that checks if there are no duplicate error handler labels
    ' this will ensure you don't get a wrong sub/fn name returned

    Foo
    Boo

End Sub


Function Foo()

    ' remember to set the label name (handlerLabel) in the handler
    ' each handler label should be unique to avoid errors
    On Error GoTo FooErr
    Cells(0, 1) = vbNullString ' cause error deliberately

FooErr:

    Dim handlerLabel$
    handlerLabel = "FooErr" ' or don't dim this and pass the errHandler name directly to the GetFnOrSubName function

    Debug.Print "Error occured in " & Application.VBE.ActiveCodePane.CodeModule.Name & ": " & GetFnOrSubName(handlerLabel)

End Function


Sub Boo()

    On Error GoTo BooErr
    Cells(0, 1) = vbNullString ' cause error deliberately

BooErr:

    Debug.Print "Error occured in " & Application.VBE.ActiveCodePane.CodeModule.Name & ": " & GetFnOrSubName("BooErr")

End Sub

' returns CodeModule reference needed in the GetFnOrSubName fn
Private Function GetCodeModule(codeModuleName As String) As VBIDE.CodeModule
    Dim VBProj As VBIDE.VBProject
    Dim VBComp As VBIDE.VBComponent

    Set VBProj = ThisWorkbook.VBProject
    Set VBComp = VBProj.VBComponents(codeModuleName)

    Set GetCodeModule = VBComp.CodeModule
End Function

' returns the name of the sub where the error occured
Private Function GetFnOrSubName$(handlerLabel$)

    Dim VBProj As VBIDE.VBProject
    Dim VBComp As VBIDE.VBComponent
    Dim CodeMod As VBIDE.CodeModule

    Set VBProj = ThisWorkbook.VBProject
    Set VBComp = VBProj.VBComponents(Application.VBE.ActiveCodePane.CodeModule.Name)
    Set CodeMod = VBComp.CodeModule

    Dim code$
    code = CodeMod.Lines(1, CodeMod.CountOfLines)

    Dim handlerAt&
    handlerAt = InStr(1, code, handlerLabel, vbTextCompare)

    If handlerAt Then

        Dim isFunction&
        Dim isSub&

        isFunction = InStrRev(Mid$(code, 1, handlerAt), "Function", -1, vbTextCompare)
        isSub = InStrRev(Mid$(code, 1, handlerAt), "Sub", -1, vbTextCompare)

        If isFunction > isSub Then
            ' it a function
            GetFnOrSubName = Split(Mid$(code, isFunction, 40), "(")(0)
        Else
            ' it a sub
            GetFnOrSubName = Split(Mid$(code, isSub, 40), "(")(0)
        End If

    End If

End Function

Ответ 2

Я использую связанный node класс стека, завернутый в одноэлементный, созданный по всему миру (выполненный через атрибуты) класс CallStack. Это позволяет мне выполнять обработку ошибок, как предлагает Дэвид Земенс (каждый раз сохраняя имя процедуры):

Public Sub SomeFunc()
    On Error Goto ErrHandler
    CallStack.Push "MyClass.SomeFunc"


    '... some code ...

    CallStack.Pop()
    Exit Sub

ErrHandler:
    'Use some Ifs or a Select Case to handle expected errors
    GlobalErrHandler() 'Make a global error handler that logs the entire callstack to a file/the immediate window/a table in Access.

End Sub

Если бы это было полезно для обсуждения, я могу опубликовать связанный код. Класс CallStack имеет метод Peek, чтобы узнать, что является последней вызванной функцией, и функцию StackTrace, чтобы получить строковый вывод всего стека.


Более конкретно к вашему вопросу я всегда интересовался использованием VBA Extensibility для автоматического добавления кода обработки ошибок котельной (как указано выше). Я никогда не собирался заниматься этим, но считаю, что это возможно.

Ответ 3

Следующий вопрос точно не отвечает на мой вопрос, но он решает мою проблему. Его нужно будет запустить во время разработки до публикации приложения.

Мое обходное отношение зависит от того, что все мои константы называются одинаковыми, потому что я использую код CPearson для вставки констант в мои процедуры во время разработки.

Библиотека VBIDE не поддерживает процедуры хорошо, поэтому я завернул их в модуле класса с именем vbeProcedure.

' Class: vbeProcedure
' requires Microsoft Visual Basic for Applications Extensibility 5.3 library
' Author: Christopher J. McClellan
' Creative Commons Share Alike and Attribute license
'   http://creativecommons.org/licenses/by-sa/3.0/

Option Compare Database
Option Explicit

Private Const vbeProcedureError As Long = 3500

Private mParentModule As CodeModule
Private isParentModSet As Boolean
Private mName As String
Private isNameSet As Boolean

Public Property Get Name() As String
    If isNameSet Then
        Name = mName
    Else
        RaiseObjectNotIntializedError
    End If
End Property

Public Property Let Name(ByVal vNewValue As String)
    If Not isNameSet Then
        mName = vNewValue
        isNameSet = True
    Else
        RaiseReadOnlyPropertyError
    End If
End Property

Public Property Get ParentModule() As CodeModule
    If isParentModSet Then
        Set ParentModule = mParentModule
    Else
        RaiseObjectNotIntializedError
    End If
End Property

Public Property Let ParentModule(ByRef vNewValue As CodeModule)
    If Not isParentModSet Then
        Set mParentModule = vNewValue
        isParentModSet = True
    Else
        RaiseReadOnlyPropertyError
    End If
End Property

Public Property Get StartLine() As Long
    If isParentModSet And isNameSet Then
        StartLine = Me.ParentModule.ProcStartLine(Me.Name, vbext_pk_Proc)
    Else
        RaiseObjectNotIntializedError
    End If
End Property

Public Property Get EndLine() As Long
    If isParentModSet And isNameSet Then
        EndLine = Me.StartLine + Me.CountOfLines
    Else
        RaiseObjectNotIntializedError
    End If
End Property

Public Property Get CountOfLines() As Long
    If isParentModSet And isNameSet Then
        CountOfLines = Me.ParentModule.ProcCountLines(Me.Name, vbext_pk_Proc)
    Else
        RaiseObjectNotIntializedError
    End If
End Property

Public Sub initialize(Name As String, codeMod As CodeModule)
    Me.Name = Name
    Me.ParentModule = codeMod
End Sub

Public Property Get Lines() As String
    If isParentModSet And isNameSet Then
        Lines = Me.ParentModule.Lines(Me.StartLine, Me.CountOfLines)
    Else
        RaiseObjectNotIntializedError
    End If
End Property

Private Sub RaiseObjectNotIntializedError()
    Err.Raise vbObjectError + vbeProcedureError + 10, CurrentProject.Name & "." & TypeName(Me), "Object Not Initialized"
End Sub

Private Sub RaiseReadOnlyPropertyError()
    Err.Raise vbObjectError + vbeProcedureError + 20, CurrentProject.Name & "." & TypeName(Me), "Property is Read-Only after initialization"
End Sub

Затем я добавил функцию к моему модулю DevUtilities (что важно позже), чтобы создать объект vbeProcedure и вернуть их коллекцию.

Private Function getProcedures(codeMod As CodeModule) As Collection
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'    Returns collection of all vbeProcedures in a CodeModule      '
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Dim StartLine As Long
    Dim ProcName As String
    Dim lastProcName As String
    Dim procs As New Collection
    Dim proc As vbeProcedure

    Dim i As Long

    ' Skip past any Option statement
    '   and any module-level variable declations.
    StartLine = codeMod.CountOfDeclarationLines + 1

    For i = StartLine To codeMod.CountOfLines
        ' get procedure name
        ProcName = codeMod.ProcOfLine(i, vbext_pk_Proc)
        If Not ProcName = lastProcName Then
            ' create new procedure object
            Set proc = New vbeProcedure
            proc.initialize ProcName, codeMod
            ' add it to collection
            procs.Add proc
            ' reset lastProcName
            lastProcName = ProcName
        End If
    Next i
    Set getProcedures = procs

End Function

Затем я обрабатываю каждую процедуру в данном модуле кода.

Private Sub fixProcNameConstants(codeMod As CodeModule)
    Dim procs As Collection
    Dim proc As vbeProcedure
    Dim i As Long 'line counter

    'getProcName codeMod
    Set procs = getProcedures(codeMod)

    For Each proc In procs
        With proc
            ' skip the proc.StartLine
            For i = .StartLine + 1 To .EndLine
                ' find constant PROC_NAME declaration
                If InStr(1, .ParentModule.Lines(i, 1), "Const PROC_NAME", vbTextCompare) Then
                    'Debug.Print .ParentModule.Lines(i, 1)
                    ' replace this whole line of code with the correct declaration
                    .ParentModule.ReplaceLine i, "Const PROC_NAME As String = " & Chr(34) & .Name & Chr(34)
                    'Debug.Print .ParentModule.Lines(i, 1)
                    Exit For
                End If
            Next i
        End With
    Next proc
End Sub

Наконец, назовем этот sub для каждого модуля кода в моем активном проекте (пока это не мой модуль "DevUtilities").

Public Sub FixAllProcNameConstants()
    Dim prj As vbProject
    Set prj = VBE.ActiveVBProject
    Dim codeMod As CodeModule
    Dim vbComp As VBComponent

    For Each vbComp In prj.VBComponents
        Set codeMod = vbComp.CodeModule
        ' don't mess with the module that'c calling this
        If Not codeMod.Name = "DevUtilities" Then
            fixProcNameConstants codeMod
        End If
    Next vbComp
End Sub

Я вернусь, если узнаю, какое колдовство vbWatchDog использует, чтобы открыть стек вызовов vba.

Ответ 4

Использовать Err.Raise

Для параметра Source pass in:

Me.Name & "." & Application.VBE.ActiveCodePane.CodeModule.ProcOfLine(Application.VBE.ActiveCodePane.TopLine, 0)

Ответ 5

Сообщение было неправильным, не вижу, как удалить мой пост, поэтому я заменил текст.

Ответ 6

Добавьте эту функцию в любой модуль, который вам нравится:

Function getModule_Func_Name()

Dim strModule, strFunction
   strModule = Application.VBE.ActiveCodePane.CodeModule
'   getProcedures (Application.VBE.ActiveCodePane.CodeModule)
strFunction = Application.VBE.ActiveCodePane.CodeModule.ProcOfLine(Application.VBE.ActiveCodePane.TopLine, 0)
getModule_Func_Name = strModule & " , " & strFunction & "()"
End Function

И затем в вашей суб/функции используйте это как обработчик Err:

ErrHandler:
    ErrModule.ShowMessageBox getModule_Func_Name
    Resume ExitSub