I'm very new to VBA and I need all the help I can get. Module 1 counts the numbers of cells with integers in the first row starting at C1 (A1 and B1 are titles)in the 'LLP Disc Sheet'. The number of cells for this specific worksheet is 9. However, 9 is not always the number. Sometimes the number is 1, 2, 3, 4, etc. It just depends if the user fills in those cells. I'm trying to store that number 9 to use in Module 2.
Module 2 produces copies of an entire sheet called 'MasterCalculator', which I plan on renaming each sheet produced to the Cell values that were counted in Module 1. The number of copies produced must match the calculation in Module 1 (Which is currently 9).
I can't seem to figure out how to reference the variable 'lc' in the t3() module in the test() module. The number of copies of the MasterCalculator Sheet is inaccurate.
MODULE 1
Public lc As Integer
Sub t3()
Dim lc As Long, sh As Worksheet
Set sh = ActiveSheet
With sh
lc = Rows(1).SpecialCells(xlCellTypeConstants, 23).Cells.Count - 1
End With
ThisWorkbook.Save
End Sub
MODULE 2
Sub test()
Dim sh As Worksheet
Dim last_is_visible As Boolean
With ActiveWorkbook
last_is_visible = .Sheets(.Sheets.Count).Visible
.Sheets(Sheets.Count).Visible = True
.Sheets("MasterCalculator").Copy After:=.Sheets(Sheets.Count)
Set sh = .Sheets(Sheets.Count)
If Not last_is_visible Then .Sheets(Sheets.Count - t3.lc).Visible = False
sh.Move After:=.Sheets("LLP Disc Sheet")
End With
End Sub
First off, your requirement isn't suitable to be filled by a global variable. It's clearly a task for a function.
Second, your line lc = Rows(1).SpecialCells(xlCellTypeConstants, 23).Cells.Count will throw an error if there are no matching SpecialCells. Therefore it would require an error handler so that it can return -1 instead of crashing.
Third, rather than make your idea of counting SpecialCells work, please consider the alternative which is to find the end of the first row by looking from the right (instead of counting from the left). The above reasoning leads me to this function.
Function ColumnsCount(Optional Ws As Worksheet) As Long
If Ws Is Nothing Then Set Ws = ActiveSheet
With Ws
ColumnsCount = .Cells(1, .Columns.Count).End(xlToLeft).Column - 1
End With
End Function
Implementation of this function into your code leads to these two lines of code in your Test procedure.
Set Sh = .Sheets(Sheets.Count)
If Not last_is_visible Then .Sheets(Sh.Index - ColumnsCount(Sh)).Visible = False
The function ColumnCount will return the count from the worksheet given to it as parameter. In the above code that is the worksheet Sh. In the code in your question it seems to be the ActiveSheet (Perhaps Sh is the active sheet. Just make sure you pass the sheet on which you want to count and the function will return the correct number.
As two matters of principle: First, avoid using ActiveSheet as much as possible. Assign your sheets to variables meaningfully named and refer to them by the names you gave yourself. This is because ActiveSheet can be affected by user action outside the scope of your code and 9 times out of 10 it isn't a meaningful name.
Second, avoid what rubberduck calls "snake_names". LastIsVisible is a meaningful name, last_is_visible is a pain in the eye. I would use LastVis because it's shorter. I also recommend to use upper and lower case letters in names and this is my reason.
As you declare names, in the Dim statements, use caps and smalls.
As you write your code, use lower case only.
VBA will correct the capitalisation of what you type to match the declaration.
So, when VBA doesn't change the names you typed you know that there is a typo. - Instant alert for no effort at all. And your code becomes easier to read into the bargain.
To me it's preferable to set down a Function:
Function Lc(Optional sh As Worksheet) As Long
If sh Is Nothing Then Set sh = ActiveSheet
With sh
Lc = .Rows(1).SpecialCells(xlCellTypeConstants, 23).Cells.Count - 1
End With
End Function
and call it whenever you need, for example:
Sub test()
Dim sh As Worksheet
Dim last_is_visible As Boolean
With ActiveWorkbook
last_is_visible = .Sheets(.Sheets.Count).Visible
.Sheets(Sheets.Count).Visible = True
.Sheets("MasterCalculator").Copy After:=.Sheets(Sheets.Count)
Set sh = .Sheets(Sheets.Count)
If Not last_is_visible Then .Sheets(Sheets.Count - Lc).Visible = False '<--- Lc will get the "current" Lc value
sh.Move After:=.Sheets("LLP Disc Sheet")
End With
End Sub
A Public variable is as handy as can be dangerous in that you have to carefully :
"follow" it throughout all your code and ensure it isn't being unwillingly set
check it doesn't persist through sessions
Related
I am struggling with proper syntax for setting variables as ranges...
Specifically, I'm testing a function I want to use in an app that creates new profiles and store the data, I will store that data on a hidden sheet, so they can be recalled at run time.
I'm currently construction a userform in order to create a new profile, the profile data needs to be stored to the first free column on the hidden sheet.
(where I will have to create a dynamic namedRange, so that i can use that range to save the associated data, and update the listbox in the userform)
Right now, I'm stumped by this:
Sub TestFindLastFunctions()
Dim wb As Workbook
Set wb = ThisWorkbook
'wb.activate 'shouldn't be neccesary
Dim ws As Worksheet
Set ws = sh_02CRepStorage
'ws.activate 'shoudn't be neccesary
Dim trgtCol As Long
trgtCol = LastColInSheet(ws) + 2
Debug.Print trgtCol ' so far so good
'Cells(1, trgtCol).Select 'another debug check - only works if sheet activated
Dim trgtCell As Range
Set trgtCell = ws.Cells(1, trgtCol) '<------- problem line
Debug.Print trgtCell '<----- prints "" to the immediate window.
End Sub
The LastColInSheet function is copied form Ron de bruin's page: https://www.rondebruin.nl/win/s9/win005.htm it simply returns a column number, in this case: 4.(One problem with it is if the sheet is empty, it returns an error, wondering if this can be fixed with an if statement in the function.)
I've tried many iterations of the problem line, some work but only if the storage sheet is activated, and give an error if not activate or selected, as the sheet will be hidden, I need this to work without activating the sheet, (although I could switch off screen activation?).
But I understand that it is best practice to avoid extraneous selects and activates, how can I just point directly to what I want and save that range into a variable?
It just doesn't seem like it should be so difficult, I must be missing something obvious.
It also seems like it shouldn't need so many lines of code to do something so simple.
I tried some more iterations of the "problem line" after some more searching...
-The real problem was with the debug.print line
Sub TestFindLastFunctions()
Dim wb As Workbook
Set wb = ThisWorkbook
'wb.activate 'shouldn't be neccesary
Dim ws As Worksheet
Set ws = sh_02CRepStorage
'ws.activate 'shoudn't be neccesary
Dim trgtCol As Long
trgtCol = LastColInSheet(ws) + 2
Debug.Print trgtCol ' so far so good
'Cells(1, trgtCol).Select 'debug Only works if already on sheet
Dim trgtCell As Range
'Set trgtCell = ws.Range _
(ws.Cells(1, trgtCol), ws.Cells(1, trgtCol))
' unnecessarily complex, but correct if using .range?
'but works if insisting on range
Set trgtCell = ws.Cells(1, trgtCol) 'back to original
Debug.Print trgtCell.Address '<---problem was here?
End Sub
I am writing a macro for the first time and came across a problem I did not manage to find an answer to online link.
I have excel files with merged cells that I need to un-merge and then duplicate the value of the merged cell to all of the newly formed separated cells and then convert this file from .xslx to .csv.
I found a code that does exactly what I wish but the code is unmerging only the working Sheet (the sheet that I'm currently working on).
I tried putting this function in a loop through all the sheets, however, when I run the loop I get a compilation error: "Wrong number of arguments or invalid property assingment"
I would be grateful for some insight as to why this is happening, how to solve it or any other idea for how to go about it.
Thanks
This is the function that I copied and it is working for one sheet
Public Sub UnMergeFill()
Dim cell As Range, joinedCells As Range
For Each cell In ThisWorkbook.ActiveSheet.UsedRange
If cell.MergeCells Then
Set joinedCells = cell.MergeArea
cell.MergeCells = False
joinedCells.Value = cell.Value
End If
Next
End Sub
this is the function I wrote and return the error
Sub UnMergeFillAllSheets()
Dim ws As Worksheet
For Each ws In Worksheets
ws.Select
Call UnMergeFill(ws)
Next
End Sub
the input is:
[[0][0][0][0]][1][1]
when the zeros are in one merged cell
the output:
[0][0][0][0][1][1]
Call UnMergeFill(ws) is invoking the UnMergeFill procedure, passing in ws as an argument...
Public Sub UnMergeFill()
...but the UnMergeFill procedure does not accept any parameters. Hence, "wrong number of arguments".
You need to change the signature of UnMergeFill to accept a Worksheet parameter:
Public Sub UnMergeFill(ByVal ws As Worksheet)
And then there's no need to .Select it anymore:
For Each ws In Worksheets
UnMergeFill ws
Next
Except you now need UnMergeFill to work off the Worksheet parameter it's given:
For Each cell In ws.UsedRange
If you want UnMergeFill to still work off whatever the ActiveSheet is given no arguments, then you can make the parameter optional:
Public Sub UnMergeFill(Optional ByVal ws As Worksheet = Nothing)
...and then verify whether the procedure was invoked with a valid object reference:
If ws Is Nothing Then Set ws = ActiveSheet
This is my first post on stackoverflow. I have two sub procedures in Excel VBA. The first one, called Sub IAR_part_2(), is intended to assign two sheets (by index location) to two variables named sheetname1 and sheetname2. after assigning the variables I am trying to pass them to my second sub procedure, called IAR_macro, to be processed. The two sheets are dependant on one another, so sheets 4 and 8 are ran through the IAR macro, sheets 5 and 9, sheets 6 and 10, etc. My problem is that I cannot figure out how to pass the sheetname variables from IAR_part_2 to IAR_macro. What am I doing wrong?
Sub IAR_part_2()
sheetname1 = Worksheets(4)
sheetname2 = Worksheets(8)
Call IAR_macro
End Sub
Sub IAR_macro(sheetname1 As Worksheet, sheetname2 As Worksheet)
Dim h As Long
Dim i As Long
Dim l As Long
Dim j As Long
Dim k As Long
Dim lr As Long
Worksheets(sheetname1).Activate
' Find the number of the last cell with data in column A and subtract 1 to populate variable i
On Error GoTo Canceled
i = (Range("B1").End(xlDown).Row) - 1
'Switch over to the Code sheet
Worksheets(sheetname2).Activate
'While the number of loops is less than variable i minus 1, copy the contents of cells A2 through A29 over and over down the worksheet
Do While l < (i - 1)
Range("A2:A29").Select
Selection.Copy
lr = Cells(Rows.Count, "A").End(xlUp).Row
Range("A" & lr + 1).Select
ActiveSheet.Paste
l = l + 1
'rest of macro follows from here...
Simple example of how to pass worksheet objects to a different sub:
Sub Macro1()
'Declare variables
Dim ws1 As Worksheet
Dim ws2 As Worksheet
'Assign variables to worksheet objects
Set ws1 = Worksheets(4)
Set ws2 = Worksheets(8)
'Call the second sub and pass the worksheet variables to it
Call Macro2(ws1, ws2)
End Sub
Sub Macro2(ByVal arg_ws1 As Worksheet, ByVal arg_ws2 As Worksheet)
'Reference the accepted arguments (in this case worksheet variables) directly:
MsgBox arg_ws1.Name
MsgBox arg_ws2.Name
'This will result in an error because you're using the passed argument incorrectly:
MsgBox ActiveWorkbook.Sheets(arg_ws1).Name '<-- Results in error
End Sub
You must reference the passed arguments directly. If you want to use the structure shown in your code, then the arguments passed need to be a string (but this method is NOT recommended):
Sub Macro1()
'Declare variables
Dim sSheet1 As String
Dim sSheet2 As String
'Assign variables to worksheet objects
sSheet1 = Worksheets(4).Name
sSheet2 = Worksheets(8).Name
'Call the second sub and pass the worksheet variables to it
Call Macro2(sSheet1, sSheet2)
End Sub
Sub Macro2(ByVal arg_sSheetName1 As String, ByVal arg_sSheetName2 As String)
'Because the arguments are strings, you can reference the worksheets this way
'This method is NOT recommended
MsgBox Worksheets(arg_sSheetName1).Name
MsgBox Worksheets(arg_sSheetName2).Name
End Sub
I noticed in your examples, you have your variables declared inside the function. Normally any variables you wish to use are better implemented by using option explicit. Also when identifying sheets, you will have less problems when addressing a sheet by its sheet number as opposed to the sheet name. That way if you need to use a variable, you can use just a integer instead as well.
Option Explicit
Dim h as Long, i as Long, l as Long, j as Long, k as Long, lr as Long
Dim x as Integer
Sub IAR_macro()
On Error GoTo Canceled
i = (Range("B1").End(xlDown).Row) - 1
Sheets(x).Activate
Do While l < (i - l)
Sheet ids can be located in the development tool. Here is an example:
This is minimal way of passing the worksheets. As far as they are objects, they are passed by reference by default:
Sub TestMe()
Dim ws1 As Worksheet
Dim ws2 As Worksheet
Set ws1 = Worksheets(1)
Set ws2 = Worksheets(2)
Passing ws1, ws2
End Sub
Sub Passing(arg_ws1 As Worksheet, arg_ws2 As Worksheet)
Debug.Print arg_ws1.Name
Debug.Print arg_ws2.Name
End Sub
VBA is not as easy and as simple as many people (mainly those who consider it to be a funny-scripting-language, written by wanna-be-developers) think. Sometimes it allows to write ByVal, but it follows its own rules and takes the argument ByRef, just to comfort you and make sure you are not going to make an error.
Saw the answer from #tigeravatar here and I have decided not to write a comment under it, but to explain in a different post why it is wrong and dangerous, as far as explaining it as a comment would have been tough.
If you try to write a Stop line here from the answer:
Sub Macro1()
'Declare variables
Dim ws1 As Worksheet
Dim ws2 As Worksheet
'Assign variables to worksheet objects
Set ws1 = Worksheets(4)
Set ws2 = Worksheets(8)
'Call the second sub and pass the worksheet variables to it
Call Macro2(ws1, ws2)
End Sub
Sub Macro2(ByVal arg_ws1 As Worksheet, ByVal arg_ws2 As Worksheet)
'Reference the accepted arguments (in this case worksheet variables) directly:
MsgBox arg_ws1.Name
Stop
MsgBox arg_ws2.Name
'This will result in an error because you're using the passed argument incorrectly:
MsgBox ActiveWorkbook.Sheets(arg_ws1).Name '<-- Results in error
End Sub
Run the answer and wait for the Stop line:
If the arg_ws1 were taken byVal, then if someone changes the name of the 8th worksheet, while the Stop is lighting, then it should still take the old name. It is ByVal, remember? Well, go ahead and change the name. Then continue with F5. Which name are you getting? Why?
The answer is because of the way the Sub is called with parenthesis in the arguments. These force ByVal and ignore anything explicitly written.
CPearson ByRef vs ByVal
Disclaimer - My Article for how to refer a function ByVal when it is ByRef
You need to pass the two variables you created to your second sub when you call the procedure:
Sub IAR_part_2()
Set sheetname1 = Worksheets(4)
Set sheetname2 = Worksheets(8)
Call IAR_macro (sheetname1,sheetname2)
End Sub
I have a snippet which throws an error, I assume because the variable s is not initialized. How would I declare the variable s?
Dim X As Integer
Dim WS As Worksheet
'Look for existing sheets named "For Export
'If found, delete existing sheet
For Each s In ActiveWorkbook.Sheets
If s.Name = "For Export" Then
Application.DisplayAlerts = False
' s.Delete
End If
Next s
Probably the easiest way is to search for Option Explicit in your code and to delete it. Then you would not be forced to declare every variable. However, if you are not fan of writing ugly & dirty code try with the following:
Dim s as Sheet
In general, it is better to use the Worksheets collection, instead of the Sheets. Thus:
Dim s as Worksheet
For each s in ActiveWorkbook.Worksheets
The Sheets collection contains both Charts and Worksheets, thus it is better to be more specific.
I have the name of a worksheet stored as a string in a variable. How do I perform some operation on this worksheet?
I though I would do something like this:
nameOfWorkSheet = "test"
ActiveWorkbook.Worksheets(nameOfWorkSheet).someOperation()
How do I get this done?
There are several options, including using the method you demonstrate, With, and using a variable.
My preference is option 4 below: Dim a variable of type Worksheet and store the worksheet and call the methods on the variable or pass it to functions, however any of the options work.
Sub Test()
Dim SheetName As String
Dim SearchText As String
Dim FoundRange As Range
SheetName = "test"
SearchText = "abc"
' 0. If you know the sheet is the ActiveSheet, you can use if directly.
Set FoundRange = ActiveSheet.UsedRange.Find(What:=SearchText)
' Since I usually have a lot of Subs/Functions, I don't use this method often.
' If I do, I store it in a variable to make it easy to change in the future or
' to pass to functions, e.g.: Set MySheet = ActiveSheet
' If your methods need to work with multiple worksheets at the same time, using
' ActiveSheet probably isn't a good idea and you should just specify the sheets.
' 1. Using Sheets or Worksheets (Least efficient if repeating or calling multiple times)
Set FoundRange = Sheets(SheetName).UsedRange.Find(What:=SearchText)
Set FoundRange = Worksheets(SheetName).UsedRange.Find(What:=SearchText)
' 2. Using Named Sheet, i.e. Sheet1 (if Worksheet is named "Sheet1"). The
' sheet names use the title/name of the worksheet, however the name must
' be a valid VBA identifier (no spaces or special characters. Use the Object
' Browser to find the sheet names if it isn't obvious. (More efficient than #1)
Set FoundRange = Sheet1.UsedRange.Find(What:=SearchText)
' 3. Using "With" (more efficient than #1)
With Sheets(SheetName)
Set FoundRange = .UsedRange.Find(What:=SearchText)
End With
' or possibly...
With Sheets(SheetName).UsedRange
Set FoundRange = .Find(What:=SearchText)
End With
' 4. Using Worksheet variable (more efficient than 1)
Dim MySheet As Worksheet
Set MySheet = Worksheets(SheetName)
Set FoundRange = MySheet.UsedRange.Find(What:=SearchText)
' Calling a Function/Sub
Test2 Sheets(SheetName) ' Option 1
Test2 Sheet1 ' Option 2
Test2 MySheet ' Option 4
End Sub
Sub Test2(TestSheet As Worksheet)
Dim RowIndex As Long
For RowIndex = 1 To TestSheet.UsedRange.Rows.Count
If TestSheet.Cells(RowIndex, 1).Value = "SomeValue" Then
' Do something
End If
Next RowIndex
End Sub
The best way is to create a variable of type Worksheet, assign the worksheet and use it every time the VBA would implicitly use the ActiveSheet.
This will help you avoid bugs that will eventually show up when your program grows in size.
For example something like Range("A1:C10").Sort Key1:=Range("A2") is good when the macro works only on one sheet. But you will eventually expand your macro to work with several sheets, find out that this doesn't work, adjust it to ShTest1.Range("A1:C10").Sort Key1:=Range("A2")... and find out that it still doesn't work.
Here is the correct way:
Dim ShTest1 As Worksheet
Set ShTest1 = Sheets("Test1")
ShTest1.Range("A1:C10").Sort Key1:=ShTest1.Range("A2")
To expand on Ryan's answer, when you are declaring variables (using Dim) you can cheat a little bit by using the predictive text feature in the VBE, as in the image below.
If it shows up in that list, then you can assign an object of that type to a variable. So not just a Worksheet, as Ryan pointed out, but also a Chart, Range, Workbook, Series and on and on.
You set that variable equal to the object you want to manipulate and then you can call methods, pass it to functions, etc, just like Ryan pointed out for this example. You might run into a couple snags when it comes to collections vs objects (Chart or Charts, Range or Ranges, etc) but with trial and error you'll get it for sure.