How to obtain the macros defined in an Excel workbook - vba

Is there any way, in either VBA or C# code, to get a list of the existing macros defined in a workbook?
Ideally, this list would have a method definition signatures, but just getting a list of the available macros would be great.
Is this possible?

I haven't done vba for Excel in a long time, but if I remember well, the object model for the code was inaccessible through scripting.
When you try to access it, you receive the following error.
Run-time error '1004':
Programmatic access to Visual Basic Project is not trusted
Try:
Tools | Macro | Security |Trusted Publisher Tab
[x] Trust access to Visual Basic Project
Now that you have access to the VB IDE, you could probably export the modules and make a text search in them, using vba / c#, using regular expressions to find sub and function declarations, then delete the exported modules.
I'm not sure if there is an other way to do this, but this should work.
You can take a look the following link, to get started with exporting the modules.
http://www.developersdex.com/vb/message.asp?p=2677&ID=%3C4FCD0AE9-5DCB-4A96-8B3C-F19C63CD3635%40microsoft.com%3E
This is where I got the information about giving thrusted access to the VB IDE.

Building on Martin's answer, after you trust access to the VBP, you can use this set of code to get an array of all the public subroutines in an Excel workbook's VB Project. You can modify it to only include subs, or just funcs, or just private or just public...
Private Sub TryGetArrayOfDecs()
Dim Decs() As String
DumpProcedureDecsToArray Decs
End Sub
Public Function DumpProcedureDecsToArray(ByRef Result() As String, Optional InDoc As Excel.Workbook) As Boolean
Dim VBProj As Object
Dim VBComp As Object
Dim VBMod As Object
If InDoc Is Nothing Then Set InDoc = ThisWorkbook
ReDim Result(1 To 1500, 1 To 4)
DumpProcedureDecsToArray = True
On Error GoTo PROC_ERR
Set VBProj = InDoc.VBProject
Dim FuncNum As Long
Dim FuncDec As String
For Each VBComp In VBProj.vbcomponents
Set VBMod = VBComp.CodeModule
For i = 1 To VBMod.countoflines
If IsSubroutineDeclaration(VBMod.Lines(i, 1)) Then
FuncDec = RemoveBlanksAndDecsFromSubDec(RemoveAsVariant(VBMod.Lines(i, 1)))
If LCase(Left(VBMod.Lines(i, 1), Len("private"))) <> "private" Then
FuncNum = FuncNum + 1
Result(FuncNum, 1) = FindToLeftOfString(InDoc.Name, ".") '
Result(FuncNum, 2) = VBMod.Name
Result(FuncNum, 3) = GetSubName(FuncDec)
Result(FuncNum, 4) = VBProj.Name
End If
End If
Next i
Next VBComp
PROC_END:
Exit Function
PROC_ERR:
GoTo PROC_END
End Function
Private Function RemoveCharFromLeftOfString(TheString As String, RemoveChar As String) As String
Dim Result As String
Result = TheString
While LCase(Left(Result, Len(RemoveChar))) = LCase(RemoveChar)
Result = Right(Result, Len(Result) - Len(RemoveChar))
Wend
RemoveCharFromLeftOfString = Result
End Function
Private Function RemoveBlanksAndDecsFromSubDec(TheLine As String) As String
Dim Result As String
Result = TheLine
Result = RemoveCharFromLeftOfString(Result, " ")
Result = RemoveCharFromLeftOfString(Result, " ")
Result = RemoveCharFromLeftOfString(Result, "Public ")
Result = RemoveCharFromLeftOfString(Result, "Private ")
Result = RemoveCharFromLeftOfString(Result, " ")
RemoveBlanksAndDecsFromSubDec = Result
End Function
Private Function RemoveAsVariant(TheLine As String) As String
Dim Result As String
Result = TheLine
Result = Replace(Result, "As Variant", "")
Result = Replace(Result, "As String", "")
Result = Replace(Result, "Function", "")
If InStr(1, Result, "( ") = 0 Then
Result = Replace(Result, "(", "( ")
End If
RemoveAsVariant = Result
End Function
Private Function IsSubroutineDeclaration(TheLine As String) As Boolean
If LCase(Left(RemoveBlanksAndDecsFromSubDec(TheLine), Len("Function "))) = "function " Or LCase(Left(RemoveBlanksAndDecsFromSubDec(TheLine), Len("sub "))) = "sub " Then
IsSubroutineDeclaration = True
End If
End Function
Private Function GetSubName(DecLine As String) As String
GetSubName = FindToRightOfString(FindToLeftOfString(DecLine, "("), " ")
End Function
Function FindToLeftOfString(FullString As String, ToFind As String) As String
If FullString = "" Then Exit Function
Dim Result As String, ToFindPos As Integer
ToFindPos = InStr(1, FullString, ToFind, vbTextCompare)
If ToFindPos > 0 Then
Result = Left(FullString, ToFindPos - 1)
Else
Result = FullString
End If
FindToLeftOfString = Result
End Function
Function FindToRightOfString(FullString As String, ToFind As String) As String
If FullString = "" Then Exit Function
Dim Result As String, ToFindPos As Integer
ToFindPos = InStr(1, FullString, ToFind, vbTextCompare)
Result = Right(FullString, Len(FullString) - ToFindPos + 1 - Len(ToFind))
If ToFindPos > 0 Then
FindToRightOfString = Result
Else
FindToRightOfString = FullString
End If
End Function

Related

VBA - Function is called depending on its position in code

I am trying to create an excel function which will fill column with data gathered from OGame's reports. I have encountered strage behavior (at least from my point of view).
I have a form where the user can copy their reports to a form's textbox. After clicking the button, the following functions should start:
Option Explicit
Private Sub CommandButton1_Click()
Dim arr() As String
Dim test As Boolean
Dim str As String
str = testFunction()
arr = readData(Raporty.Value)
' Dim element As Variant
' For Each element In arr
' Debug.Print element
' Debug.Print "-------------------------------"
' Next element
test = writeData(arr)
Debug.Print (test)
Debug.Print ("str: " + str)
Unload ufWczytajRaporty
End Sub
Private Function testFunction() As String
Debug.Print ("testFunction")
testFunction = "testFunctionText"
End Function
Private Function writeData(arr1() As String) As Boolean
Debug.Print ("writeData")
For Each element In arr1
writeDataRow (element)
Next element
writeData = True
End Function
Private Function readData(text As String) As String()
Debug.Print ("readData")
Dim list As Object
Dim substr As Integer
Dim temp As String
Dim arr(0 To 10) As String
Dim result() As String
Dim counter As Integer
Set list = CreateObject("System.Collections.ArrayList")
counter = 0
If text = "" Then
MsgBox "Dane nie mogą być puste! Proszę uzupełnić poprawnymi raportami!", vbCritical
Else
If InStr(1, text, "Przybył statek handlowy") = 1 Then
While (InStr(1, text, "Towar został już odebrany."))
substr = InStr(1, text, "Towar został już odebrany.") + 26
arr(counter) = Left(text, substr)
temp = Mid(text, substr, Len(text))
text = temp
counter = counter + 1
Wend
Unload ufWczytajRaporty
Else
MsgBox "Niepoprawne raporty! Proszę wkleć dobre raporty!", vbCritical
End If
End If
ReDim result(0 To counter)
Dim i As Integer
For i = 0 To counter
result(i) = arr(i)
Next i
readData = arr
End Function
This functions provide the following result in Immediate:
testFunction
readData
False
str: testFunctionText
As you can see the above false is a call of writeData fuction, however it never entered the function. writeData function contains Debug.Print ("writeData") but it was never displayed.
Furthermore if I change places the following function:
str = testFunction()
arr = readData(Raporty.Value)
to
arr = readData(Raporty.Value)
str = testFunction()
testFunction is not called aswell.
Can someone explain me what is happening? Why those functions are not called?

Check if ActiveX label contains part of string

I am using this code to hide a label based on if it contains % sign only and nothing else.
It is this part of the code it is erroring now when running. Error: "OLEFormat.Object: Invalid Request. Command cannot be applied to a shape range with multiple shapes"
What should be the correct code?
If InStr(1, myRange.OLEFormat.Object.Caption, "%", vbTextCompare) > 0 Then
Sub c_Three_RemovePercent()
For slideNumber = 1 To 11
Set mydocument = ActivePresentation.Slides(slideNumber)
mydocument.Select
Dim myArray() As Variant
Dim myRange As Object
myArray = Array("Lbl_V1", "Lbl_V2", "Lbl_V3", "Lbl_V4", "Lbl_V5")
Set myRange = ActivePresentation.Slides(1).Shapes.Range(myArray)
With mydocument.Shapes.Range(myArray)
If InStr(1, myRange.OLEFormat.Object.Caption, "%", vbTextCompare) > 0 Then
mydocument.Shapes(myRange).Visible = False
Else: mydocument.Shapes(myRange).Visible = True
End If
End With
Next slideNumber
End Sub
All these blindfolded late-bound member calls are easily confusing: you don't get IntelliSense to help you navigate the available members.
You're looking for an OLEObject, so declare one; assign it:
Dim oleLabel As Excel.OLEObject
Set oleLabel = ActivePresentation.Slides(1).Shapes("SomeShapeName").OLEFormat.Object
Now you want the control that's in that OLEObject's Object property, and you want to cast that control to its MSForms.Label interface:
Dim labelControl As MSForms.Label
Set labelControl = oleLabel.Object
Now you have an early-bound MSForms.Label interface to query, and IntelliSense guides you all the way:
If Contains(labelControl.Caption, "%") Then
'...
Else
'...
End If
Where Contains could look something like this:
Public Function Contains(ByVal source As String, ByVal substring As String) As Boolean
Contains = InStr(1, source, substring, vbTextCompare) > 0
End Function
You have an array of label control names you want to iterate - just iterate it:
Dim labelNames As Variant
labelNames = Array("label1", "label2", "label3", ...)
Dim i As Long
For i = LBound(labelNames) To UBound(labelNames)
Set oleLabel = currentSlide.Shapes(labelNames(i)).OLEObject
oleLabel.Visible = Not Contains(labelControl.Caption, "%")
Next
Note how this:
If BooleanExpression Then
Thing = True
Else
Thing = False
End If
Can be rewritten as:
Thing = BooleanExpression
For checking if string contains the vba function INSTR is typically best. Basically in the below example... Starting in the first position, check this text, look for "%", case insensative.
If InStr(1, myRange.OLEFormat.Object.Caption, "%", vbTextCompare) > 0 Then
mydocument.Shapes(myRange).Visible = False
Else: mydocument.Shapes(myRange).Visible = True
End If

Get part of string that matches with another string

I am doing a VBA Macro for Excel and I need to get the part of a file path that matches with a specific string.
I mean, I have a Variant called FileInfo that contains the path of the Workbook that I am using at that moment (inside a For), for example, Variant may look like:
C:\Users\myUser\Desktop\SVN-Folder\trunk\G\INC20825\Estimación Temporal_v01r00.xlsx
I want to make a function that returns only the part of the path that matches with "INC*" and if the path doesn't have that match, return null.
So the function in this case may return: INC20825
I tried with this but did not work
'This function returns the INC folder where is contained
Function INCFolder(FileInfo As Variant)
Dim i As Integer
If FileInfo Like "INC*" Then
i = InStr(FileInfo, "INC")
INCFolder = Mid(FileInfo, i, 8)
Else
INCFolder = Null
End If
End Function
EDIT with partial solution:
I made it working to get the 8 characters of INC* with the following code:
'This function returns the INC folder where is contained
Function INCFolder(FileInfo As Variant)
Dim i As Integer
i = InStr(FileInfo, "INC")
If i = 0 Then
INCFolder = Null
Else
INCFolder = Mid(FileInfo, i, 8)
End If
End Function
Problems will come when INC is bigger or smaller than 8
You can use Split to seperate your \ from your full path to PathArr array elements, and then loop through PathArr elements and look for "INC".
The code below will give you flexibility with the number of characters you have for "INC".
Code
Option Explicit
Sub test()
Const FullName = "C:\Users\myUser\Desktop\SVN-Folder\trunk\G\INC20825\Estimación Temporal_v01r00.xlsx"
Dim INCSection As String
INCSection = INCFolder(FullName)
End Sub
Function INCFolder(FileInfo As Variant) As String
Dim i As Long
Dim PathArr As Variant
If FileInfo Like "*INC*" Then
PathArr = Split(FileInfo, "\") ' split folders to array
For i = 0 To UBound(PathArr) ' loop through array and look for "*INC*"
If PathArr(i) Like "*INC*" Then
INCFolder = PathArr(i)
Exit Function
End If
Next i
Else
INCFolder = "Error!"
End If
End Function
Just add one more * in the Like:
Option Explicit
Public Const pathName = "C:\Folder\trunk\G\INC20825\Estimación Temporal_v01r00.xlsx"
Function INCFolder(FileInfo As Variant)
Dim i As Long
If FileInfo Like "*INC*" Then
i = InStr(FileInfo, "INC")
INCFolder = Mid(FileInfo, i, 8)
Else
INCFolder = False
End If
End Function
Just the alternate way to get the result
Function INCFolder(FileInfo As Variant)
If FileInfo Like "*INC*" Then
INCFolder = Mid(WorksheetFunction.Substitute(Mid(FileInfo, InStr(FileInfo, "\INC"), Len(FileInfo)), "\", "|", 2), 2, WorksheetFunction.Search("|", WorksheetFunction.Substitute(Mid(FileInfo, InStr(FileInfo, "\INC"), Len(FileInfo)), "\", "|", 2)) - 2)
Else
INCFolder = Null
End If
End Function

Search for a certain style in word 2010 and make it into a bookmark using vba

How to make a style as a bookmark in word 2010?
You won't be able to use most of the text in the document as the bookmark name. It is just illegal to use certain characters in a bookmark name in Word/VBA. It may be possible to add such characters in bookmark names in an XML format of the document, so if it is required, you can ask a separate question.
This feels like way too much code to post on SO. You really need to explain what framework you have in place and tell us where your hurdles are. We can't do this again. "Works for me". If you have any questions though don't hesitate to ask.
Run the "RunMe" macro at the bottom.
Private Function IsParagraphStyledWithHeading(para As Paragraph) As Boolean
Dim flag As Boolean: flag = False
If InStr(1, para.Style, "heading", vbTextCompare) > 0 Then
flag = True
End If
IsParagraphStyledWithHeading = flag
End Function
Private Function GetTextRangeOfStyledParagraph(para As Paragraph) As String
Dim textOfRange As String: textOfRange = para.Range.Text
GetTextRangeOfStyledParagraph = textOfRange
End Function
Private Function BookmarkNameAlreadyExist(bookmarkName As String) As Boolean
Dim bookmark As bookmark
Dim flag As Boolean: flag = False
For Each bookmark In ActiveDocument.Bookmarks
If bookmarkName = bookmark.name Then
flag = True
End If
Next
BookmarkNameAlreadyExist = flag
End Function
Private Function CreateUniqueBookmarkName(bookmarkName As String)
Dim uniqueBookmarkName As String
Dim guid As String: guid = Mid$(CreateObject("Scriptlet.TypeLib").guid, 2, 36)
guid = Replace(guid, "-", "", , , vbTextCompare)
uniqueBookmarkName = bookmarkName & guid
CreateUniqueBookmarkName = uniqueBookmarkName
End Function
Private Function BookmarkIt(rng As Range, bookmarkName As String)
Dim cleanName As String: cleanName = MakeValidBMName(bookmarkName)
If BookmarkNameAlreadyExist(cleanName) Then
cleanName = CreateUniqueBookmarkName(cleanName)
End If
ActiveDocument.Bookmarks.Add name:=cleanName, Range:=rng
End Function
''shamelessly stolen from gmaxey at http://www.vbaexpress.com/forum/showthread.php?t=37674
Private Function MakeValidBMName(strIn As String)
Dim pFirstChr As String
Dim i As Long
Dim tempStr As String
strIn = Trim(strIn)
pFirstChr = Left(strIn, 1)
If Not pFirstChr Like "[A-Za-z]" Then
strIn = "A_" & strIn
End If
For i = 1 To Len(strIn)
Select Case Asc(Mid$(strIn, i, 1))
Case 49 To 58, 65 To 90, 97 To 122
tempStr = tempStr & Mid$(strIn, i, 1)
Case Else
tempStr = tempStr & "_"
End Select
Next i
tempStr = Replace(tempStr, " ", " ")
MakeValidBMName = tempStr
End Function
Sub RunMe()
Dim para As Paragraph
Dim textOfPara As String
For Each para In ActiveDocument.Paragraphs
If IsParagraphStyledWithHeading(para) Then
textOfPara = GetTextRangeOfStyledParagraph(para)
If para.Range.Bookmarks.Count < 1 Then
BookmarkIt para.Range, textOfPara
End If
End If
Next
End Sub

VBA. How to find position of first digit in string

I have string "ololo123".
I need get position of first digit - 1.
How to set mask of search ?
Here is a lightweight and fast method that avoids regex/reference additions, thus helping with overhead and transportability should that be an advantage.
Public Function GetNumLoc(xValue As String) As Integer
For GetNumLoc = 1 To Len(xValue)
If Mid(xValue, GetNumLoc, 1) Like "#" Then Exit Function
Next
GetNumLoc = 0
End Function
Something like this should do the trick for you:
Public Function GetPositionOfFirstNumericCharacter(ByVal s As String) As Integer
For i = 1 To Len(s)
Dim currentCharacter As String
currentCharacter = Mid(s, i, 1)
If IsNumeric(currentCharacter) = True Then
GetPositionOfFirstNumericCharacter = i
Exit Function
End If
Next i
End Function
You can then call it like this:
Dim iPosition as Integer
iPosition = GetPositionOfFirstNumericCharacter("ololo123")
Not sure on your environment, but this worked in Excel 2010
'Added reference for Microsoft VBScript Regular Expressions 5.5
Const myString As String = "ololo123"
Dim regex As New RegExp
Dim regmatch As MatchCollection
regex.Pattern = "\d"
Set regmatch = regex.Execute(myString)
MsgBox (regmatch.Item(0).FirstIndex) ' Outputs 5
I actually have that function:
Public Function GetNumericPosition(ByVal s As String) As Integer
Dim result As Integer
Dim i As Integer
Dim ii As Integer
result = -1
ii = Len(s)
For i = 1 To ii
If IsNumeric(Mid$(s, i, 1)) Then
result = i
Exit For
End If
Next
GetNumericPosition = result
End Function
You could try regex, and then you'd have two problems. My VBAfu is not up to snuff, but I'll give it a go:
Function FirstDigit(strData As String) As Integer
Dim RE As Object REMatches As Object
Set RE = CreateObject("vbscript.regexp")
With RE
.Pattern = "[0-9]"
End With
Set REMatches = RE.Execute(strData)
FirstDigit = REMatches(0).FirstIndex
End Function
Then you just call it with FirstDigit("ololo123").
If speed is an issue, this will run a bit faster than Robs (noi Rob):
Public Sub Example()
Const myString As String = "ololo123"
Dim position As Long
position = GetFirstNumeric(myString)
If position > 0 Then
MsgBox "Found numeric at postion " & position & "."
Else
MsgBox "Numeric not found."
End If
End Sub
Public Function GetFirstNumeric(ByVal value As String) As Long
Dim i As Long
Dim bytValue() As Byte
Dim lngRtnVal As Long
bytValue = value
For i = 0 To UBound(bytValue) Step 2
Select Case bytValue(i)
Case vbKey0 To vbKey9
If bytValue(i + 1) = 0 Then
lngRtnVal = (i \ 2) + 1
Exit For
End If
End Select
Next
GetFirstNumeric = lngRtnVal
End Function
An improved version of spere's answer (can't edit his answer), which works for any pattern
Private Function GetNumLoc(textValue As String, pattern As String) As Integer
For GetNumLoc = 1 To (Len(textValue) - Len(pattern) + 1)
If Mid(textValue, GetNumLoc, Len(pattern)) Like pattern Then Exit Function
Next
GetNumLoc = 0
End Function
To get the pattern value you can use this:
Private Function GetTextByPattern(textValue As String, pattern As String) As String
Dim NumLoc As Integer
For NumLoc = 1 To (Len(textValue) - Len(pattern) + 1)
If Mid(textValue, NumLoc, Len(pattern)) Like pattern Then
GetTextByPattern = Mid(textValue, NumLoc, Len(pattern))
Exit Function
End If
Next
GetTextByPattern = ""
End Function
Example use:
dim bill as String
bill = "BILLNUMBER 2202/1132/1 PT2200136"
Debug.Print GetNumLoc(bill , "PT#######")
'Printed result:
'24
Debug.Print GetTextByPattern(bill , "PT#######")
'Printed result:
'PT2200136