Excel VBA global variables "lifetime"? - excel

Sorry about the non descriptive Title, I just didn't know how to describe my goal.
I'm new at VBA and didn't yet understand how things really work.
I've written a function which gets a directory from the user, and displays data from the first file in the directory. Now, I want to add a "next" button.
When the "next" button is pressed, my code should display data from the next file in the directory.
I tried to use global variables but they seem to get initialized each time the button is pressed.
What is the best way to achieve my goal? Do I have to use the spreadsheet as memory and write and read everything from there? Or does Excel VBA have some other "live memory" mechanism?
Thanks,
Li

Globals will not normally be reinitialized when you click a button. They will be reinitialized if you recompile your VBA project. Therefore, while debugging, you may see a global being reinitialized.
You can use the spreadsheet as memory. One way to do this is to have a worksheet whose Visibility property you set to xlSheetVeryHidden (you can do this from the VBA project). This worksheet won't be visible to users, so your VBA application can use it to store data.

This could be approached many ways, as with any problem I guess!
You could break the problem up into two subroutines:
1) Retrieve all the file names in the selected directory and display the first file's data
2) If it's not the last file, get the next file's data and display it
You could use a global variable to store the filenames and an index to remember where you are up to in the collection of filenames.
Global filenames As Collection
Global fileIndex As Integer
Public Sub GetFilenames()
Dim selectedDirectory As String
Dim currentFile As String
selectedDirectory = "selected\directory\"
currentFile = Dir$(selectedDirectory)
Set filenames = New Collection
While currentFile <> ""
filenames.Add selectedDirectory & currentFile
currentFile = Dir$()
Wend
' Make sure there were files
If filenames.Count >= 1 Then
fileIndex = 1
' Call a method to display data
DisplayData(filenames(fileIndex))
Else
' No files
End If
End Sub
Public Sub GetNextFile()
' Make sure we have a filenames object
If Not filenames Is Nothing Then
If fileIndex < filenames.Count Then
fileIndex = fileIndex + 1
' Call the display method again
DisplayData(filenames(fileIndex))
Else
' Decide what to do after reaching the final file
End If
Else
' No filenames
End If
End Sub
I didn't include the DisplayData procedure as I'm not sure what type of files you're grabbing or what you are doing with them but if it were say excel files it could be something like:
Public Function DisplayData(filename As String)
Dim displayWb As Workbook
Set displayWb = Workbooks.Open(filename)
' Do things with displayWb
End Function
You could then set the macro of the button to "GetNextFile" and it will cycle through the files after each click. As for the lifetime of global variables, they only reinitialize when the VBA project is reset or when they are specifically initialized through a procedure or the immediate window.

Perhaps these two functions can also help you:
SaveSetting
GetSetting
as showed here: http://www.j-walk.com/ss/excel/tips/tip60.htm

Related

How to add (or copy) a new userform in VBA using code inside a Module (not Insert Button)

I need to be able to have identical userforms open simultaneously as a user needs to access info from a dataset to update entries.
Each userform will have the same text boxes, command boxes, etc.
So the userform needs to be of structure userform(i).
How do I create the userforms dynamically? The user selects ID# and new form is created.
I can handle the loops and passing the name into the form to update all the references, but it’s the making a dynamic copy in code that has me completely stumped.
I was thinking the code would be something like:
Dim frm As UserForm
Set frm = UserForms.Add
frm.Name = "NewName_i"
I would then have a loop that would look for how many "NewName_i" exists using Forms.Count and Forms.Visible and then would just add one more as it is needed.
But I cannot get past the creating of a new form dynamically with code.
If you need to show the same form (e.g.) 5 times then you can do something like this:
Dim colForms As Collection 'holds references to the opened forms
Sub TestForms()
Dim i As Long
Set colForms = New Collection
For i = 1 To 5
Dim f As frmTestx
Set f = New frmTestx
f.Show vbModeless 'if not modeless then code stops here...
colForms.Add f
Next i
End Sub

WorkbookOpen event sometimes not firing - event used for set global variables

I have a really basic problem with my projects and I would like to know which approach is the best. I like to use (hated) globals, only for a few the most important objects in a workbook.
I am declaring e.g. my data tables in a such way:
'#Folder("Main")
Option Exclicit
Public tblDatabase As Listobject
Public tblReport As Listobject
Sub setMyTables()
Set tblDatabase = wsDatabase.ListObjects("tDatabase")
Set tblReport = wsReport.ListObjects("tReport")
End Sub
In the past I used this macro before actions on the table, e.g.:
Function getIdFromDatabaseTable() As Variant
' set variable-object to use
setMyTables <-- I used to table-setting-sub in every
macro which requires one of my table
' get ID from table
Dim arr As Variant
arr = tblDatabase.ListColumns("ID").DataBodyRange.Value2
' assign array to function result
getIdFromDataTable = arr
End Function
But why I had to begin almost every macro with calling setMyTables() macro? So I've started to use workbook open event to set my object variables:
[code in ordinary Module]
'#Folder("Main")
Option Exclicit
Public tblDatabase As Listobject
Public tblReport As Listobject
And call setMyTables() macro in Workbook_Open() event code. And here my problem is:
[TLTR] Setting variable-objects in Workbook-Open event seems unrielable. It seems it is not firing sometimes. I am sure that no macro error would reset the project and 'clear' already set variables, because sometimes it throws error on the very first macro run. It is not working occasionally and I don't know what pattern behind it is, I send Excel workbooks to my clients, and it's hard to debug what's realy going on there.
Additional comments
I've just read that this could happen if file is not in trusted localizations, I would like get to know best approach to handle declaring the most used objects globally (if possible without modifying someones trusted folders or another local-PC settings).
I know that I can set a 'flag' bool variable such as wasWorkbookOpenEventFired, but I would have to call checking function or make ifs on almost every Sub or Function in a workbook. So I think it isn't good solution too. Thanks for hints!
You'd have more robust results if you define public functions which each return a specific table, and use those instead of global variables:
Function DatabaseTable() As ListObject
Static rv As ListObject '<< cache the table here
'if your code gets reset then this will just re-cache the table
If rv Is Nothing then Set rv = wsDatabase.ListObjects("tDatabase")
Set DatabaseTable = rv
End Function

Call helper function within parent without redefining objects from parent in the helper

I'm working in Excel with VBA to collect data for a table I'm building I have to go out to a TN3270 emulator to get it. In order to work with with the emulator I have to define a few objects to do the work. I also have a few helper functions that are used by multiple functions to navigate to different screens in the emulator. So far in order to use them I have had to copy the object definitions into those functions to get them to work. This works most of the time but occasionally (and in a way I cant predictably replicate) I get an error when the helper is recreating a particular object to use.
Option Explicit
Public Sub gather_data()
Dim TN_Emulator As Object
Dim Workbook As Object
Set TN_Emulator = CreateObject("TN_Emulator.Program")
Set Workbook = ActiveWorkbook
Dim string_from_excel As String
#for loop to go through table rows
#put value in string_from_excel
If string_from_excel = some condition
go_to_screen_2
#grab and put data back in excel
Else
go_to_screen_3
#grab and put data back in excel
End If
go_to_screen_1
#next loop logic
End Sub
Public Sub go_to_screen_1()
Dim TN_Emulator As Object
#the next step occasionally throws the error
Set TN_Emulator = CreateObject("TN_Emulator.Program")
#send instructions to the emulator
End Sub
Is there a way to import the existing objects (that get created and used without any errors) without redefining them into the helper functions to avoid this problem? I have tried searching in google but I don't think I'm using the right search terms.
First thanks goes to #JosephC and #Damian for posting the answer for me in the comments.
From JosephC 'The Key words you're looking for are: "How to pass arguments to a function".', and he provided the following link ByRef vs ByVal describing two different ways to pass arguments in the function call.
And from Damian the solution to my immediate concern. Instead of declaring and setting the objects that will be used in body of the helper function. Place the object names and types in the parentheses of the initial helper name, and when calling the helper from the other function also in the parentheses, shown below.
Option Explicit
Public Sub gather_data()
Dim TN_Emulator As Object
Dim Workbook As Object
Set TN_Emulator = CreateObject("TN_Emulator.Program")
Set Workbook = ActiveWorkbook
Dim string_from_excel As String
#for loop to go through table rows
#put value in string_from_excel
If string_from_excel = some condition
Call go_to_screen_2(TN_Emulator)
#grab and put data back in excel
Else
Call go_to_screen_3(TN_Emulator)
#grab and put data back in excel
End If
Call go_to_screen_1(TN_Emulator)
#next loop logic
End Sub
Public Sub go_to_screen_1(TN_Emulator As Object)
#send instructions to the emulator
End Sub
I believe I understood the instructions correctly, and have successfully tested this for my-self. I also passed multiple objects in the helper function definition and calls as needed for my actual application, in the same order each time Ex.
Sub go_to_screen_1(TN_Emulator As Object, ConnectionName As Object)
and
Call go_to_screen_1(TN_Emulator, ConnectionName)

VBA best practices for modules relative to modeless userforms

I came across this similar issue and read the replies: Modeless form that still pauses code execution
I have been attempting to apply in my own situation the suggestion provided by David Zemens. In my situation, I cannot seem to find an approach that incorporates Mr. Zemen's suggestion without also utilizing a GoTo.
I am wondering if there is a better or more elegant solution.
Here is an outline of what I am doing:
I have a UserForm with a Command Button that begins the code execution that will perform several actions on multiple Excel workbooks. As such, there are a number of blocks of code and the successful completion of one block of code allows for the execution of the subsequent block of code.
At a certain point, depending on the situation, the code might require User input; in other situations, the needed data is obtainable from an Excel. If input is needed from the User, another UserForm is displayed.
The User may need to view several different Excel sheets before entering the input, so the UserForm is modeless. So the code comes to a stop until the User enters the needed input and clicks another Command Button.
It is at this point I am having trouble: how to resume the program flow. Is the only way to 'pick-up where it left-off' is by using a GoTo statement? Or is there some way to organize the modules so there is a single consistent program flow, defined in one spot and not duplicated from the point at which User input might be needed?
Here is my take on the problem . Hope I understood the problem correctly.
Assumptions:
There are two user forms.
UserForm1 with a button to start the processing.
UserForm2 with a button to supply intermediate input.
A sub inside a module to start/ launch UserForm1.
VBA Code (for the sub routine)
Sub LaunchUserForm1()
Dim frm As New UserForm1
'/ Launch the main userform.
frm.Show vbModeless
End Sub
VBA Code (for UserForm1)
Private Sub cmdStart_Click()
Dim i As Long
Dim linc As Long
Dim bCancel As Boolean
Dim frm As UserForm2
'/ Prints 1 to 5 plus the value returned from UserForm2.
For i = 1 To 5
If i = 2 Then
Set frm = New UserForm2
'/ Launch supplementary form.
frm.Show vbModeless
'<< This is just a PoC. If you have large number of inputs, better way will be
' to create another prop such as Waiting(Boolean Type) and then manipulate it as and when User
' supplies valid input. Then validate the same in While loop>>
'/ Wait till we get the value from UserForm2.
'/ Or the User Cancels the Form with out any input.
Do While linc < 1 And (linc < 1 And bCancel = False)
linc = frm.Prop1
bCancel = frm.Cancel
DoEvents
Loop
Set frm = Nothing
End If
Debug.Print i + linc
Next
MsgBox "User Form1's ops finished."
End Sub
VBA Code (for UserForm2)
Dim m_Cancel As Boolean
Dim m_prop1 As Long
Public Property Let Prop1(lVal As Long)
m_prop1 = lVal
End Property
Public Property Get Prop1() As Long
Prop1 = m_prop1
End Property
Public Property Let Cancel(bVal As Boolean)
m_Cancel = bVal
End Property
Public Property Get Cancel() As Boolean
Cancel = m_Cancel
End Property
Private Sub cmdlinc_Click()
'/Set the Property Value to 10
Me.Prop1 = 10
Me.Hide
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
'/ Diasble X button
Me.Cancel = True
Me.Hide
Cancel = True
End Sub
OK so here are my thoughts.
You have a userform frmSelectUpdateSheet which you wish to use in order to allow the user to select the sheet, when the sheet can't be determined programmatically. The problem is that if you do .Show vbModeless (which allows the user to navigate the worksheet/s), then code continues to execute which either leads to errors or otherwise undesired output.
I think it's possible to adapt the method I described in the previous answer. However, that's out of the question here unless you're paying me to reverse engineer all of your code :P
Assuming you have a Worksheet object variable (or a string representing the sheet name, etc.) which needs to be assigned at this point (and that this variable is Public in scope), just use the CommandButton on the form to assign this based on the selected item in the frmSelectUpdateSheet list box.
This is probably a superior approach for a number of reasons (not the least of which is trying to avoid application redesign for this sort of fringe case), such as:
This keeps your form vbModal, and does prevent the user from inadvertently tampering with the worksheet during the process, etc.
Using this approach, the thread remains with the vbModal displayed frmSelectUpdateSheet, and you rely on the form's event procedures for control of process flow/code execution.
It should be easier (and hence, cheaper) to implement; whether you're doing it yourself or outsourcing it.
It should be easier (and hence, cheaper) to maintain.
NOW, on closer inspection, it looks like you're already doing this sort of approach with the cmdbtnSelect_Click event handler, which leads me to believe there's a related/follow-up problem:
The sheet names (in listbox) are not sufficient for user to identify the correct worksheet. So if the user needs the ability to "scroll" the sheet (e.g., to review data which does not fit in the window, etc.), then add some spinner buttons or other form controls to allow them to navigate the sheet.

importing data from many workbooks in different folders

I am looking to import/copy data from many workbooks into a summary workbook. The workbooks are arranged in different sub-folders, I.e
C:\data1\results_2001.xlm
C:\data2\results_2002.xlm
C:\data3\results_2003.xlm
The names are similar but differ slightly to differentiate them. At present, I import the files individually, and I want to automate the process. The results files (above) are amongst other excel files so I cannot target them by file type.
How would I import these files by partial file name?
One option is to create an array of the filepaths to your excel sheets and then loop over the array and get the data you want into your summary sheet.
Sub CreateSummary()
Dim wkbs() As Variant, wkb As Integer, owb As Workbook
wkbs = Array("C:\data1\results_2001.xlm", "C:\data2\results_2002.xlm", "C:\data3\results_2003.xlm")
For wkb = 0 To UBound(wkbs)
Set owb = Application.Workbooks.Open(wkbs(wkb)) //Open each workbook
With owb
//Get the data you want into your summary workbook
.Close
End With
Next wkb
End Sub
Another way, especially if only a one time operation: Go into Cmd.exe, do a Dir for the files you're looking for, and send it to a text file (eg, something like dir c:\results_*.xlm /s /b > c:\myList.txt). Then import the text file to your worksheet, step thru each cell in the list, opening each workbook in turn.
You can do it in any languages, but for you who is asking this question, i think it's gonna be a little challenging, so here is what you need to do :
create a function that will list files/folders from given path
loop through all items found, if it's a folder , recursive it
if the item fits your target(name, extension, ...) , read it and load the content to the summary
something like this, i believe you will achieve this easily using VBA, look here
Literally, it will be like this, please note that this is not valid code, just something i write down to help you figure it out :
function loopthepath (string pathtoloop)
foreach(dirItem item in pathtoloop.getdirItem)
{
if (item is folder)
{
loopthepath(pathtoloop + item)
}
else
{
if (item fits mydescription)
{
load the content to the summary
}
}
}

Resources