I use the following macro to iterate through file paths stored in collection. On some test directory, total run time was 2,3 sec without setting openedApp and openedDoc to nothing and 2,75 with it. Is releasing both of these objects necessary in this case? I intend to use macro on a directory with thousands of files, so I would like to avoid any problems with memory which could cause errors or slow down macro execution.
Dim openedApp As Word.Application
Dim openedDoc As Word.Document
For Each filePath In filesCollection
Set openedApp = New Word.Application
Set openedDoc = openedApp.Documents.Open(filePath)
'openedApp.Visible = True
openedDoc.Close
openedApp.Quit
Set openedApp = Nothing: Set openedDoc = Nothing
Next filePath
There is no need to explicitly set you variables to Nothing. As descibred by this answer VB6 used a reference-counting GC. The GC is triggered deterministically when the last reference to a given object is set to Nothing. Setting local references to Nothing is unnecessary, this happens as they go out of scope.
In fact, if performance matters you can move the creation of the Word application outside the loop body:
Dim openedApp As Word.Application
Dim openedDoc As Word.Document
Set openedApp = New Word.Application
For Each filePath In filesCollection
Set openedDoc = openedApp.Documents.Open(filePath)
openedDoc.Close
Next filePath
openedApp.Quit
There is no need to set VBA objects to nothing. The VBA garbage collector will clean up all the variables when the subroutine goes out of scope. This is legacy from the early days of the VBA when computers weren't as powerful and the garbage collector wasn't as good.
The exception to the rule is when you are using external objects that run outside of the VBA such as connections and recordsets. These should be closed and set to nothing.
Along the same lines, there is no need to erase array at the end of a subroutine. If you have a large array (millions of elements) that is no longer needed and you still have more code to run then you might get a performance boost from erasing the array.
Related
All,
I wrote some VBA code to move the currently selected email folder in an Outlook session to a folder called "archive 2023". It did not work; I spent ages working out why but could not fix the code. Eventually, I settled it by researching and then coming at the problem from a different angle. Although happy, and learning a lot, I still don't know why my original code did not work. And that bugs me (if you excuse the pun).
I'll start with my original code that did not work - I've gone heavy on the comments.
Sub archive_a_folder()
'firsty create the variable I'll store the current folder in as an object
Dim current_folder As Outlook.Folder
'then put the folder, selected in the active instance of Outlook, into the variable
Set current_folder = Application.ActiveExplorer.CurrentFolder
Debug.Print current_folder.Name 'I put this in to check the above worked - and it did!
'I wrote a little code to find out the EntryID property of the folder called "archive 2023"
'I then put the EntryID (which is a string) into a variable
Dim archiveID As String
archiveID = "xxxx" 'instead of xxxx this is a really long string
'I then create a MAPI namespace so I can use the GetFolderFromID method
Dim ns As Outlook.NameSpace
Set ns = Application.GetNamespace("MAPI")
'I then create an Outlook.Folder variable and put the "archive 2023" folder in there by ...
'... using the GetFolderFromID method using the EntryID
Dim archive_folder As Outlook.Folder
Set archive_folder = ns.GetFolderFromID(archiveID)
Debug.Print archive_folder.Name 'Did this to check the above works and it does!
'So at this point I thought I had two correctly assigned Outlook.Folder object variables ...
'... One assigned with the folder that needs moving and one being the destination folder
'The documentation states the MoveTo method should be used like this...
'... Folder.MoveTo(Folder) with the first Folder (an object) being moved to the second.
current_folder.MoveTo(archive_folder)
'I get an object expected error.
End Sub
Running the code line by line proved everything was working right up to current_folder.MoveTo(archive_folder).
The debugging print outs show that the variables current_folder and archive_folder are correctly assigned. I even printed out the variables' types to ensure they were both of the Folder type and they were (they were actually type FolderMAPI but I think thats OK).
I tried creating a new Folder.Outlook variable and having the below statement:
set new_folder = current_folder.MoveTo(archive_folder)
or
new_folder = current_folder.MoveTo(archive_folder)
but niether worked. (I saw that the MoveTo method returned a Folder so that's why I tried that.
Eventually, after research, I re-wrote is like this and it worked.
Sub archive_folder()
'get the current folder and put it in a Folder variable
Dim current_folder As Outlook.Folder
Set current_folder = Application.ActiveExplorer.CurrentFolder
'get a namespace variable so I can use some of its methods later
Dim ns As Outlook.NameSpace
Set ns = Application.GetNamespace("MAPI")
'create inbox as a Folder variable
Dim inbox As Outlook.Folder
'using a namespace method assign the actual in-box to the inbox variable
'olFolderInbox is an inbuilt referene to the default in box folder
Set inbox = ns.GetDefaultFolder(olFolderInbox)
'create a Foler variable that will be assigned the destination folder
Dim archive_folder As Outlook.Folder
'this seems oddly cumbersome but works!
'take parent of the inbox Folder and look for "archive 2003" beneath it
'assign this to the archive folder variable.
Set archive_folder = inbox.Parent.Folders("archive 2023")
'The using the MoveTo method move the current_folder to the
'archive folder
current_folder.MoveTo archive_folder
'when I check in my Outlook window, its moved!
Exit Sub
End Sub
If I had to guess at what the problem is, its something to do with GetFolderFromID not returning a Folder object with all the properties needed for the MoveTo method to work.
I'm probably thinking too 'real world' mistakenly imagining folders actually being stored in other folders. The system probably just looks as the Parent and Folders properties of all the folders and builds a tree for the GUI. Mayube GetFolderFromID does not return these property values correctly so MoveTo does not think its an object at all this the error. This would seem likely if MoveTo just messed about with some of the parent / folders properties.
If this is the case though, what would be the point of the GetFolderFromID function?
Or maybe I'm being punished for trying to skip learning the basics of a language.
Any help?
Aldus
Edit:
I can't believe I did not clock that I should not have used parentheses for the MoveTo method. ##DmitryStreblechenko saw me right in the comments.
To make me feel better I massively reduced the size of the code...
Sub archive_a_folder()
archiveID = "xxx" `xxx is the EntrhyID of the destination folder
Application.ActiveExplorer.CurrentFolder.MoveTo Application.GetNamespace("MAPI").GetFolderFromID(archiveID)
End Sub
:-)
I want to show this question as answered but want to make clear it was answered in the comments and not by me. In essence, and I can't believe I made this mistake, I used parenthesis when I should not have.
Where I used current_folder.MoveTo(archive_folder) I should have used current_folder.MoveTo archive_folder
I fell for the old trick of assuming my syntax was correct but there was some deeper problem. But no, I just used a couple of brackets!
I used to code many years ago so have enough of an understanding to risk skipping some of the basics when learning a new language but the danger of that is what led to this question.
Oh well, you live and learn.
I have a set of functions in an Access database where I am generating Word documents based on a fixed template in a folder.
I am doing this with the function shown below. For easier maintenance, I would like to be able to define the Word-template paths as public constants in the begining of my module. And therefore, I have been trying to create the adjusted function below.
Original function:
Function MyFunc(rs as DAO.Recordset)
Dim objWord As Object
Dim objDoc As Object
...
Set objWord = CreateObject("Word.Application")
Set objDoc = objWord.Documents.Open("C:\test_template.docx")
...
End Function
Adjusted function:
Public Const ReminderOneTemplate As Variant = "C:\test_template.docx"
...
Function MyFunc(rs as DAO.Recordset)
Dim objWord As Object
Dim objDoc As Object
...
Set objWord = CreateObject("Word.Application")
for the Word.Documents.Open method in the next, subsequent line of code I have tried this:
Set objDoc = objWord.Documents.Open(ReminderOneTemplate)
...
End Function
and
Set objDoc = objWord.Documents.Open(Chr(34) & ReminderOneTemplate & Chr(34))
...
End Function
But the function keeps returning Run-time error '424' Object required when I use a constant as input to the Word.Documents.Open method.
Can anyone explain why this is the case and what I am doing wrong. Is it not possible to pass a Constant to the Word.Open method?
Thanks.
I refactored your function, using the following sub procedure to test the basic code of creating a Word instance, and then opening an existing document using a constant for the document name.
I used Office 2007 to test the code, and everything worked fine. I don't see anything wrong with your code, what version of Access and Word are you using? Notice that I added some code to test whether the objWord variable is actually assigned a value by the call to CreateObject. I would suggest 2 things to help try to resolve the problem you are having:
1. use a String for the filename. I know the documentation for the Open method of the Documents collection says that the filename argument is a Variant, but the code does seem to work better if it is a String.
2. Make sure you set the instance of Word to be visible, otherwise you clutter up your system with invisible instances of Word (which will not be listed in Task Manager) and the only way to get rid of them is to restart the computer. If the Word instance is visible, you can switch to it and see if Word is displaying any error messages.
You may notice that the Word document my code opens is a macro-enabled .docm file. I did this because I tested whether a document with an Open event-handler that caused a runtime error would show the error message in the code in Access, but it does not.
When I first ran the code with the constant declared as a Variant, I did get an error, but not the Object required error that is giving you a problem. I then noticed that when I re-opened the Word document in Word, that I got an error message from Word that "the last time this document was opened it caused a serious error, are you sure you want to continue to open the document?" I would suggest you make sure that you can open your Word document in Word without errors or problems. I also suggest you add code similar to what I have in the example below to ensure that the objWord variable is indeed being initialized by the CreateObject method -- if CreatObject is failing to create an instance of Word, then objWord will still be Nothing, and might then produce an object required runtime error. (problems in the Registry can make CreateObject fail.)
I'm really sorry, but I have no idea why you are getting the error you are getting. I think if you redeclare the constant as a String, and ensure that you make the Word instance visible, that your code will work! The only problem I had when testing was that the document failed to open until I added the code to make the Word instance visible. But I did not get the same error that you are trying to overcome.
I have develop a library of Access VBA code for exactly this type of task -- using code in Access to create instances of Word, Excel, and to open documents and worksheets. If you think looking at a code library designed to provide easily called procedures and functions for inter-operability among MS Office applications, you can download it from here: [http://www.didjiman.com/business/vbademo/libMSOffice.htm] The code in the library has been tested to work in all versions of MS Office from 2003 though 2016, and is released to the public under the Gnu public license. The code is in a zip archive that contains an Access .accdb file with all the code, and a PDF document discussing the functions and procedures, and how to use them, along with complete code listings.
Public Const ReminderOneTemplate As String = "C:\users\matthew\documents\temp\test document1.docm"
Sub testWord_DocOpen()
Dim objWord As Object
Dim objDoc As Object
Set objWord = CreateObject("Word.Application")
objWord.Visible = True 'make the Word application window visible
objWord.Application.WindowState = 1 'maximize the Word application window
If (objWord Is Nothing) Then
Debug.Print "Word object NOT initialized."
End If
Set objDoc = objWord.Documents.Open(ReminderOneTemplate)
End Sub
I'm currently writing some code so that when I send an email based upon certain criteria it will save it in a specific folders. I know that rules do something like this, but believe me, it will take forever and wouldn't be practical to set that many rules. So here's what I'm trying to do. When an email is sent, code looks into the title of the email to find a project number. Then it pops up a userform where the user can make multiple filing choice based on it's need.
It first save the project number using the VBA.SaveSetting method
It then returns object "objX" which is in fact the email object that is being send.
So in the code below I'm looping thru a list of public folders. The first part of the folders name is the project number. The project number I'm looking for is saved in "proj_folder" local variable.
So I'm looping thru all the folders to find the folder name that is beginning with "proj_folder" variable.
It's working fine except for the fact that it can get pretty slow if there's a lot of folders to loop thru.
Looking at the code below, would someone be kind enough to share a way to improve my looping speed. Right now it can take around 2 seconds to loop thru let's say 30 folders. Sometime it can get up to more than 200 folders.
Sub MoveProject(objX)
Dim objNS As Outlook.NameSpace
Dim projectParentFolder As Outlook.MAPIFolder
Dim objFolder As Outlook.MAPIFolder
Dim proj_folder As String
Dim intX As Long
'recall of the name of the folder saved from a previouly filed userform
proj_folder = VBA.GetSetting("mail filing", "num_projet", "num_proj", vbNullString)
sub_folder_1 = "Quebec"
sub_folder_2 = Left(proj_folder, 3)
Set objNS = Application.GetNamespace("MAPI")
Set projectParentFolder = objNS.Folders("Public Folder - UserAdress#server.com").Folders("All Public Folders").Folders(sub_folder1).Folders(sub_folder2)
'=============THIS IS THE PART WHERE I WOULD LIKE TO IMPROVE THE SPEED==============================
For intX = 1 To projectParentFolder.Folders.Count 'searching for folder name beginning
If Left(projectParentFolder.Folders.item(intX).Name, Len(proj_folder)) = proj_folder Then
Set objFolder = projectParentFolder.Folders.item(intX)
Exit For
End If
Next
objX.Move objFolder 'moving mail to objFolder
Set objX = Nothing
Set objFolder = Nothing
Set projectParentFolder = Nothing
Set objNS = Nothing
End Sub
You have too many dots in the loop for one. Here's a general VBA speed post of mine.
When setting properties or calling methods, each is a function call in the CPU. That means stack setup overhead. Function calls are slower than inline code. Use loops rather than functions in VBA for the same reason.
For a start don't specify all those properties over and over again. Unless you change them they don't change.
With Selection.Find
.ClearFormatting
.Replacement.ClearFormatting
.Forward = True
.Wrap = wdFindContinue
.Format = False
.MatchCase = False
.MatchWholeWord = False
.MatchByte = False
.MatchAllWordForms = False
.MatchSoundsLike = False
.MatchWildcards = False
.MatchFuzzy = False
For loop to go through each word pair
.Text = SrcText
.Replacement.Text = DestText
.Find.Execute Replace:=wdReplaceAll
Next
End With
Minimise Dots
So if you are interested in performance minimise dots (each dot is a lookup), especially in loops.
There are two ways. One is to set objects to the lowest object if you are going to access more than once.
eg (slower)
set xlapp = CreateObject("Excel.Application")
msgbox xlapp.worksheets(0).name
(faster because you omitt a dot every time you use the object)
set xlapp = CreateObject("Excel.Application")
set wsheet = xlapp.worksheets(0)
msgbox wsheet.name
The second way is with. You can only have one with active at a time.
This skips 100 lookups.
with wsheet
For x = 1 to 100
msgbox .name
Next
end with
String Concatination
And don't join strings one character at a time. See this from a VBScript programmer. It requires 50,000 bytes and many allocation and deallocation to make a 100 character string.
http://blogs.msdn.com/b/ericlippert/archive/2003/10/20/53248.aspx
Reading Properties
Don't reread properties that don't change especially if out of process or late bound. Put them into a variable. Reading variables is quick compared to an object(s) lookup (which is also a function call or at least two if late bound) and then a function call.
Variables
Constants and Literals are pretty much the same once compiled.
Const x = 5
msgbox x
is the same as
msgbox 5
Literals are inserted direct in code. String and object variables have managers, incuring overhead. Avoid creating variables for no reason. This is an example of a pointless and slow variable.
x = "This is a string"
msgbox x
compared to
const x = "This is a string"
msgbox x
or
msgbox "This is a string"
Object Types
Two concepts here - in or out of process and early or late binding.
exefiles are connected to out of process. All calls are marshalled over RPC (a networking protocol). Dllfiles are in process and function calls are made direct with a jump.
Early binding is set x = objecttype. Functions are looked up when you write the program. On execution the program is hard coded to jump to address stored in the vtable for that function.
Late binding is set x = createobject("objecttype"). Each function call goes like this. "Hi object do you have a print command". "Yes", it replies, "command number 3". "Hi object can you please do command number 3". "Sure, here's the result".
From Visual Basic Concepts (part of Help)
You can make your Visual Basic applications run faster by optimizing the way Visual Basic resolves object references. The speed with which Visual Basic handles object references can be affected by:
Whether or not the ActiveX component has been implemented as an in-process server or an out-of-process server.
Whether an object reference is early-bound or late-bound. In general, if a component has been implemented as part of an executable file (.exe file), it is an out-of-process server and runs in its own process. If it has been implemented as a dynamic-link library, it is an in-process server and runs in the same process as the client application.
Applications that use in-process servers usually run faster than those that use out-of-process servers because the application doesn't have to cross process boundaries to use an object's properties, methods, and events. For more information about in-process and out-of-process servers, see "In-Process and Out-of-Process Servers."
Object references are early-bound if they use object variables declared as variables of a specific class. Object references are late-bound if they use object variables declared as variables of the generic Object class. Object references that use early-bound variables usually run faster than those that use late-bound variables.
Excel Specific
See this link from a Microsoft person. This is excel specific rather than VBA. Autocalc and other calc options/screenupdating etc.
http://blogs.office.com/2009/03/12/excel-vba-performance-coding-best-practices/
.
Edit
I don't have Outlook installed so something like this.
Removed dots, changed to an enumerated For Each, and moved the len function outside the loop so it's not called over and over again.
Set projectParentFolder = objNS.Folders("Public Folder - UserAdress#server.com").Folders("All Public Folders").Folders(sub_folder1).Folders(sub_folder2)
prog_folder_len = Len(Prog_Folder)
For Each Fldr in ProjectParentFolder.Folders
If Left(Fldr.Name, prog_folder_len) = proj_folder Then
Set objFolder = Fldr
Exit For
End If
Next
A lot of collections can be accessed by name.
Does something like set objfolder = Fldr(Prog_Folder) or set objfolder = Fldr.item(Prog_Folder) not work?
Collections are implemented by the object. So one cannot know without installing the object the capabilities of a collection.
Also unlike For x = n to n, For each is also object implemented and might be faster than For x = n to n.
For the messages, you can use MAPIFolder.Items.Find/FindNext/Restrict or MAPIFolder.GetTable to find an item using a custom condition. Unfortunately there is nothing like that for the MAPIFolder.Folders collection in the Outlook Object Model - it was assumed that the number of subfolders is always small.
The best you can do with Outlook Object Model is pass the full name of the child folder to Folders.Item() - if there is an exact match (case insensitive), MAPIFolder.Folders.Item() will be able to return it without looping through all subfolders.
If you need a substring (or any other) match, you can either switch to Extended MAPI (C++ or Delphi only, not an option in VBA) and use MAPIFolder.Folders.RawTable to retrieve the IMAPITable MAPI interface that you can use to search for a subfolder. Or you can use Redemption (I am its author) and its MAPITable object. Your code would look something like the following:
set Table = CreateObject("Redemption.MAPITable")
Table.Item = projectParentFolder.Folders
Set Recordset = Table.ExecSQL("SELECT EntryID from Folder where Name like '" & proj_folder & "%' ")
If not Recordset.EOF Then
strEntryID = Recordset.Fields(0).Value
set objFolder = Application.Session.GetFolderFromID(strEntryID)
end If
Redemption version of the folder object (RDOFolder) also exposes Folders.Find/FindNext and Folders.Restrict methods (similar to the methods exposed by the Items collection in Outlook) that allow to specify an arbitrary search clause:
set Session = CreateObject("Redemption.RDOSession")
Session.MAPIOBJECT = Application.Session.MAPIOBJECT
set Folder = Session.GetFolderFromID(Application.ActiveExplorer.CurrentFolder.EntryID)
set subFolder = Folder.Folders.Find("Name LIKE 'MAPI%'")
if subFolder Is Nothing Then
MsgBox "No such subfolder"
else
MsgBox "Found subfolder named '" & subFolder.Name & "'"
end if
the following two lines
Dim curTasks As Tasks
Set curTasks = Application.Tasks
get the list of all current tasks and work like charm in vba-word but not in vba-excel.
is there a way to port it into vba-excel?
As I said in comments, the Excel object in VBA doesn't have the concept of tasks. You can do the below though in an Excel Module (although I'm still not sure why you would do it):
Dim curTasks As Tasks
Dim wrd As Word.Application
Set wrd = CreateObject("Word.Application")
Set curTasks = wrd.Tasks
NOTE: you have to add a reference to Microsoft Word Object Library to get this to work
I'm deploying an early bound styled VBA module that needs Scripting.Dictionary and RegExp.
The script, predictably, fails when it runs on another computer.
The user has to go to Tools->Reference in the VBA IDE and add a reference to those two libraries manually to make it work.
Hence lies the problem. Asking the non-technical end user to go to the IDE and manually add references is asking way too much of them.
The other alternative is to rewrite the whole (very long script written by someone else) to use late binding. I rather not take this path if there are other methods.
As an altervative, some people suggest adding a reference programatically like so:
Application.VBE.ActiveVBProject.References.AddFromFile [Path to library]
Is this the correct solution and if so are there any downsides of this strategy?
If not, are there other methods that will to enable the code to remain early bound yet does not require references to be added manually by the user.
Suggestions involving direct calls to the Win32/64 API are also welcome.
Thanks.
In my own limited environment (small # of other people using spreadsheets I develop, relatively standard machine setups), if I create the file and add the references, and then give a copy to someone else, they can open it with no problems and not have to do anything, so keep that in mind with this answer. (I'm wondering why that doesn't work for you.) Also, this was with Excel.
Rather than adding a reference from a file path, you might consider using the GUID property instead.
Here is some code I once used to automatically create references in a newly created workbook. (It's part of a script that would export code, references, and unit tests on worksheets to text for use with Subversion and then later reconstitute the workbook from the text files.) You might find it useful to your situation. (EH and cleanup removed to keep it short...)
'Export refs in existing workbook to text file
Private Sub exportRefs_(srcWbk As Workbook)
Dim fs As FileSystemObject
Set fs = New FileSystemObject
Dim tsout As TextStream
Set tsout = fs.CreateTextFile(fs.BuildPath(getTargetPath_(srcWbk), "refs.refs"))
Dim ref As Reference
For Each ref In Application.ThisWorkbook.VBProject.References
Call tsout.WriteLine(ref.GUID)
Next ref
'<EH + cleanup...>
End Sub
'Add refs to newly created workbook based on previously exported text file
Private Sub importRefs_(wbk As Workbook, path As String)
Dim fs As FileSystemObject
Set fs = New FileSystemObject
Dim tsin As TextStream
Set tsin = fs.OpenTextFile(path)
Dim line As String
Dim ref As Reference
While Not tsin.AtEndOfStream
line = tsin.ReadLine()
Set ref = Nothing
On Error Resume Next
Set ref = wbk.VBProject.References.AddFromGuid(line, 0, 0)
On Error GoTo 0
If ref Is Nothing Then
Debug.Print "add failed: " & line
End If
Wend
'<EH + cleanup...>
End Sub
Like, I said, limited environment, but hopefully it helps.