So, sometimes when I try to execute this command, it gives me an error. The problem is that it is very inconsistent. In certain cases, it works and in others, it just doesn't.
This is the line from getCellVal function. cellName is a string and s is an integer.
getCellVal = Sheets(s).Range(cellName).Value
This time it is giving me:
Run-time error '438':
Object doesn't support this property or method
This line actually worked without problems moments ago. I added some other functions that use it and now it's not working anymore.
Any ideas about why?
Unqualified calls into the Sheets collection implicitly refer to whatever workbook is currently active, and it's important to know that the collection contains Worksheet objects, ...but also Chart objects.
Since you mean to work with a Worksheet, use the Worksheets collection instead.
If you're not getting an "index out of bounds" error, then the sheet you asked for does exist. But error 438 points to that sheet not being a Worksheet (and thus not having a Range member).
I bet the active workbook has a chart sheet at index s.
The solution is simply to be explicit.
If you mean to work with the workbook that contains the code that's running, qualify Workbook member calls with ThisWorkbook:
getCellVal = ThisWorkbook.Worksheets(s).Range(cellName).Value
If you mean to work with a workbook, then you need to get ahold of the Workbook object when you open it:
Dim wb As Workbook
Set wb = Application.Workbooks.Open(path)
'...
getCellVal = wb.Worksheets(s).Range(cellName).Value
Related
My main problem is assigning a variable an manipulating it in VBA.
I know that this code works, but I'm not sure if it this is a proper way to assign a variable in VBA like I have with currentcell = Cells(i, 1).Value
Sub C()
Dim i As Integer
For i = 1 To 4
currentcell = Cells(i, 1).Value
MsgBox currentcell
Next i
End Sub
What the macro recorder and a lot of tutorials won't tell you, is that these calls are implicitly referring to the ActiveSheet, which is obviously implicitly in the ActiveWorkbook. When all you ever have to deal with is a single worksheet, it's probably fine, but reality is that it's never the case, and code written with implicit references to the ActiveSheet is incredibly frail, bug-prone, and the underlying cause behind way too many Stack Overflow questions tagged with vba:
Cells
Range
Name
Rows
Columns
Whenever you use any of those, you're making calls against some global-scope Property Get accessor that "conveniently" fetches the ActiveSheet reference for you:
Screenshot of Rubberduck's toolbar, showing the declaration site of the selected code. Disclaimer: I manage that open-source VBIDE add-in project.
So yes, this "works", but what you'll want is to work off an explicit Worksheet object reference instead. There are many ways to get a Worksheet object reference, but the best one is to not care about where the worksheet is coming from, and take it as a parameter:
Sub DoSomething(ByVal ws As Worksheet)
Dim currentCell As Variant
Dim i As Integer
For i = 1 To 4
currentcell = ws.Cells(i, 1).Value
'in-cell error values can't be converted to a string:
If Not IsError(currentCell) Then MsgBox currentcell
Next i
End Sub
This takes the responsibility of knowing exactly what worksheet to work with away from the procedure, and gives it to the calling code, which can look like this:
DoSomething Sheet1 'uses the sheet's CodeName property
Or this:
DoSomething ThisWorkbook.Worksheets("Period" & n)
Or whatever. There are plenty of ways, each with their own gotchas - often you'll see something like this:
DoSomething Sheets("Sheet1") 'where "Sheet1" is in ThisWorkbook
Except Sheets implicitly refers to the ActiveWorkbook which may or may not be ThisWorkbook, and the Sheets collection could return a Chart object, which isn't a Worksheet. Also if "Sheet1" is in ThisWorkbook (the workbook with the code that's running), then referring to a worksheet by name means your code breaks as soon as the user renames the tab for that sheet.
The most robust way to refer to a worksheet that exists at compile/design-time, is by its CodeName. Select the sheet in the Project Explorer (Ctrl+R), then bring up the Properties toolwindow (F4), and set its CodeName by changing the (Name) property to whatever you need: VBA defines a global-scope global object variable by that name, so you can call it MyAwesomeReport and then refer to it as such in code:
DoSomething MyAwesomeReport
And that code will not break if the user changes the worksheet's "name".
Your code (however small that snippet might be) has other issues, notably it's using undeclared variables, which means you're not using Option Explicit, which is another thing that will end up biting you in the rear end and come to Stack Overflow with an embarrassing question that boils down to a typo - because without Option Explicit specified at the top of every module, VBA will happily compile a typo, making your code behave strangely instead of simply not compiling. These bugs can be excruciatingly hard to find, and they're ridiculously easy to prevent:
Use. Option. Explicit. Always.
I am working on a VBA script in Workbook_A which creates a series of separate workbooks (Workbook_B, Workbook_C). The script will create these separate workbooks, name them according to variables present in Workbook_A (in this case those variables are Workbook_B, etc.), write simulation results in sheets by different simulation run, and save them.
So far, I am able to open/create new workbooks and save them correctly. I'm running into issues when it comes to creating new sheets. Here are the relevant portions of the code:
... Initializing other variables...
Dim workbookname As String
Dim wksht1 As Worksheet
... irrelevant code ...
Set wkbk = Workbooks.Add
wkbk.title = savedVeh
workbookname = savedVeh & "full"
wkbk.SaveAs fileName:=workbookname, FileFormat:=56
Workbook_A.Activate
' Starting process of creating sheets for different results
Set wksht1 = Workbooks(workbookname).Worksheets.Add '***
wksht1.Name = "const_speeds"
ffastsim_validation.Activate
I get an error on the line with the '*** commented. The error states:
"The specified dimension is not valid for the current chart type."
This is my first time playing with VBA - am I just making a syntax error? Or is wksht1 dimensioned improperly for the operation I'm trying to perform? I'd appreciate any help explaining what's going on here.
You do:
wkbk.SaveAs fileName:=workbookname, FileFormat:=56
And then:
Set wksht1 = Workbooks(workbookname).Worksheets.Add
If I understand correctly, that Workbook object you're trying to re-dereference again, would be the wkbk object. So why bother dereferencing it again from the Workbooks collection? You already have it:
Set wksht1 = wkbk.Worksheets.Add
I may be blind, but I've been working with VBA for a few years now but still write out
Workbook("Book1").Sheets("Sheet1").Range("A1").Value
or (after dimming Book1 as a workbook and Sheet1 as a string
Book1.Sheets(Sheet1).Range("A1").Value
Is there a way that you can shorthand the "workbook.sheets" part (without doing a "With" statement)?
Sure. Just do it the wrong way:
Sheet1.Activate
Range("A1").Value = 42
Unqualified in a standard code module, Range is a member of _Global, which implements its Range property by returning the specified range on whichever worksheet happens to be active... if any (in a worksheet's code-behind, it implicitly refers to Me.Range, i.e. a range on that sheet).
If you're going to implicitly work off ActiveSheet, you can also defer type resolution to run-time with a less performant late-bound call, and make the host application (here Excel) evaluate a bracketed expression for even faster typing:
[A1].Value = 42
Heck, the Range type has a default member that points to its Value, so you could even do this:
[A1] = 42
As you can see, less code isn't always better code. Qualify your Worksheet member calls, and use default members consciously and judiciously.
Every time someone makes an implicit call on _Global, a baby unicorn dies and two new Stack Overflow questions involving errors stemming from unqualified worksheet calls are summonned from the darkness.
Sarcasm aside, if you find yourself constantly chaining such Workbook("Book1").Sheets("Sheet1").Range(...)... calls, then you're constantly dereferencing the same objects, over and over: that's not only redundant to type, it's also redundant to execute.
If you're working with ThisWorkbook (the workbook running the code), you never have a legitimate reason to do this to dereference a worksheet that exists at compile-time. Use its code name instead:
Sheet1.Range(...)...
If the workbook only exists at run-time, or otherwise isn't ThisWorkbook, then at one point in time your code opened or created that workbook - there's no need to ever dereference it from the Workbooks collection, ...if you stored the reference in the first place:
Set wbExisting = Workbooks.Open(path)
Set wbNew = Workbooks.Add
Same for worksheets that are created at run-time in other workbooks by your code: keep that object reference!
Set wsNew = wbNew.Worksheets.Add
This leaves only 1 scenario where you would ever need a string to dereference a specific worksheet: the sheet already exists in a workbook that isn't ThisWorkbook.
If that workbook's structure isn't (or can't be) protected, avoid hard-coding the sheet's index or name if you can:
Set wsExisting = wbExisting.Worksheets(1) ' user may have moved it!
Set wsExisting = wbExisting.Worksheets("Summary") ' user may have renamed it!
TL;DR
Work with objects. Declare objects, assign object references, and work with them, pass them as arguments to your procedures. There's no reason to be constantly dereferencing objects like you're doing. If you need to do it, then you only ever need to do it once.
Sure. Just do it the right way:
Dim wb As Workbook
Set wb = Workbooks("Book1")
Dim ws As Worksheet
Set ws = wb.Worksheets("Sheet1")
Dim x As Variant
x = ws.Range("A1").Value
(Sorry Mat's Mug - I had to have a bit of a dig with that first line :D)
OK, I'm going to take the "Book1" name literally, and assume that you're writing code to deal with a new workbook - probably with something like:
Dim myWorkbook As Workbook
Workbooks.Add
Set myWorkbook = Workbooks("Book1")
That's already a bad start, because:
The language of the user will determine the "Book" part of the name.
The numeric suffix will increment with every additional new workbook
So, many inexperienced coders try this:
Dim myWorkbook As Workbook
Workbooks.Add
Set myWorkbook = ActiveWorkbook
But that's open to error too. What if there are event handlers looking to change the active workbook? What is the user changes the active workbook while stepping through code?
The best way to assign your myWorkbook variable is with something like this:
Dim myWorkbook As Workbook
Set myWorkbook = Workbooks.Add
And, just like when adding a new workbook, you should follow the same approach when opening an existing workbook:
Dim myWorkbook As Workbook
Set myWorkbook = Workbooks.Open("C:\Foo.xlsx")
In both cases, you know you've got a reference to the correct workbook, and you don't care what it is called or whether it is active. You've just made your code more robust and more efficient.
Alternatively, if your VBA is working with the workbook in which it resides, you can just use ThisWorkbook or the codename of the sheet(s).
Debug.Print ThisWorkbook.Name
'By default, a sheet named "Sheet" has a codename of 'Sheet1'
Debug.Assert Sheet1.Name = ThisWorkbook("Sheet1").Name
'But give meaningful names to your sheet name and
'sheet codename, and your code becomes clearer:
Debug.Assert AppSettings.Name = ThisWorkbook("Settings").Name
You'll probably find that most of your code deals with the workbook in which it resides, or existing workbooks that your code opens, or new workbooks that your code creates. All of those situations are handled above. On the rare occasions in which your code must interact with workbooks that are already open, or are opened by some other process, you'll need to refer to the workbook by name, or enumerate the Workbooks collection:
Dim myWorkbook As Workbook
Set myWorkbook = Workbooks("DailyChecklist.xlsx")
For Each myWorkbook In Workbooks
Debug.Print myWorkbook.Name
Next myWorkbook
The one exception being add-ins, which can't be enumerated using the Workbooks collection, but can be referenced with Workbooks("MyAddin.xlam")
Like this
Sub temp()
Dim WB As Workbook, WS As Worksheet
Set WB = ActiveWorkbook
Set WS = WB.Sheets(2)
MsgBox WS.Range("A2").Text
End Sub
You can set the worksheet variable along with its parent workbook
Dim wb1s1 As Worksheet
Set wb1s1 = Workbook("Book1").Sheets("Sheet1")
And then write
wb1s1.Range("A1").Value
Why is this not working? Run time error 91?
Dim fornameCurr As String
Dim surnameCurr As String
Dim rowCurr As Long
rowCurr = 13
fornameCurr = Activesheet.Cells(rowCurr, 1) << ERROR HERE
surnameCurr = Activesheet.Cells(rowCurr, 2)
Runtime error 91 suggests that ActiveSheet Is Nothing (because ActiveSheet is the only presumed Object in the few lines of code you've provided, and certainly the only Object on the line raising the error).
Several scenarios I can think of might contribute to this:
The code is being called from an Add-In, in Excel or another application, and no Excel workbooks are open. In this case, there is no ActiveWorkbook and also no ActiveSheet.
The code is being executed from another application (Outlook,
Access, Word, PowerPoint, etc.), but you're using the Excel built-in
ActiveSheet which doesn't exist in the other applications.
You've shadowed the built-in ActiveSheet with Dim Activesheet As
Worksheet and neglected to assign a worksheet object to that variable, so it's Nothing by default.
Resolutions:
Add logic to check for presence of Workbook object, etc.
Declare a Worksheet object variable and assign to it.
Prefer to avoid shadowing existing built-in names, ensure assignment to the object variable
I run a spreadsheet report that holds about 50 columns of data for anywhere from 1 to 5000 rows. I'm only interested in 4 columns, but they are never in the same location as these reports are set-up a bit differently for each client. I then take those 4 columns and paste into a new workbook that I can import into another program.
I have three macros created that accomplish this task flawlessy if ran from the local file. When I load them into the personal.xls for use on various files I have issues. Specifically workbook/worksheet referencing issues.
Parts of the macro run to the sheet I intend from them to result on, while other parts act on the personal.xls file itself. This confuses me because I don't have any lines that use commands such as 'thisworkbook' or 'activeworksheet'.
For example:
- The first line is coded to rename Sheet1. The macro renames Sheet1 in personal.xls.
- The next line is the first of four Find commands that locate where the columns i'm interested are located and then move them. This macro runs perfectly on the sheet I intend.
I think my best course is to begin each macro by naming the active workbook and then breaking out each command to the workbook level instead of starting with Worksheets, Range, etc.
Can anyone help me understand what VBA is thinking when performing macros from personal.xls and how to best avoid the macros being run on that sheet itself?
There are two approaches you can take. I use one or both in my code - it's not a one or the other situations.
Declare Variables
Start by defining each sheet that you want to work on in a variable. I generally stay at the sheet level, but that's just a personal choice. If you'd rather be at the workbook level, that's OK too. A procedure might looks like:
Dim shSource as Worksheet
Dim shDest as Worksheet
Set shSource = Workbooks("SomeBook").Worksheets(1)
Set shDest = ActiveWorkbook.Worksheets("Summary")
then whenever I reference a Range or Cells or anything else on a sheet, I preface it with that sheet object variable. Even if I need to get to the workbook, I start with the sheet. If I needed to, for instance, close the Source workbook from the above example, I would use
shSource.Parent.Close False
I set up the sheet variables I need and then everything I do is in terms of those variables.
Edit
If you're opening or creating workbooks, then variables is definitely the way to go. For example, if you're opening a workbook, you could use one of these two examples
Dim wb As Workbook
Set wb = Workbooks.Open(C:\...)
Dim ws As Worksheet
Set ws = Workbooks.Open("C:\...).Worksheets(1)
or creating new, one of these two examples:
Dim wb As Workbook
Set wb = Workbooks.Add
Dim ws as Worksheet
Set ws = Workbooks.Add.Worksheets(1)
With Blocks
When I'm only trying to get at something one time, it seems like a waste to set up a bunch of variables. In those cases, I use a With Block so I can still have fully qualified references, but without a bunch of clutter in my code.
With Workbook("MyBook")
With .Worksheets("First_Sheet")
.Range("A1").Value = "stuff"
End With
With .Worksheets("Second_Sheet")
.Range("G10").Formula = "=A1"
End With
End With
I probably prefer the variable method, but I use them both.
Edit 2: Implicit Referencing
You should always explicitly reference your workbooks and worksheets, but it's still instructional to know how Excel will behave if you don't. A line of code that starts like Range("A1").Value = ... is called an unqualified reference. You're referencing a range, but you're not saying which sheet its on or which workbook that sheet is in. Excel handles unqualified references differently depending on where your code is.
In a Sheet's Class Module (like where you use sheet events like SelectionChange), unqualified references refer to the sheet represented by that module. If you're in the Sheet1 module working in the Change event and you code x = Range("G1").Value then the G1 you are referring to is on Sheet1. In this case, you should be using the Me keyword rather than relying on Excel.
In any other module (like a Standard Module), unqualified references refer to the ActiveSheet. The same x = Range("G1").Value code in a Standard Module refers to G1 on whichever sheet has the focus.
Excel's treatment of unqualified references is very reliable. You could easily create robust code by relying on Excel to resolve the qualified references. But you shouldn't. Your code will be more readable and easier to debug if you qualify every reference. I qualify every reference. And that's not one of those things I "always" do except when I'm lazy - I really do it 100% of the time.