EDIT at the bottom of the question
I have a function in my code, that gets a dictionary of samples, fills in data from another workbook and returns filled dictionary. Everything worked fine, but once I changed the opening of the file to be ReadOnly, I experience problems.
Here is the code (which has been simplified to remove redundant parts):
Function get_samples_data(ByVal instructions As Scripting.Dictionary, ByRef patients_data As Scripting.Dictionary) As Scripting.Dictionary
'takes a dictionary of samples and fills their data from the file <0 GL all RL>
Dim wb As Workbook
Dim ws As Worksheet
Dim data_start As Long
Dim data_end As Long
Dim rw As Range
Dim rw_nr As String
'open <GP all> in ReadOnly mode based on path and filename in specific cells
Application.ScreenUpdating = False
Set wb = Workbooks.Open(ThisWorkbook.Sheets(1).Cells(13, 2).Value2 & ThisWorkbook.Sheets(1).Cells(13, 1).Value2, False, True)
Set ws = wb.Worksheets("ALL")
'get row nr. of the first and the last sample to export
data_start = ws.Columns("A:A").Find(what:=instructions("from_sample"), LookIn:=xlValues, LookAt:=xlWhole, SearchOrder:=xlByRows).Row
data_end = ws.Columns("A:A").Find(what:=instructions("to_sample"), LookIn:=xlValues, LookAt:=xlWhole, SearchOrder:=xlByRows).Row
'main loop
For i = data_start To data_end
Set rw = ws.Rows(i)
rw_nr = rw.Cells(1, 1).Value
If rw.Cells(1, 11).Value = instructions("group") Then
If patients_data.Exists(rw_nr) Then
Set patients_data(rw_nr) = fetch_sample_data(rw, patients_data(rw_nr))
End If
End If
Next
'close <GP all> without saving
wb.Close (False)
Set get_samples_data = patients_data
End Function
When I debugged, I noticed, that the data is lost on the call of wb.Close(False). Until that point data is intact, but once the source workbook is closed, the data (which is a range object) is turned blank. Not set to nothing, which happens when the data is not found in the source workbook, but all properties of the range object can be seen in debugger, but all have a value of .
Before I changed the openmode to ReadOnly, everything worked and data stayed there.
What did I miss? Why are data, stored in a different variable, lost?
EDIT:
Fetch sample data indeed returns a range object.
Private Function fetch_sample_data(ByVal rw As Range, ByRef sm As sample) As sample
Dim data As Range
Set data = Range(rw.Cells(1, 19), rw.Cells(1, 63))
Set sm.data = data
Set fetch_sample_data = sm
End Function
I tried changing the order of closing and setting the return value, but the error prevails.
Is it so then, that a Range object is always only a reference to a range in a worksheet? If I want the data to stay, do I need to change all Range objects in question to arrays? Or is there a way to create a Range object independent of a workbook (I do not want to copy the range into any sheet in the main workbook carrying the macro)?
Below is the main sub, as #Pᴇʜ asked for it. I will not add the remaining functions, because the whole code is scattered over 1 form, 2 modules and 14 classes (many carrying long methods).
The two commented open commands are those that caused everything to work properly. The closing commands were at the end of main sub, so in regards to the comment of #Pᴇʜ, if range object is always only a reference to an actual range of cells, they were available for the whole duration of the program.
Sub RL_creator_GP_main()
Dim instructions As New Scripting.Dictionary
Dim samples As Scripting.Dictionary
Dim result_list As Variant
Dim rep As cReport
Dim scribe As New descriptor
Application.ScreenUpdating = False
'get instructions from inputboxes (group, from sample, to sample)
Set instructions = procedures.input_instructions()
If instructions.Exists("terminated") Then
Exit Sub
End If
'get <GP all> and <RL headers> ready
'Call procedures.prepare_file("GP all.xlsx", pth:=ThisWorkbook.Sheets(1).Cells(12, 2).Value)
'Call procedures.prepare_file("RL headers.xlsx", pth:=ThisWorkbook.Sheets(1).Cells(13, 2).Value)
'get patients data from <RL headers>, closes the file afterwards
Set samples = procedures.get_patients_data(instructions)
'get patients data from <GP all>, closes the file afterwards
Set samples = procedures.get_samples_data(instructions, samples)
because samples is submitted ByRef to get_samples_data you don't need to return it:
Sub RL_creator_GP_main()
'your code here …
'get patients data from <RL headers>, closes the file afterwards
Set samples = procedures.get_patients_data(instructions)
'get patients data from <GP all>, closes the file afterwards
procedures.get_samples_data instructions, samples 'this call will change the original samples because it is ByRef!
In fetch_sample_data you add a range to your dictionary. But a Range object is only a reference to the worksheet and does not contain data itself. So instead of that turn the range into an array to add the actual data instead of only a reference:
Private Sub fetch_sample_data(ByVal rw As Range, ByRef sm As sample)
Dim data() As Variant
data = Range(rw.Cells(1, 19), rw.Cells(1, 63)).Value
Set sm.data = data
'again you don't need a function to return the sample as it is ByRef
End Sub
Finally get_samples_data should be a sub not a function. And call fetch_sample_data as a sub like fetch_sample_data rw, patients_data(rw_nr)
Sub get_samples_data(ByVal instructions As Scripting.Dictionary, ByRef patients_data As Scripting.Dictionary)
'takes a dictionary of samples and fills their data from the file <0 GL all RL>
Dim wb As Workbook
Dim ws As Worksheet
Dim data_start As Long
Dim data_end As Long
Dim rw As Range
Dim rw_nr As String
'open <GP all> in ReadOnly mode based on path and filename in specific cells
Application.ScreenUpdating = False
Set wb = Workbooks.Open(ThisWorkbook.Sheets(1).Cells(13, 2).Value2 & ThisWorkbook.Sheets(1).Cells(13, 1).Value2, False, True)
Set ws = wb.Worksheets("ALL")
'get row nr. of the first and the last sample to export
data_start = ws.Columns("A:A").Find(what:=instructions("from_sample"), LookIn:=xlValues, LookAt:=xlWhole, SearchOrder:=xlByRows).Row
data_end = ws.Columns("A:A").Find(what:=instructions("to_sample"), LookIn:=xlValues, LookAt:=xlWhole, SearchOrder:=xlByRows).Row
'main loop
For i = data_start To data_end
Set rw = ws.Rows(i)
rw_nr = rw.Cells(1, 1).Value
If rw.Cells(1, 11).Value = instructions("group") Then
If patients_data.Exists(rw_nr) Then
fetch_sample_data rw, patients_data(rw_nr)
End If
End If
Next
'close <GP all> without saving
wb.Close (False)
End Sub
Background explanation
Calling functions and subs:
First of all the Call statement is not needed. Parameters in functions are always in parenhesis, the function is used to return a value.
Result = MyFunction(Param1, Param2) ' functions return a result and parameters are in parentesis
MySub Param1, Param2 ' subs don't return a result and don't use parentesis
Call MySub(Param1, Param2) ' But with the Call statement they need parentesis
What does ByRef do:
If you declare a parameter ByRef that means you don't submit data to the sub but only a reference (By Reference) to that data in the memory. So if you have the following sub:
Sub MySub(ByVal Param1, ByRef Param2)
Param1 = 1
Param2 = 2
End Sub
And use it like
Sub Example()
Dim Var1 As Long: Var1 = 10
Dim Var2 As Long: Var2 = 20
MySub Var1, Var2 'note Var2 is submitted ByRef!
Debug.Print Var1, Var2 'returns 10, 2 the value in Var2 got changed by MySub without returning anything
End Sub
So when you submit the variabe by reference that means MySub changes the value in Var2 when performing Param2 = 2 because Param2 and Var2 reference the same space in memory. While if you submit ByVal (by value) you actually make a copy of the data in the memory and Param1 and Var1 reference different places in the memory.
That is why you don't need a function to return something if you submit it ByRef you already changed the data in the memory.
So in your code if you declare Sub get_samples_data(ByVal instructions As Scripting.Dictionary, ByRef patients_data As Scripting.Dictionary) then calling it like procedures.get_samples_data instructions, samples makes patients_data and samples point to the same space in memory. So because the data is only once in the memory and there is only 2 links pointing to them any changes made in one of the links actually edits the exact same data in memory. Therefore you don't need to return data.
Related
I am very new to VBA, having started programming it yesterday. I am writing a data processing program which requires keeping track of two cells, one on each spreadsheet. The code which reproduces the errors I am experiencing is below. When I call the sub moveCell() in sub Processor(), nothing happens to DIRow and DIColumn, and the code spits out error 1004 at the line indicated. I have tried using DICell = DICell.Offset(), but it returns the same error.
How can I redefine a Range variable to be a different cell?
'<<Main Processor Code>>'
Sub Processor()
Dim PDRow As Integer
Dim PDColumn As Integer
Dim DIRow As Integer
Dim DIColumn As Integer
PDRow = 1
PDColumn = 1
DIRow = 1
DIColumn = 1
Dim PDCell As Range
Dim DICell As Range
Set PDCell = Worksheets("Processed Data").Cells(PDRow, PDColumn)
Set DICell = Worksheets("Data Input").Cells(DIRow, DIColumn)
Call moveCell(2, 0, "Data Input")
End Sub
'<<Function which moves the cell which defines the range>>'
Sub moveCell(r As Integer, c As Integer, sheet As String)
If sheet = "Processed Data" Then
PDRow = PDRow + r
PDColumn = PDColumn + c
Set PDCell = Worksheets("Data Input").Cells(PDRow, PDColumn)
ElseIf sheet = "Data Input" Then
DIRow = DIRow + r '<<<<<<This line does nothing to DIRow's value
DIColumn = DIColumn + c
Set DICell = Worksheets("Data Input").Cells(DIRow, DIColumn) '<<<<<<This line causes error 1004
End If
End Sub
As far as I can tell, you could instead use a quick Function instead. There doesn't seem to be any difference in your If statement results in the moveCell() function, except which worksheet you're using.
We can make this simpler by referring to the Range you're passing to moveCell.
Option Explicit ' forces you to declare all variables
Sub something()
Dim PDCell As Range
Set PDCell = Worksheets("Processed Data").Cells(1, 1)
Dim DICell As Range
Set DICell = Worksheets("Data Input").Cells(1, 1)
PDCell.Select ' can remove
Set PDCell = moveCell(2, 0, PDCell, PDCell.Worksheet.Name)
PDCell.Select ' can remove
Worksheets(DICell.Worksheet.Name).Activate ' can remove
DICell.Select ' can remove
Set DICell = moveCell(5, 0, DICell, DICell.Worksheet.Name)
DICell.Select ' can remove
End Sub
Function moveCell(rowsToMove As Long, colsToMove As Long, cel As Range, ws As String) As Range
Set moveCell = Worksheets(ws).Cells(cel.Row + rowsToMove, cel.Column + colsToMove)
End Function
I've included some rows you don't need (which I've marked with a comment afterwards), but that will show you how the routine works. You can step through with F8 to help see it step-by-step.
Edit: Although, you don't need a separate function at all. Just use OFFSET().
Set PDCell = ...whatever originally
Set PDCell = PDCell.Offset([rows],[cols])
I have a Word file with approximately 10 pages and 20 tables. Some of these tables have checkboxes. I want to copy these tables into an Excel file.
The following code copies all tables from my Word file into my Excel file:
Sub Import()
Option Explicit
Dim wb As Workbook
Dim sh As Worksheet
Dim sheet As Worksheet
Dim lzeile As Integer
Set wb = ActiveWorkbook
Set sh = wb.ActiveSheet
Set sheet = wb.Worksheets("Tabelle1")
Dim Btxt As Object
Set Btxt = CreateObject("Word.Application")
Btxt.Visible = True
Btxt.documents.Open "C:\Users\*.doc" '*=path
lzeile = 0
For i = 1 To 20
Btxt.ActiveDocument.Tables(i).Range.Copy
Application.Goto sheet.Cells(1 + lzeile, 1)
sheet.PasteSpecial Format:="HTML", Link:=False, DisplayAsIcon:=False
lzeile = sheet.Cells(Rows.Count, 1).End(xlUp).Row
lzeile = lzeile + 1
sheet.Cells(lzeile, 1) = "Tabelle" & i
Next i
Btxt.Quit
Set Btxt = Nothing
Set wb = Nothing
Set sh = Nothing
End Sub
It does not copy checkboxes or the value (0 = not checked / 1 = checked) of the checkbox.
I can write the value of a checkbox into a cell in my excel sheet with this line:
sheet.Cells(j, 10) = Btxt.ActiveDocument.Tables(i).FormFields.Item("Kontrollkästchen" & j).Result
With a loop j over all "Kontrollkästchen" (german translation of contentcontrol or formfield item) so basically the name of all formfield items in this Word file.
How can I get the position of these formfield items or identify which formfield item / ContentControl is in which table?
I tried to go through all rows and columns in each table because none of them are larger than 10x10. But I can´t find a way to check if a checkbox is maybe in table 3 on column 5 row 5 and then read the name of this checkbox to a safe the value (0 / 1) in the Excel cell on the same position in my copied table.
The solution depends on whether they're formfields or content controls.
Assuming they're formfields:
Sub Demo()
Dim i As Long, j As Long, Rng As Range
With ActiveDocument
For i = .FormFields.Count To 1 Step -1
With .FormFields(i)
If .Type = wdFieldFormCheckBox Then
j = Abs(.CheckBox.Value)
Set Rng = .Range
.Delete
Rng.Text = j
End If
End With
Next
End With
End Sub
Assuming they're content controls:
Sub Demo()
Dim i As Long, j As Long, Rng As Range
With ActiveDocument
For i = .ContentControls.Count To 1 Step -1
With .ContentControls(i)
If .Type = wdContentControlCheckBox Then
j = Abs(.Checked)
Set Rng = .Range
.Delete
Rng.Text = j
End If
End With
Next
End With
End Sub
For the sake of simplicity and clarity, the sample code below leaves out the parts having to do with Excel, as well as creating the instance of the Word Application. It shows only how to access the Word document's checkboxes and convert those to static values.
At the end, also, the document is closed without saving changes, which means forms protection and the checkboxes should be left intact - the macro will not have affected them.
Note: You should have Option Explicit at the top of the code page, not inside a "Sub".
How it works
The document to be processed is opened and at that moment set to an object (doc). Use this instead of ActiveDocument as it will be much clearer and, in case the user would try to do something, won't affect macro execution.
If the document has forms protection, this must be turned off in order to delete the checkboxes and insert static values.
Then all the form fields are looped. If they are checkboxes, the value is determined, the checkbox removed and the value assigned to the range the checkbox occupied.
After this has completed comes the code to transfer data to Excel. Then the document is closed without saving changes.
Sub ConvertCheckBoxesToValues()
Dim ff As Object ' Word.FormField
Dim doc As Object ' Word.Document
Dim cbValue As String
Dim rngFF As Object ' Word.Range
Set doc = Btxt.Documents.Open("C:\Users\*.doc") '*=path
If doc.ProtectionType <> -1 Then 'wdNoProtection
doc.Unprotect
End If
For Each ff In doc.FormFields
If ff.Type = 71 Then 'wdFieldFormCheckBox
If ff.CheckBox.value = True Then
cbValue = "1"
Else
cbValue = "0"
End If
Set rngFF = ff.Range
ff.Delete
rngFF = cbValue
End If
Next
'Transfer the information to Excel, then
doc.Close 0 'wdDoNotSaveChanges
End Sub
I don't have much experience with VBA so it's been difficult to troubleshoot this. When running the code, it outputs Array(i<=i) instead of Array(i)
I've tested the for condition and found Array(0) properly returns the result. However Array(1) will print Array(1) with Array(0) and so on.
The goal of this code is to group worksheets based on their name and print them to pdfs based on grouping, i.e. all sheets starting with I1 to a single pdf.
Sub Test()
FolderPath = "C:\Example"
Dim aWS()
Dim n As Integer
Dim ws As Worksheet
Dim DocTypes()
DocTypes = Array("I1","I2","I3")
For i = LBound(DocTypes) To UBound(DocTypes)
For Each ws In Worksheets
If Left(ws.Name, 2) = DocTypes(i) Then
n = n + 1
ReDim Preserve aWS(1 To n) 'Add 1 to to array length
aWS(n) = ws.Name 'Add worksheet name meeting If condition
End If
Next ws
Sheets(aWS).Select
ActiveSheet.ExportAsFixedFormat Type:=xlTypePDF, Filename:=FolderPath &
DocTypes(i), _
openafterpublish:=True, ignoreprintareas:=False
Next i
End Sub
What I expect is:
i = 0 to 2
First Array(i) = "I1" so output all sheets beginning with "I1" as a pdf
Then move to i = 1
Here Array(i) = "I2" so output all sheets beginning with "I2" as a pdf
However when I step forward it doesn't seem to be following this logic and I don't understand why. I'm thinking it has to do with the selection, it would follow that if i=0 was selected, then i=1 was added to the selection this problem would make sense. I've tried re-selecting a single sheet right before Next i to force past this but it didn't work. This leads me to think I've made a logical mistake in my for loops.
You might not be aware but you can use a variant as a control variable in a for each to iterate an array of variants. Your use of redim to extend an array by 1 item suggests that you should be using a scripting dictionary as an intermediate step to your array. The .Items method of a scripting dictionary returns an array of items so it is easy to get your array that you use subsequently. Here is your code revised to use a scripting.dictionary and a variant control variable. In your specific case we are basically using the scripting.dictionary as a list by making the key and the item the same thing.
Option Explicit
Sub Test()
Const FolderPath As String = "C:\Example"
Dim aWS As Scripting.Dictionary
Dim ws As excel.Worksheet
Dim DocTypes() As Variant
Dim DocType As Variant
DocTypes = Array("I1", "I2", "I3")
For Each DocType In DocTypes
Set aWS = New Scripting.Dictionary
For Each ws In Worksheets
If DocType = left(ws.Name, 2) Then
aWS.Add Key:=ws.Name, Item:=ws.Name
End If
Next ws
Sheets(aWS.Items).Select
ActiveSheet.ExportAsFixedFormat _
Type:=xlTypePDF, _
FileName:=FolderPath & DocType, _
openafterpublish:=True, _
ignoreprintareas:=False
Next
End Sub
Use Selection.ExportAsFixedFormat etc instead of ActiveSheet. The ActiveSheet is always only one sheet while your selection comprises many.
Upon further study I find that you may have to include making a selection for each of the worksheets, like Ws.UsedRange.Select. Take a look at this thread.
Here is an example setting:
Dim wb As Workbook
Dim ws As Worksheet
Dim rn As Range
More specifically in my scenario:
The call:
Select Case CheckInDb(Range("Username"), "\..\_DataBase_\", "UserBase.xlsm", Sheets("UD_Base"), Range("UD_Base[U_ID]"))
The function header:
Function CheckInDb(What As Variant, Folder As String, FileName As String, Ws As Worksheet, Rn As Range)
I wanted to do something like this within the function (after opening the file, etc.):
(note: Wb is generated within the function)
CheckInDb = IsError(Application.Match(What,Wb.Ws.Rn, 0))
I have tried in several ways to precombine them step by step instead, but did not succeed with any
for example
set ws = wb.Sheets("RangeAdding")
would work manually, but Ws=Wb.Ws NOT. How to work around this, to achieve all 3 parts coming from variables?
Adding more information
I will post my full function to show what the real issue is. I have modded it a bit according to your suggestions, and the problem is:
The Workbook only gets opened inside the function, however if it is not open already when I write the full range in the calling it will give me an error, that it does not exist. Therefore I wanted to first ever use the range "live" inside the function, when the reference is already open.
Sub checkUser()
Select Case CheckInDb(Range("Username"), "\..\_DataBase_\", "UserBase.xlsm", Workbooks("Userbase.xlsm").Sheets("UD_Base").Range("UD_Base[U_ID]"))
Case True: Range("Status_U").Value = ("Szabad")
Case False: Range("Status_U").Value = ("Foglalt")
End Select
End Sub
Function CheckInDb(What As Variant, Folder As String, FileName As String, Rng As Range)
Dim Wb As Workbook
Dim wasOpen As Boolean
Dim File As String, Path As String
' Relative path:
Path = ThisWorkbook.Path & Folder
File = Path & FileName
On Error Resume Next
Set Wb = Workbooks(FileName)
wasOpen = True
On Error GoTo 0
If Wb Is Nothing Then
Set Wb = Workbooks.Open(File, , True) 'with settings: (File, true,true, , PW)
wasOpen = False
End If
'...
CheckInDb = IsError(Application.Match(What, Rng, 0))
'...
Select Case wasOpen
Case True
'Wb.Save
Case False
'Wb.Save
Wb.Close (False)
End Select
Set Wb = Nothing
End Function
Your rn object already knows which Worksheet object it belongs to. (You can use rn.Parent to refer to that worksheet.) And the worksheet knows which Workbook object it belongs to. (You can use rn.Parent.Parent to refer to that workbook.)
So your code should be:
CheckInDb = IsError(Application.Match(What, Rn, 0))
This means there is no need to pass the workbook and worksheet information to your function (because it is inherent in the Range you are passing), i.e.:
Select Case CheckInDb(Range("Username"), Range("UD_Base[U_ID]"))
and
Function CheckInDb(What As Variant, Rn As Range)
Based on your edit to the question, which now makes it clear that all the objects in the calling procedure don't exist when you execute the call (and therefore the call fails), you should pass the names of your workbook (as you already do), worksheet and range, i.e.:
Sub checkUser()
Select Case CheckInDb(Range("Username"), _
"\..\_DataBase_\", "UserBase.xlsm", _
"UD_Base", _
"U_ID")
Case True: Range("Status_U").Value = ("Szabad")
Case False: Range("Status_U").Value = ("Foglalt")
End Select
End Sub
Function CheckInDb(What As Variant, _
Folder As String, FileName As String, _
wsName As String,
rngName As String) As Boolean
'...
'... existing code to create wb object
'...
Dim rn As Range
Set rn = wb.Worksheets(wsName).Range(rngName)
CheckInDb = IsError(Application.Match(What, Rn, 0))
I am working on a complex model that needs to lookup values in a series of distinct tables. If I embed all of the information in the model itself, then the file quickly becomes unwieldy. I am hoping to find a solution where I can have a series of CSV files that contain all of the lookup tables, and then have my VBA code just quickly read each CSV file as necessary and return the appropriate value.
My initial thought is to read each CSV file in working memory as needed, lookup the necessary values, then discard the information once the lookups are complete. Is that most efficient way to do it?
Here's an idea which might work for you: load your csv file into a variant array the first time it's required, then subsequent calls will use the cached data. You can lookup values in any column and return the corresponding value from any other column.
EDIT: updated to show how to populate lookup arrays from CSV files
Sub Tester()
Dim arr1, arr2
arr1 = CsvToArray("D:\Analysis\tmp\Data1.csv")
arr2 = CsvToArray("D:\Analysis\tmp\Data2.csv")
Debug.Print TestLookup(arr1, "lookup1", 2, 1)
Debug.Print TestLookup(arr2, "lookup2", 3, 1)
'bunch more lookups...
End Sub
Function TestLookup(arr, val, lookincol As Integer, returnfromcol As Integer)
Dim r
r = Application.Match(val, Application.Index(arr, 0, lookincol), 0)
If Not IsError(r) Then
TestLookup = arr(r, returnfromcol)
Else
TestLookup = "Not found" 'or some other "error" value
End If
End Function
Function CsvToArray(filepath As String) As Variant
Dim wb As Workbook
Application.ScreenUpdating = False
Set wb = Workbooks.Open(filepath)
CsvToArray = wb.Sheets(1).Range("A1").CurrentRegion.Value
wb.Close False
End Function
if you really have to do it in excel, then this is a method:
Function GetData(This As String, ResultCol As Integer)
Dim LastRow As Long
Application.ScreenUpdating = False 'Turn off screen refreshing
Workbooks.Open Filename:="E:\my_files\tables.csv" 'Open the CSV file
LastRow = Cells.Find("*", SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Row 'used for range in vlookup
GetData = Application.WorksheetFunction.VLookup(This, Range(Cells(1, 1), Cells(LastRow, ResultCol)), ResultCol, False)
Workbooks("Tables.csv").Close 'Close the CSV file
Application.ScreenUpdating = True 'Turn on screen refreshing
End Function