i do have several hundred of excel files. Every excel file contains a makro in the "workbooks_open" method. I want to open all these files, comment out the code, save and close the file.
a loop through all files with open/close is not a problem, but with the changing of the vba code i have no idea.
many thanks in advance!
Sub test()
Dim Path as string = "C:\123\"
Dim cDir As String
cDir = Dir(Path & "*.xlsx")
Do While cDir <> ""
Application.DisplayAlerts = False
'### open
Workbooks.Open Filename:=Path & cDir
'### here i want to deactivate/comment out the makro in the workbook_open method
'### save
ActiveWorkbook.Save
ActiveWorkbook.Saved = True
'### close
ActiveWorkbook.Close False
cDir = Dir
Loop
End Sub
To access the code of a workbook using code, you need to allow access to the VBE via code - see https://stackoverflow.com/a/11680865/7599798 how to do so.
You access all the coding stuff of a workbook using its VBProject-Property.
If you want to use the Types and Constants of the Project, add a reference to Microsoft Visual Basic for Applications Extensibility
The VBProject contains a collection of Components VBComponents, this is the list you see in the VBE in the project window, it contains all modules, classes and forms.
The Workbook-Module has the Name ThisWorkbook and it's type = 100 (use vbext_ct_Document if you have added the mentioned reference)
To access the code of a module, use the property CodeModule of the component.
The lines of code can be fetched using the lines-property of CodeModule, you need to pass two parameters (startrow and numbers of rows).
The lines-property is read only, if you want to change code, you can use the methods InsertLines, DeleteLines and ReplaceLines
Have a look to the next routine to see how it could look like. It will simply replace the Workbook_Open()-routine with Workbook_Open_BACKUP() so it will no longer fire when the workbook is opened.
Sub RemoveOnOpen(wb As Workbook)
Dim i As Long
With wb.VBProject
For i = 1 to .VBComponents.Count
' Debug.Print .VBComponents(i).Type, .VBComponents(i).Name
If .VBComponents(i).Type = vbext_ct_Document And .VBComponents(i).Name = "ThisWorkbook" Then
Dim row As Long
For row = 1 To .VBComponents(i).CodeModule.CountOfLines
Dim module As CodeModule, line As String
Set module = .VBComponents(i).CodeModule
line = Trim(module.Lines(row, 1))
If Left(line, 27) = "Private Sub Workbook_Open()" Then
module.ReplaceLine row, Replace(line, "Workbook_Open()", "Workbook_Open_BACKUP()")
End If
Next
End If
Next i
End With
End Sub
Update: As T.M. noted, the name of the Workbook module may be different if used in a different language environment, you should check this.
I also added a Trim-statement when checking the code line for the Sub.
Please, use the next Sub. It should be called by the code iterating between all workbooks to be changed:
Sub ComSpecSub(wb As Workbook, moduleName As String, strLine As String)
Dim objThisWb As VBComponent, CodeM As CodeModule, i As Long, j As Long
Set objThisWb = wb.VBProject.VBComponents("ThisWorkbook")
Set CodeM = objThisWb.CodeModule
If CodeM.Find(strLine, 1, 1, CodeM.CountOfLines, 1, False) = True Then
For i = 1 To CodeM.CountOfLines
If InStr(CodeM.lines(i, 1), strLine) > 0 Then
If left(CodeM.lines(i, 1), 1) = "'" Then Exit Sub 'already commented...
'if running the code again
Do While i + j <= CodeM.CountOfLines
CodeM.ReplaceLine i + j, "'" & CodeM.lines(i + j, 1)
If InStr(CodeM.lines(i + j, 1), "End Sub") > 0 Then Exit Do
j = j + 1
Loop
End If
Next i
End If
End Sub
The above code needs a reference to 'Microsoft Visual Basic for Applications Extensibility'
It should be called from your code as:
ComSpecSub ActiveWorkbook, "ThisWorkbook", "Private Sub Workbook_Open()"
ActiveWorkbook.Close True
If adding the required reference looks problematic, please firstly run the next code, which will add it automatically:
Sub addExtenssibilityReference()
'Add a reference to 'Microsoft Visual Basic for Applications Extensibilty 5.3':
ThisWorkbook.VBProject.References.AddFromGuid _
GUID:="{0002E157-0000-0000-C000-000000000046}", _
Major:=5, Minor:=3
End Sub
Language independant & no loops
In addition to the valid answers of #FunThomas (following his renaming idea) and #FaneDuru I demonstrate an approach with two benefits:
the component ThisWorkbook can be found independantly from regional language settings via wb.VBProject.VBComponents(wb.CodeName),
as workbooks can be referenced not only by their name string which may differ for other languages than English,
but also via a workbook's wb.CodeName property (similar for sheets);
the effective procedure start row can be found in one go via
.ProcBodyLine(srchProcName, 0), where the zero input defines a sub or function procedure kind (other than Get|Let|Set props);
Further hints:
Needs a library reference to Microsoft Visual Basic for Applications Extensibility 5.3 (c.f. also #FaneDuru's progamatical approach).
Generally replacing a code line by another should consider possible line breaks ( _) resulting in two or several lines, too; due to the brevity of the procedure I don't assume a line break before "Workbook_Open" (like e.g. `Private Sub _".
Sub BackUp(wb as WorkBook, Optional ByVal srchProcName As String = "Workbook_Open")
'Purp: change a given procedures name in ThisWorkbook (e.g. "Workbook_Open") by adding "_BACKUP"
'0) Define backup name string
Dim backupName As String: backupName = srchProcName & "_BACKUP"
'1) Access ThisWorkbook directly by its CodeName (independant from regional language settings)!
Dim myComp As VBIDE.VBComponent
Set myComp = wb.VBProject.VBComponents(wb.CodeName)
'Debug.Print "** Code(Name): " & wb.CodeName & " (Local Name: " & myComp.Name & ")"
'2) Search directly for the effective start row of srchProcName (e.g. "Workbook_Open")
Dim effectiveRow As Long
With myComp.CodeModule ' the component's code module
On Error Resume Next
effectiveRow = .ProcBodyLine(srchProcName, 0) ' find effective row of search procedure
Select Case Err.Number
Case 0
Dim newContent As String
newContent = Replace(Trim(.Lines(effectiveRow, 1)), srchProcName, backupName)
.ReplaceLine effectiveRow, newContent
Debug.Print "** " & wb.Name & vbNewLine & "" _
; " Changed procedure '" & srchProcName & "' in row " & effectiveRow & _
" to " & backupName
Case 35
Debug.Print "** " & wb.Name & vbNewLine & _
" Error " & Err.Number & " " & Err.Description & vbNewLine & _
" Procedure '" & srchProcName & "' doesn't exist!" & vbNewLine & _
" (Possibly already 'backupped')": Err.Clear
Case Else
Debug.Print "** " & wb.Name & vbNewLine & _
" Error " & Err.Number & " " & Err.Description: Err.Clear
End Select
End With
End Sub
Example output in VB Editor's immeditate window
Inserting Backup ActiveWorkbook or a pre-set Backup wb in your code should suffice to rename existing "Workbook_Open" procedures by a "_BACKUP" suffix.
** ExampleWorkbook147.xlsm
Changed procedure 'Workbook_Open' in row 8 to Workbook_Open_BACKUP
In reply of #T.M comment and nice answer:
The next solution uses Find, which besides returning True when the searched string has been found, it modifies the StartLine parameter, if used as a variable. Then, since the question also involves commenting all the procedure lines, not only changing the declaration line, it will do it, without iteration, too:
Sub findProcThisWb(Optional wb As Workbook, Optional strLine As String = "Workbook_Open")
Dim thisWBCodeM As CodeModule, foundLine As Long, ProcExists As Boolean, arrPr
Dim procName As String, strCodeLine As String, strProcedure As String, strComProc As String
If wb Is Nothing Then Set wb = ThisWorkbook
Set thisWBCodeM = wb.VBProject.VBComponents(wb.CodeName).CodeModule
foundLine = 1 'initialize the line where from Find starts searching
Dim noLines As Long 'it will keep the found procedure number of lines
With thisWBCodeM
' ProcExists = .Find(strLine, foundLine, .CountOfLines, 1, -1, False, False) ' OP
ProcExists = .Find(strLine, foundLine, 1, .CountOfLines, -1, False, False) ' << Edit/2022-01-24 corr. argument order
Debug.Print foundLine: ' the line of the found procedure, if it has been found!
If ProcExists Then
strCodeLine = .lines(foundLine, 1) 'return the whole line
Debug.Print strCodeLine 'the whole line where the searched string has been found
procName = .ProcOfLine(foundLine, vbext_pk_Proc): Debug.Print "Proc name = " & procName
noLines = .ProcCountLines(procName, vbext_pk_Proc): Debug.Print "Number of procedure lines = " & noLines
strProcedure = .lines(foundLine, noLines): Debug.Print "The whole procedure:" & vbLf & strProcedure
arrPr = Split(strProcedure, vbLf)
strComProc = "'" & Join(arrPr, vbLf & "'"): Debug.Print "The whole commented procedure:" & vbLf; strComProc
'Delete the actual procedure lines:
.DeleteLines foundLine, noLines - 1 ' Edit 2022-01-24: -1
'Add the commented procedure code (from string, but not in the same place, after the declaration lines):
.AddFromString strComProc
End If
End With
End Sub
We have a macro that loops through a set of 50 workbooks that have different amounts of sheets. The sheets look similar but all have different sheet names.
We want to place a formula in the first sheet ("Framsida") that searches through column B in sheet 3 to the last sheet to identify how many unique entries there are.
We have been working with PRODUCTSUM and FREQUENCY.
The formula works when pasted into the sheet manually.
When trying this with the macro, it starts linking to other data sources with the error message
"This workbook contains links to other data sources".
The code we tried:
Worksheets("Framsida").Range("m5").Formula = "=SUMPRODUCT(--(FREQUENCY(''" & Sheets(3).Name & " : " & Sheets(Sheets.Count).Name & " '!$B$6:$b$200, ' " & ActiveWorkbook.Sheets(3).Name & " : " & ActiveWorkbook.Sheets(Sheets.Count).Name & " '!$B$6:$b$200)<>0))"
This is the result that comes out in sheet "Framsida" when running the macro:
=PRODUKTSUMMA(--(FREKVENS('8007029 :[ 8007062 ] 8007062 '!$B$6:$B$200; '8007029 :[ 8007062 ] 8007062 '!$B$6:$B$200)<>0))
Where PRODUKTSUMMA=PRODUCTSUM
and FREKVENS=FREQUENCY
It adds the last sheet name in square brackets and we have no idea why. We are open for suggestions to other solutions.
This is the entire loop:
Sub SummeringFramsida()
'Variabler för loopen
Dim MyFile As String
Dim Filepath As String
'-----------------------------------------------------------------------------------'
'Öppnar filer tills det att man kommer till Huvudfilen i listan, filerna som ska sökas måste alltså ligga ovanför i listan'
Filepath = "C:\Users\JohannaFalkenstrand\Desktop\Excelfix\"
MyFile = Dir(Filepath)
Do While Len(MyFile) > 0
If MyFile = "Huvudfil.xlsm" Then
Exit Sub
End If
Workbooks.Open (Filepath & MyFile)
Workbooks(MyFile).Activate
Worksheets("Framsida").Range("m5").Formula = "=SUMPRODUCT(--(FREQUENCY('" & Sheets(3).Name & " : " & Sheets(Sheets.Count).Name & " '!$B$6:$b$200, ' " & ActiveWorkbook.Sheets(3).Name & " : " & ActiveWorkbook.Sheets(Sheets.Count).Name & " '!$B$6:$b$200)<>0))"
'Stänger, sparar och går till nästa fil'
Workbooks(MyFile).Save
Workbooks(MyFile).Close
MyFile = Dir
Loop
End Sub
Give it a try with Range(...).Address(1,1,xlA1,1). This will give you a string of range reference that contains both the workbook and the sheet reference. Then you can compile the required formula with simple string manipulation, like
For Each wb in <SomeCollectionOfWorkbooks>
For Each sh in wb.Sheets
Debug.Print "Copy this to the required cell = SUM(" & _
sh.Range("B6:B200").Address(1,1,xlA1,1) & ")"
Next
Next
The key is the external reference parameter of .Address.
It looks like you want to use same range but on different sheets at same time, so you need to check this out:
Create a reference to the same cell range on multiple
worksheets
Applying this to your code this should kind of work:
Worksheets("Framsida").Range("m5").Formula = "=SUMPRODUCT(--(FREQUENCY(" & Sheets(3).Name & ":" & Sheets(Sheets.Count).Name & "!$B$6:$b$200," & ActiveWorkbook.Sheets(3).Name & ":" & ActiveWorkbook.Sheets(Sheets.Count).Name & "!$B$6:$b$200)<>0))"
I am new to VBA and to use VBA on excel and write a macro to export an xml file per row (see the example in the print screen).
Unfortunately, I obtain following error
Run-time error '2147024891 (80070005)':
System error: -2147024891.
And when I click on the button "Debug" it jumps to the code line
doc.Save sFile
I use the following code for reading the excel sheet (Microsoft Excel for Mac Version 16.49) and creating a xml file:
Sub CustomerOutToXML()
sTemplateXML = _
"<?xml version='1.0'?>" + vbNewLine + _
"<ENVELOPE>" + vbNewLine + _
"<TRANSACTION>" + vbNewLine + _
"<TYPE>" + vbNewLine + "</TYPE>" + vbNewLine + _
"</TRANSACTION>" + vbNewLine + _
"<CONTENT>" + vbNewLine + vbNewLine + _
"<DATE>" + vbNewLine + "</DATE>" + vbNewLine + _
"<SSCC>" + vbNewLine + "</SSCC>" + vbNewLine + _
"<ORDER>" + vbNewLine + "</ORDER>" + vbNewLine + _
"</CONTENT>" + vbNewLine + _
"</ENVELOPE>"
Set doc = CreateObject("MSXML2.DOMDocument")
doc.async = False
doc.validateOnParse = False
doc.resolveExternals = False
With ActiveWorkbook.Worksheets(1)
lLastRow = .UsedRange.Rows.Count
For lRow = 2 To 7
sFile = "/Users/xxx/Documents/" & .Cells(lRow, 1).Value & ".xml"
Dim sDATE As String
Dim sSSCC As String 'Not Long
Dim sORDER As String
sDATE = CStr(.Cells(lRow, 2).Value)
sSSCC = .Cells(lRow, 3).Text ' <<< Not .Value
sORDER = CStr(.Cells(lRow, 4).Value)
sTransactionType = ActiveSheet.Name
doc.LoadXML sTemplateXML
doc.getElementsByTagName("DATE")(0).appendChild doc.createTextNode(sDATE)
doc.getElementsByTagName("TYPE")(0).appendChild doc.createTextNode(sTransactionType)
doc.getElementsByTagName("SSCC")(0).appendChild doc.createTextNode(sSSCC)
doc.getElementsByTagName("ORDER")(0).appendChild doc.createTextNode(sORDER)
doc.Save sFile
Next
End With
End Sub
This is an example of the excelsheet:
enter image description here
I would be really happy for some quick help. Thanks in advance.
While your issue is not reproducible, consider some tips that may help you diagnose or resolve your issue:
Use Option Explicit at the very top of module (outside of Sub or Function) to be sure all objects are properly defined. Relatedly, place all Dim objects to top of subroutine or function to help readability.
Incorporate proper error handling with On Error GoTo... to capture runtime exceptions and appropriately exit the sub routine after unsetting objects with or without errors.
Avoid use of ActiveWorkbook and ActiveSheet which can affect workflow if you have many workbooks open. Instead use actually assigned object or ThisWorkbook object. See this canonical Excel post, How to avoid using Select in Excel VBA (where second answer discusses Active* methods).
Try using the early binding version of external libraries like MSXML that can expose useful error properties and enable Intellisense in VBA editor. Currently, you use late binding with CreateObject. If needing late binding for portability among many users, then ignore.
Dim doc As MSXML2.DOMDocument
Set doc = New MSXML2.DOMDocument
When using With blocks, follow through in all its properties. In fact, you can avoid use of ActiveSheet:
sTransactionType = .Name
I read an XML document to collect user logon times.
<UserName="Jenny" Monday="7:00" Tuesday="7:30" Wednesday="0" Thursday="7:10" Friday="7:25" Saturday="6:00" Sunday="0"/><UserName="Simon" Monday="8:20" Tuesday="7:45" Wednesday="7:45" Thursday="7:10" Friday="7:25" Saturday="7:00" Sunday="0"/><UserName="Jenny" Monday="8:00" Tuesday="8:30" Wednesday="8:00" Thursday="7:10" Friday="7:25" Saturday="0" Sunday="0"/>Chris
I can get and paste info into a table on an Excel spreadsheet one by one.
Dim pTeamTimes As New XMLdoc
Dim objPlan As IXMLDOMElement
'build file path
strTeamXMLPath = "C:\Users\Public\Libraries\Times" & ".xml"
'load document
objPlan .LoadDocument strTeamXMLPath , "TeamTimes"
For Each objPlan In pTeamTimes.Root.ChildNodes
.Cells(intRow, 13) = User
.Cells(intRow, 14) = objPlan .getAttribute("Monday")
.Cells(intRow, 15) = objPlan .Attributes.getNamedItem("Tuesday").Text
.Cells(intRow, 16) = objPlan .Attributes.getNamedItem("Wednesday").Text
.Cells(intRow, 17) = objPlan .Attributes.getNamedItem("Thursday").Text
.Cells(intRow, 18) = objPlan .Attributes.getNamedItem("Friday").Text
.Cells(intRow, 19) = objPlan .Attributes.getNamedItem("Saturday").Text
.Cells(intRow, 20) = objPlan .Attributes.getNamedItem("Sunday").Text
intRow = intRow + 1
Next objPlan
This pastes the data into a table with the user name on the left and the times to the right row by row (see below).
User|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday
Is there a way to loop through the file and extract the appropriate data for the appropriate header?
I also played with .getAttribute("Monday") and .Attributes.getNamedItem("Tuesday").Text
Is there a major difference in using each of these methods?
Streamlining display of xml node attributes (array approach)
Your XML file snippet's syntax isn't correct. Each node has to show its node name after the opening bracket <SomeNode ...>, but this nodename cannot be followed immediately by an equals character = indicating always a following attribute assignment.
Therefore I built a wellformed xml structure to be able to illustrate a working MCV Example choosing to ("re")name the individual nodes <User ...> followed by UserName and weekday attributes (Monday="7:00" Tuesday="" ...).
<?xml version="1.0" encoding="UTF-8"?>
<AllUsers>
<Team>
<User UserName="Jenny" Monday="7:00" Tuesday="7:30" Wednesday="0" Thursday="7:10" Friday="7:25" Saturday="6:00" Sunday="0"/>
<User UserName="Simon" Monday="8:20" Tuesday="7:45" Wednesday="7:45" Thursday="7:10" Friday="7:25" Saturday="7:00" Sunday="0"/>
<User UserName="Jenny2" Monday="8:00" Tuesday="8:30" Wednesday="8:00" Thursday="7:10" Friday="7:25" Saturday="0" Sunday="0"/>
</Team>
</AllUsers>
Example code
Based on this assumed example structure (loaded here via .LoadXML), I demonstrate how to streamline code getting attributes via a NodeList loop based on the following xml content.
In order to allow a quickly reproducible example, I didn't refer to an external file via .Load, but to a pure string content (received by help function getContent()) loaded via .LoadXML. Of course loading an external file needs the following syntax: xDoc.Load strTeamXMLPath. - Btw several parts of the original code aren't clear, e.g. I don't know what's behind .LoadDocument
Sub GetAttributes()
Dim xdoc As MSXML2.DOMDocument60 ' early binding needs reference to Microsoft 'XML, v6.0'
Set xdoc = New MSXML2.DOMDocument60
If xdoc.LoadXML(getContent()) Then
Dim users As MSXML2.IXMLDOMNodeList
Set users = xdoc.SelectNodes("//Team/User") ' nodelist is zero-based
ReDim tmp(1 To users.Length, 1 To 8)
Dim i As Long
For i = 1 To users.Length
Dim j As Long
For j = 1 To users(i - 1).Attributes.Length
'assign to 1-based 2-dim tmp array
'(whereas users nodelist incl. node attributes list are zero-based!)
tmp(i, j) = users(i - 1).Attributes(j - 1).Text
'Debug.Print i & "." & j, tmp(i, j) ' optional display in VB Editor's immediate window
Next
Debug.Print i, Join(Application.Index(tmp, i, 0), "|")
Next i
Else ' XML Parse Error
Debug.Print getParseError(xdoc)
End If
' write tmp to any target
With Sheet1 ' << change to your project's sheet Code(Name)
' 'a) write captions starting from cell M1 (optional)
' For i = 1 To UBound(tmp, 2)
' .Range("M1").Offset(0, i - 1) = users(0).Attributes(i - 1).BaseName
' Next i
'write tmp results
.Range("M2").Resize(UBound(tmp, 1), UBound(tmp, 2)) = tmp
End With
End Sub
Additionaly you could test other syntax, e.g. for the 1st user (Jenny) to get the Monday attribute:
Debug.Print users(0).Attributes.getNamedItem("Monday").BaseName ' "Monday"
Debug.Print users(0).Attributes.getNamedItem("Monday").Text ' 7:00
Debug.Print xdoc.DocumentElement.SelectSingleNode("Team/User[1]/#Monday").Text ' 7:00
Help functions
Function getContent() gets an assumed minimal, but wellformed xml string content as described above replacing the unknown structure of OP's external file content.
Function getContent() As String
Dim tmp As String
tmp = "<?xml version=""1.0"" encoding=""UTF-8""?>" & vbNewLine
tmp = tmp & "<AllUsers>" & vbNewLine & " <Team>" & vbNewLine & _
vbTab & "<User UserName=""Jenny"" Monday=""7:00"" Tuesday=""7:30"" Wednesday=""0"" Thursday=""7:10"" Friday=""7:25"" Saturday=""6:00"" Sunday=""0""/>" & vbNewLine & _
vbTab & "<User UserName=""Simon"" Monday=""8:20"" Tuesday=""7:45"" Wednesday=""7:45"" Thursday=""7:10"" Friday=""7:25"" Saturday=""7:00"" Sunday=""0""/>" & vbNewLine & _
vbTab & "<User UserName=""Jenny2"" Monday=""8:00"" Tuesday=""8:30"" Wednesday=""8:00"" Thursday=""7:10"" Friday=""7:25"" Saturday=""0"" Sunday=""0""/>" & vbNewLine & _
" </Team>" & vbNewLine & "</AllUsers>"
getContent = tmp
Debug.Print getContent
End Function
Function getParseError(xdoc As MSXML2.DOMDocument60) As String
Dim xPE As MSXML2.IXMLDOMParseError
Set xPE = xdoc.parseError
With xPE
getParseError = "Load Error " & .ErrorCode & " XML File " & vbCrLf & _
Replace(.Url, "file:///", "") & vbCrLf & vbCrLf & _
xPE.reason & _
"Source Text: " & .srcText & vbCrLf & vbCrLf & _
"Line No: " & .Line & vbCrLf & _
"Line Pos: " & .linepos & vbCrLf & _
"File Pos: " & .filepos & vbCrLf & vbCrLf
End With
End Function
I wanted to extract list of defects using a filter criteria. I tried the VBA code from OTA here, but compile fails on the following declarations with User defined type not defined:
Dim BugFact As BugFactory
Dim BugFilter As TDFilter
Dim bugList As List
Dim theBug As Bug
Note: I do not have administrative privileges on ALM.
The full VBA code:
Sub BugFilter()
Dim BugFact As BugFactory
Dim BugFilter As TDFilter
Dim bugList As List
Dim theBug As Bug
Dim i%, msg$
' Get the bug factory filter.
'tdc is the global TDConnection object.
Set BugFact = tdc.BugFactory
Set BugFilter = BugFact.Filter
' Set the filter values.
BugFilter.Filter("BG_STATUS") = "Closed"
BugFilter.order("BG_PRIORITY") = 1
MsgBox BugFilter.Text
'Create a list of defects from the filter
' and show a few of them.
Set bugList = BugFilter.NewList
msg = "Number of defects = " & bugList.Count & Chr(13)
For Each theBug In bugList
msg = msg & theBug.ID & ", " & theBug.Summary & ", " _
& theBug.Status & ", " & theBug.Priority & Chr(13)
i = i + 1
If i > 10 Then Exit For
Next
MsgBox msg
End Sub
You need to add a reference to the OTA COM Type library (see here); otherwise your program will not know about the OTA types such as BugFactory and TDFilter.