VBA - Why opening Word from Excel is slow? - excel

I wondered if anyone can shed some light on why opening a Word document would take a few seconds from Excel? The code below quickly finds/opens a specific subfolder using InStr i.e. debug.print of the subfolder name is immediate, however opening the specific Word doc takes about 4 seconds. I tried testing a similar procedure in Word itself it opened the document almost immediately. I'm still learning VBA and I'm not sure what the reason would be other than its something to do with the last bit re strFile
Any suggestions would be appreciated.
Sub LoopSubfolderAndFiles()
Dim fso As Object
Dim folder As Object
Dim subfolder1 As Object
Dim strTextFind1 As String
Dim strFileFound As String
Dim CurrFile As Object
Dim myFile As Object
Dim strFile As String
Dim strExtension As String
Dim wordApp As New Word.Application
Set fso = CreateObject("Scripting.FileSystemObject")
Set folder = fso.GetFolder("Enter FILEPATH name..........")
Set subfolder1 = folder.subfolders
strTextFind1 = "useful"
strFileFound = "test"
strExtension = ".doc"
For Each subfolder1 In subfolder1
If InStr(1, subfolder1.Name, strTextFind1, vbTextCompare) > 0 Then
Set CurrFile = fso.GetFolder(subfolder1)
Debug.Print subfolder1.Name
Exit For
End If
Next
For Each CurrFile In CurrFile.Files
If InStr(1, CurrFile.Name, strFileFound, vbTextCompare) > 0 Then
Set myFile = fso.GetFile(CurrFile)
strFile = myFile.Path
wordApp.Visible = True
wordApp.Documents.Open (strFile)
Debug.Print strFile
End If
Next
Set fso = Nothing
Set folder = Nothing
Set subfolder1 = Nothing
Set CurrFile = Nothing
End Sub

There is nothing substantive wrong with your code. Word is slow.
The difference could be inprocess vs outofprocess. Out of Process calls are made using the RPC networking remote call procedure. Hidden windows are created so messages can be received. It's all very complicated so out of process calls work under all circumstances. In Process Calls are just a machine code jump instruction. Several clock cycles vs tens of thousands or more.
There are some minor issues.
These lines are pointless. This is handled at the end of each line for implicit variables and every end function etc for explicit variables. See Declaring Variables Memory Leaks
Set fso = Nothing
Set folder = Nothing
Set subfolder1 = Nothing
Set CurrFile = Nothing
If you want to do this indirection then they need to be const. The compiler will put them into the line where used as literals. Use variables only where needed.
strTextFind1 = "useful"
strFileFound = "test"
strExtension = ".doc"
So
const strTextFind1 = "useful"
const strFileFound = "test"
const strExtension = ".doc"
You are late binding to FSO. Use early binding as you do for Word. See Pointers needed for speeding up nested loop macro in VBA. Then instead of Dim folder As Object dim it as you do word.

Looking at your code, it's not just opening the document, it's also starting a new instance of the Word application*. So there are a number of factors that are taking time:
Starting Word. Have you ever timed how long it takes Word to start when you click the icon? First, the application itself needs to load. Then, there may be any number of add-ins loading, which will take time.
When an outside application "automates" another application there is a time "hit" for the "cross-barrier" communication. VBA within an Office application is usually quite fast; the same commands run from a different application will be (noticeably) slower.
'* You should never declare and instantiate an application in the same line in VBA. You should change your code to:
Dim wordApp as Word.Application
Set wordApp = New Word.Application

Related

VBA opening excel workbooks in recursion

I'm trying to write code for creating the map of excel workooks network (like one file with links to seven other files, which in turn have their own links to maybe different files, etc.). Since I don't know a priori the set of all files in the network, I want to do this by recursion. I've written this piece of code:
Sub recLink(strPath As String)
Dim WB As Workbook
Set WB = Workbooks.Open(strPath , False, True)
If Not IsEmpty(WB.LinkSources(xlExcelLinks)) Then
For Each LNK In WB.LinkSources(xlExcelLinks)
Debug.Print LNK
Call recLink(Str(LNK))
Next LNK
Else
End If
WB.Close (False)
End Sub
and the problem is the excel app shuts down when trying to open a workbook in the second iteration. That is true even for small and simple files created for the purpose of testing.
Can you please help me with making this work? What am I missing here?
The cause
The issue is on the Str(LNK) from "Call recLink(Str(LNK))".
The solution
Create a string variable and set LNK to this var, then you can call the function using the string variable. This will work.
Dim strLink as String
.
.
strLink = LNK
Call recLink( strLink )
.
.
How I found
I only discovered because I was trying to put all links in array first, and the same issue occoured when VBA was simply going to define the array with LNK. So I figure out that the issue could not be the recursive call and the only thing that was different was the STR () function.
arrLink(x) = Str(LNK)
My sugestion
This code keep all the windows of Excel hide, the presentation prettier and the execution faster.
Function recLink(strPath As String)
Dim objMaster As Object
Dim wbkMaster As Workbook
Dim strLink As String
Set objMaster = CreateObject("Excel.Application")
With objMaster
.Visible = False
Set wbkMaster = .Workbooks.Open(strPath)
If Not IsEmpty(wbkMaster.LinkSources(xlExcelLinks)) Then
For Each LNK In wbkMaster.LinkSources(xlExcelLinks)
strLink = LNK
Debug.Print strLink
Call recLink(strLink)
Next LNK
Else
End If
End With
wbkMaster.Close (False)
Set objMaster = Nothing
Set wbkMaster = Nothing
End Function
Conclusion
Test both codes and choose what is the best for you.
Regards
Have a nice weekend.

How can I delete multiple blank .csv files?

I have some VBA code in excel 2010 that imports multiple .csv files in to one excel workbook however, sometimes there is a rogue file that contains nothing and has a file size of zero that throws up an error, I then have to manually go to the folder and delete this and run my macro again.
Therefore I'm after some help that will allow me to check the file size's of all .csv file contained in a folder and delete any that are zero before I import them. Is there a way I can do this? Or possibly another suggested method that would help?
I'm very new to VBA so please be patient if I don't fully understand straight away.
I have looked into FileLen(C:\Test\test.csv) = 0 Then Kill said file.
But this only deletes specifically the file mentioned. I would rather check all file sizes and if any are zero, kill them.
You can use the FileSystemObject to check files in a folder, and delete them if they are of the correct type and size = 0.
Be very careful running this as the deletions will be permanent (not recoverable).
Option Explicit
'Set reference to Microsoft Scripting Runtime
Sub terfuge()
Dim FSO As FileSystemObject, FI As File, FIs As Files, FO As Folder
Const strBasePath As String = "full_path_of_folder_to_search"
Dim bMsg As Integer
Set FSO = New FileSystemObject
Set FO = FSO.GetFolder(strBasePath)
Set FIs = FO.Files
For Each FI In FIs
If FI.Name Like "*.csv" Then
If FI.Size = 0 Then
bMsg = MsgBox(Prompt:="Are you sure you want to delete " & FI.Name & "?", Buttons:=vbYesNoCancel)
Select Case bMsg
Case vbYes
FI.Delete
Case vbCancel
Exit Sub
End Select
End If
End If
Next FI
End Sub
You can use the DIR command to search through all files in a folder then kill any with a file size of 0.
The code below searches through a specified folder and lists the *.xls filenames in a sheet called Main.
It should be easy to adapt this and combine with your own Kill command.
Sub Directory()
Dim strPath As String
Dim strFolderPath As String
Dim strFileName As String
Dim intRow As Integer
Dim intColumn As Integer
Dim intNumber As Integer
intRow = 1
intColumn = 1
intNumber = 0
strFolderPath = "h:\excel\*.xls" 'search through H drive excel folder
strFileName = Dir(strFolderPath) 'Get name of first file
Do
Sheets("Main").Cells(intRow, intColumn) = strFileName 'write filename
strFileName = Dir 'get next filename
intRow = intRow + 1
Loop Until strFileName = ""
End Sub
Thanks both for your speedy responses, I managed to eventually get my loop going jiggling Andy's sample.
I'd already started dabbling with his idea before Ron responded so continued with it.
I do however like the idea of the user being asked first whether or not they would like to delete the file, so I think I'll have a stab at Ron's suggestion too and maybe learn something along the way.
Thanks again.

How do I make linked Word and Excel documents portable?

We have some Report template documents in Word that are linked to template excel documents with various OLE linked objects from Excel to Word.
Since OLE uses absolute paths vs. relative, copy pasting the two documents to another location (or even moving them) breaks the OLE links.
So, what's the best way to facilitate moving the documents to another location AND moving the Word template and being able to link it to another Excel document in the new location?
I've searched through numerous sites and found some solutions for technical people:
http://www.msofficeforums.com/word/38722-word-fields-relative-paths-external-files.html
https://answers.microsoft.com/en-us/msoffice/forum/all/making-excel-links-in-word-portable-ie-relative/8f67c68e-6406-4ef2-9b97-4d96c43dcb2c,
BUT this needs to be easy enough for non-technical people to use.
I would like to be able to copy and paste BOTH documents (the Word template AND the linked Excel template) to a new location and have them work the same way they did in the original location.
I would also like to be able to copy just the Word template to a new location and link it to an Excel template in that new location.
I ended up writing a solution to a problem that I had a difficult time finding an answer for, so I wanted to share what ended up working for me.
The code looks in the working directory of the word document, finds the first excel document (I only have 1 excel file per folder in my job, so this setup works for me), and changes the source of all OLE objects in the word document to match the excel document, which makes it possible to create a word/excel template pair and move them to different locations.
*NOTE: I've used Windows sepecific objects/functions for I/O, i.e. MyFile, MyFSO, MyFolder... etc., but I don't think it would be terribly difficult to make the I/O platform agnostic.
**NOTE: I also haven't really added any error checking as it's a quick and dirty macro that's used internally to facilitate portability AND I've never used vba before, so garbage cleanup etc. was a just kind of thrown in there, if there's a way to refactor everything and clean it up, please let me know.
Sub UpdateWordLinks()
Dim newFilePath As Variant
Dim excelDocs As Variant
Dim range As Word.range
Dim shape As shape
Dim section As Word.section
excelDocs = GetFileNamesbyExt(ThisDocument.Path, ".xlsx")
'The new file path as a string (the text to replace with)'
newFilePath = ThisDocument.Path & Application.PathSeparator & excelDocs(1)
Call updateFields(ThisDocument.fields, newFilePath)
For Each section In ThisDocument.Sections
Call updateHeaderFooterLinks(section.headers, newFilePath)
Call updateHeaderFooterLinks(section.Footers, newFilePath)
Next
'Update the links
ThisDocument.fields.Update
Set newFilePath = Nothing
Set excelDocs(1) = Nothing
Set excelDocs = Nothing
Set range = Nothing
Set shape = Nothing
Set section = Nothing
End Sub
Function GetFileNamesbyExt(ByVal FolderPath As String, FileExt As String) As Variant
Dim Result As Variant
Dim i As Integer
Dim MyFile As Object
Dim MyFSO As Object
Dim MyFolder As Object
Dim MyFiles As Object
Set MyFSO = CreateObject("Scripting.FileSystemObject")
Set MyFolder = MyFSO.GetFolder(FolderPath)
Set MyFiles = MyFolder.Files
ReDim Result(1 To MyFiles.count)
i = 1
For Each MyFile In MyFiles
If InStr(1, MyFile.Name, FileExt) <> 0 Then
Result(i) = MyFile.Name
i = i + 1
End If
Next MyFile
ReDim Preserve Result(1 To i - 1)
GetFileNamesbyExt = Result
Set MyFile = Nothing
Set MyFSO = Nothing
Set MyFolder = Nothing
Set MyFiles = Nothing
End Function
Function updateHeaderFooterLinks(headersFooters As headersFooters, newFilePath As Variant)
Dim headerFooter As Word.headerFooter
For Each headerFooter In headersFooters
Call updateFields(headerFooter.range.fields, newFilePath)
Next
Set headerFooter = Nothing
End Function
Function updateFields(fields As fields, newFilePath As Variant)
Dim field As field
Dim oldFilePath As Variant
For Each field In fields
If field.Type = wdFieldLink Then
oldFilePath = field.LinkFormat.SourceFullName
field.Code.Text = Replace(field.Code.Text, _
Replace(oldFilePath, "\", "\\"), _
Replace(newFilePath, "\", "\\"))
End If
Next
Set field = Nothing
Set oldFilePath = Nothing
End Function
It works for me by allowing me to copy paste either both a word and excel file together to a new location and run the macro, or by allowing me to copy paste the word document only and run the macro to link it to an excel doc in the new location.
**I should also note that I only needed to look in the body and header/footer stories for the links we use, so this code is not as robust as it could be, but I don' think it would be too tough to add another loop or two to cover off any missing stories
Cheers!
If you are still looking for a way to create portable links between Excel and Word, our Excel-to-Word Document Automation Add-in may help. Unlike native linking through Office you can: rename files, copy/paste and reorganize content, share the linked files, etc. The same Excel document can update multiple destination Word and/or PowerPoint report templates. You only need to set up the links once and they can be updated numerous times. I hope this helps, you can find out more at https://analysisplace.com/Document-Automation

Access files with long paths (over 260)

I'm using Microsoft Scripting Runtime (FSO) to parse folders and produce a list of all of its contents, the folders are on a network and resultant paths end up longer than 260. The minimum code I have is as below:-
Private Sub ProcessFolder(ByVal StrFolder As String)
Dim Fl As File
Dim Fldr As Folder
Dim RootFldr As Folder
Set RootFldr = FS.GetFolder(StrFolder)
For Each Fl In RootFldr.Files
Debug.Print Fl.Path
Next
For Each Fldr In RootFldr.SubFolders
DoEvents
ProcessFolder Fldr.Path
Next
Set RootFldr = nothing
End sub
At a certain level StrFolder length became 259, the Set RootFldr ... folder line worked but For Each Fl In RootFldr.Files gave the error of 76: Path not found, presumably because the content causes the path to breach the 260 limit.
There were files in the folder when looking in Windows Explorer. I am using Excel as the host for this code as I'm outputting the result to workbooks.
Just to be super clear on my question and its background, I need to use FSO (happy to be shown alternatives if they exist) to access files deeper than 260 characters deep in their network path. I need it as FSO as the tool I have is taking the folder paths and the file paths, name, size created, and modified.
The technique to convert MAXFILE encumbered DOS path names to native OS path names is well established and documented. Summarizing:
Prefix a path that uses a drive letter with \\?\, like \\?\C:\foo\bar\baz.txt
Prefix a path that uses a file share with '\\?\UNC\, like \\?\UNC\server\share\baz.txt.
Works well with FileSystemObject too, at least when I tested your code on Windows 10. That might not necessarily be the case in older Windows versions or with the network redirector on your server. Tested by using the FAR file manager to create subdirectories with long names and verified with:
Dim path = "\\?\C:\temp\LongNameTest"
ProcessFolder path
Produced:
\\?\c:\temp\LongNameTest\VeryLongFolderName0123456789012345678901234567890123456789012345678901234567890123456789\VeryLongFolderName0123456789012345678901234567890123456789012345678901234567890123456789\VeryLongFolderName0123456789012345678901234567890123456789012345678901234567890123456789\VeryLongFolderName0123456789012345678901234567890123456789012345678901234567890123456789\VeryLongFolderName0123456789012345678901234567890123456789012345678901234567890123456789\Chrysanthemum.jpg
Which is 488 characters long. Things to keep in mind:
Native path names must be full paths, they cannot be relative paths. In other words, they must always start with a drive letter or share name and start from the root of the drive/share.
You get the native path name back, don't forget to strip the prefix off again if you display it.
Not tested but should fail, there is still a limitation on the the length of the filename itself (without the directory names), can't be longer than 259 chars. Shouldn't be a problem at all since the user can't create them either.
This took a little creative coding but the use of ShortPath was the answer.
This tool was to create a list of every folder and file in a root folder, the files also showing their size, and created/modified dates. The issue was when the resultant path of a file or folder was over 260, then the error Error 76: Path Not Found was thrown and the code would not capture the content of that area.
Using Microsoft Scripting Runtime (FSO) ShortPath would get around this issue but the path went from human readable to coded:-
Full path
\\ServerName00000\Root_Root_contentmanagement\DPT\STANDARDS_GUIDELINES\VENDOR_CERTIFICATION_FILES\PDFX_CERTIFICATION_ALL\2006_2007\DPT\CompantName0\Approved\Quark\India under Colonial Rule_structure sample\058231738X\Douglas M. Peers_01_058231738X\SUPPORT\ADDITIONAL INFORMATION\IUC-XTG & XML file
Short Path
\\lo3uppesaapp001\pesa_cmcoe_contentmanagement\CTS\S4SJ05~5\V275SE~8\PDM5D9~G\2N52EQ~5\HPE\GS9C6L~U\Approved\Quark\IQPSJ5~F\0CWHH1~G\DOFNHA~8\SUPPORT\A6NO7S~K\IUC-XTG & XML file
(Note I've altered the full path to protect IP and company info but the size is the same)
You can see while I could pass short path to someone and they could put it into Windows Explorer to get there, they would know know where it went by simply looking, to get around this a used a global variable that kept the folder path as a full string and followed what the short path was doing. this string is then what I output to the user. The below code is cut down but shows how I achieved it.
The short answer is ShortPath in FSO will get past the issue but the path will not be pretty.
Dim FS As New FileSystemObject
Dim LngRow As Long
Dim StrFolderPath As String
Dim WkBk As Excel.Workbook
Dim WkSht As Excel.Worksheet
Public Sub Run_Master()
Set WkBk = Application.Workbooks.Add
WkBk.SaveAs ThisWorkbook.Path & "\Data.xlsx"
Set WkSht = WkBk.Worksheets(1)
WkSht.Range("A1") = "Path"
WkSht.Range("B1") = "File Name"
WkSht.Range("C1") = "Size (KB)"
WkSht.Range("D1") = "Created"
WkSht.Range("E1") = "Modified"
LngRow = 2
Run "\\ServerName00000\AREA_DEPT0_TASK000"
Set WkSht = Nothing
WkBk.Close 1
Set WkBk = Nothing
MsgBox "Done!"
End Sub
Private Sub Run(ByVal StrVolumeToCheck As String)
Dim Fldr As Folder
Dim Fldr2 As Folder
Set Fldr = FS.GetFolder(StrVolumeToCheck)
'This is the variable that follows the full path name
StrFolderPath = Fldr.Path
WkSht.Range("A" & LngRow) = StrFolderPath
LngRow = LngRow +1
For Each Fldr2 In Fldr.SubFolders
If (Left(Fldr2.Name, 1) <> ".") And (UCase(Trim(Fldr2.Name)) <> "LOST+FOUND") Then
ProcessFolder Fldr2.Path
End If
Next
Set Fldr = Nothing
End Sub
Private Sub ProcessFolder(ByVal StrFolder As String)
'This is the one that will will be called recursively to list all files and folders
Dim Fls As Files
Dim Fl As File
Dim Fldrs As Folders
Dim Fldr As Folder
Dim RootFldr As Folder
Set RootFldr = FS.GetFolder(StrFolder)
If (RootFldr.Name <> "lost+found") And (Left(RootFldr.Name, 1) <> ".") Then
'Add to my full folder path
StrFolderPath = StrFolderPath & "\" & RootFldr.Name
WkSht.Range("A" & LngRow) = StrFolderPath
WkSht.Range("D1") = RootFldr.DateCreated
WkSht.Range("E1") = RootFldr.DateLastModified
Lngrow = LngRow + 1
'This uses the short path to get the files in FSO
Set Fls = FS.GetFolder(RootFldr.ShortPath).Files
For Each Fl In Fls
'This output our string variable of the path (i.e. not the short path)
WkSht.Range("A" & LngRow) = StrFolderPath
WkSht.Range("B" & LngRow) = Fl.Name
WkSht.Range("C" & LngRow) = Fl.Size /1024 '(bytes to kilobytes)
WkSht.Range("D" & LngRow) = Fl.DateCreated
WkSht.Range("E" & LngRow) = Fl.DateLastModified
LngRow = LngRow + 1
Next
Set Fls = Nothing
'This uses the short path to get the sub-folders in FSO
Set Fldrs = FS.GetFolder(RootFldr.ShortPath).SubFolders
For Each Fldr In Fldrs
'Recurse this Proc
ProcessFolder Fldr.Path
DoEvents
Next
Set Fldrs = Nothing
'Now we have processed this folder, trim the folder name off of the string
StrFolderPath = Left(StrFolderPath, Len(StrFolderPath) - Len(RootFldr.Name)+1)
End If
Set RootFldr = Nothing
End Sub
As mentioned this is a cut version of the code that is working for me to exemplify the the method used to get past this limit. Actually seems quite rudimentary once I'd done it.
I got around this once using the subst command of the command shell. It allows you to assign a drive letter to a local path (kind of like a network share).

Searching for All Files Containing a Specific Extention in a Folder and Subfolders [duplicate]

This question already has answers here:
Get list of sub-directories in VBA
(5 answers)
Closed 8 years ago.
I understand that the answer to this question may be similar to another, but the question is posed in a different way. This question is based on the fact that the user, me, did not know FileSearch was removed. The other is conceptually based, and contains prior knowledge of excel's 2010 changes...
I have found some code here
Sub Search()
Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
Set objSearch = objExcel.FileSearch
objSearch.Lookin = "D:\Music"
objSearch.SearchSubfolders = TRUE
objSearch.FileName = "*.wma"
objSearch.Execute
For Each strFile in objSearch.FoundFiles
Wscript.Echo strFile
Next
objExcel.Quit
End Sub
I tried to run that code on my machine, with it adapted to one of my folders and an extention within the folder, but it returned an error 445 (object doesn't support this action). I'm using excel 2010.
Does anyone know what's going on? I'm trying to help out a co-worker, but I don't know much about File I/O beyond the simple stuff in VBA.
FileSearch was removed from VBA in Office 2007. Thankfully it's not difficult to create your own routine for searching files using the FileSystemObject (add the Windows Scripting Runtime as a reference to get Intellisense code hints).
This is the one that I use - your list of files will be returned as a Collection by the FileList function. It should be simple to add a filter to this to only populate the collection with files of a particular extension.
[Note that you'll need to add the Windows Scripting Runtime reference as mentioned above since the objects are early bound in my example]
Function FileList(Path As String) As Collection
Dim FSO as New Scripting.FileSystemObject
Dim StartingFolder As Scripting.Folder
Set StartingFolder = FSO.GetFolder(Path)
Set FileList = New Collection
RecursiveGetFiles StartingFolder, FileList
End Function
Private Sub RecursiveGetFiles(StartingFolder As Scripting.Folder, ByRef FullFileList As Collection)
Dim File As Scripting.File
For Each File In StartingFolder.Files
FullFileList.Add File, File.Path
Next File
Dim SubFolder As Scripting.Folder
For Each SubFolder In StartingFolder.SubFolders
RecursiveGetFiles SubFolder, FullFileList
Next SubFolder
End Function
This code can then be called by some parent routine, i.e.
Sub Search(Path As String)
Dim ListOfFiles As Collection
Set ListOfFiles = FileList(Path)
Dim File As Scripting.File
For Each File In ListOfFiles
Debug.Print File.Name
Next File
End Sub
Sub Search()
Dim StrFile As String, Path As String, FileName As String
Path = "D:\Music"
FileName = "*.wma"
StrFile = Dir(Path & FileName)
Do While Len(StrFile) > 0
Msgbox StrFile
StrFile = Dir
Loop
End Sub

Resources