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.
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.
When writing VBA code, if you know you're going to be operating in the same workbook and on a certain worksheet, should you specify ActiveWorkbook for the workbook objects in your code there?
Example:
ActiveWorkbook.Sheets("Sheet1").Range("$A$1")
vs
Range("$A$1")
Above code assumes you are working off of Sheet1 through local macros(under Sheet1 object in VBA).
Better than using ActiveWorkbook in most situations is using WorkBooks("Book1") This is more specific and robust and I believe faster to execute than activating a workbook and then pointing to it when referencing ranges.
The VBA will default to the last active workbook if you do not designate one when using lines of code like Range("A1") So if you are working in just one workbook it is not necessary since the last active workbook should always be the workbook you want the code to execute in. I typically only designate sheets and ranges when I'm working in a single workbook since it is a little redundant to tell excel to select the active workbook when it already does by default in the absence of a specific workbook designation.
Just make sure that you will be running the macro from the open workbook that you want the code to execute in.
Including a workbook designation will never hurt and it will make your code more robust. But if you will only ever have the code execute in a single workbook it is not necessary.
What I want to do
I want a code in my workbook (wbDestination) that opens another workbook (wbBosOriginal) and copies an entire sheet as values (wbBosOriginal has a lot of code in it, in modules and in the worksheet in question, and I do not want this code because it references stuff in wbB that doesn't exist in wbDestination). I have had great problems pasting as values, because it will not paste columns and rows that are currently hidden. So this is why I decided to import the whole sheet instead.
What I tried and what's wrong with it
Here is a block of code I used to copy the worksheet in the destination workbook, in the last index position. The problem with it is that some links still exist to the old workbook (Formulas, validation lists, conditionnal formatting). I have deleted all these links but STILL when I paste the sheet successfully, save and reopen, I have an error saying some content is unreadable. I believe there are still some elements linked to the old workbook.
Set wbBosOriginal = Workbooks.Open(strChosenPath, ReadOnly:=True)
With wbBosOriginal.Sheets("BOS")
.Visible = True
'Pastes the ws in last position in wbDestination
.Copy after:=wbDestination.Sheets(wbDestination.Worksheets.Count)
Set wsNewBos = Worksheets(Worksheets.Count)
'Deletes VBA code in the copied sheet
ThisWorkbook.VBProject.VBComponents.Item(wsNewBos.CodeName).CodeModule.DeleteLines 1, _
ThisWorkbook.VBProject.VBComponents.Item(wsNewBos.CodeName).CodeModule.CountOfLines
End With
The worksheet is successfully pasted with no code in it, with everything else it had previously. I then remove all formulas, conditionnal formatting, and validation lists. Even after removing those as well, I still get an error when opening the workbook.
My question
Apart from conditional formatting, validation lists, VBA code, and formulas linking a worksheet that was pasted to a new workbook, what other elements could cause the workbook from opening in repair mode every time due to existing links to the old workbook?
If my question is not clear, comment and I will clarify.
Dealing directly with VBE seems a bit heavy-handed to me. If your code is manipulating several workbooks, I would put the code in an add-in and not have it in any workbook. (Technically *.xlam addins are workbooks, but when I say "workbook" I mean normal *.xls, *.xlsx, *.xlsm, etc.)
That said, if you're just copying cell values (which may be formulas) between different workbooks, you shouldn't have any dependencies other than cell references, named ranges, and user-defined functions in the original workbook. I would make sure there are none of those. Please also share how you are ensuring your formulas do not have broken references.
If the issue you are having is caused by trying to avoid hidden columns and rows not allowing pastevalues, why not unhide the rows and columns and then copy only the values to the new book?
Just cycle through each of the sheets in the original book and use the method .UsedRange.Hidden = False. As far as I am aware, this should unhide every cell on the sheet and allow you to do the original pastevalues calls
This works fast and smooth (it's harder to delete ALL the data Imo):
Sub tests()
Dim AllRange As Range: Set AllRange = ActiveSheet.UsedRange
Dim ItemRange As Range
Dim myWbDestination As Workbook: Set myWbDestination = ThisWorkbook
Dim SheetDestination As String: SheetDestination = ("Sheet2")
For Each ItemRange In AllRange
With myWbDestination.Sheets(SheetDestination)
.Range(ItemRange.Address) = ItemRange.Value
End With
Next ItemRange
End Sub
Repair mode can be triggered by many factors, you would need to post the code you are getting to look for an explanation, it would be like asking why vba may broke
So here's what I'm trying to do:
Open file: Pc_Profile
Create new sheet: Sheet1
Copy desired cells from Pc_Profile to Sheet1 (see script below)
Copy entire Sheet1 to new excel file: db.xls
Rename sheet to content of cell A5
Create new sheet for next script run
Basically I'm trying to automate an extraction of a TON of excel files into a single organized file. Each script call should extract to its own sheet so there's no overwritten information.
Here is what I have working so far. It just copies the desired cells to a new sheet within the same file.
' Create Excel object
Set objExcel = CreateObject("Excel.Application")
' Open the workbook
Set objWorkbook = objExcel.Workbooks.Open _
("\\[directory]\Pc_Profile.xls")
' Set to True or False, whatever you like
objExcel.Visible = True
objWorkbook.Worksheets("Pc_Profile").Range("A5:D5").Copy
objWorkbook.Worksheets("Sheet1").Range("A1").PasteSpecial
objWorkbook.Worksheets("Pc_Profile").Range("A8:B8").Copy
objWorkbook.Worksheets("Sheet1").Range("A2").PasteSpecial
objWorkbook.Worksheets("Pc_Profile").Range("A13:B13").Copy
objWorkbook.Worksheets("Sheet1").Range("A3").PasteSpecial
objWorkbook.Worksheets("Pc_Profile").Range("A15:D17").Copy
objWorkbook.Worksheets("Sheet1").Range("A4").PasteSpecial
objWorkbook.Worksheets("Pc_Profile").Range("A24:E26").Copy
objWorkbook.Worksheets("Sheet1").Range("A7").PasteSpecial
objWorkbook.Worksheets("Pc_Profile").Range("A28:B30").Copy
objWorkbook.Worksheets("Sheet1").Range("A10").PasteSpecial
objWorkbook.Worksheets("Pc_Profile").Range("A43:B43").Copy
objWorkbook.Worksheets("Sheet1").Range("A13").PasteSpecial
objWorkbook.Worksheets("Pc_Profile").Range("A45:B45").Copy
objWorkbook.Worksheets("Sheet1").Range("A14").PasteSpecial
' Activate Sheet2 so you can see it actually pasted the data
objWorkbook.Worksheets("Sheet2").Activate
I would really appreciate the extra push. I'm automating this for a work project and have no experience with VB - I just learned that on the go.
A couple things that are good practice to get into before I get to your actual question:
1) Any macro that you expect to run a long time should have Application.ScreenUpdating = False before any actual work is done in the code, this tells Excel not to bother with changing what's displayed on the screen (big performance booster). Be sure to include an Application.ScreenUpdating = True near the end of your code
2) Similar to #1, you generally want to include Application.Calculation = xlManual to boost performance. If you have large ranges of cells that your macro needs accurate up-to-date values from, it may be easier to leave the calculation automatic, but that doesn't appear to be the case in this instance.
3) You don't need to create a new Excel instance (which is what your first line of code does). You're already in a perfectly good instance of Excel. This also saves you having to close the other instance at the end of the macro (or worse from forgetting to do so and having memory get hogged by Excel processes that aren't really in use)
As to your specific problem, it sounds like you have more workbooks that Pc_profile to copy from, and that you're wanting to create a new "db.xls" with each run of the macro. Based on those assumptions all you need to do is nest your code starting with 'Open the workbook and objWorkbook.Worksheets("Sheet1").Range("A14").PasteSpecial inside a Do While loop. The thing I'm not sure about is how to control the loop. If the list of files is always the same, you should just include a list on a sheet in the workbook that holds the macro and just iterate through that.
The other thing you should do for ease of coding, and to make the loop more effective is declare and use a Worksheet variable and set if for each workbook to the appropriate sheet to pull data from. Ex.
Dim ws as Worksheet
'The Dim is outside your loop, but this would be inside it
Set ws = objWorkbook.Worksheets("whatever_the_sheet's_name_is")
This way you can replace each occurrence of objWorkbook.Worksheets("Pc_Profile"). with ws., easier to type, easier to read, easier to update, and less error prone.
Next, you don't actually have code for moving Sheet1 to a new workbook, or renaming it. To move it (as well as the other Sheet1's yet to be created), you should, before getting to the Do While loop, have the following
Dim target as Workbook
Set target = Application.Workbooks.Add
Then at almost the end of the loop, you need objWorkbook.Worksheets("Sheet1").Move Before:=Target.Sheets(1)
Last, you need to include objWorkbook.Close SaveChanges:=False after you've moved Sheet1 out of the Pc_Profile and renamed it.
I am trying to update some formulas from one workbook, to another workbook. Everything is working great until I run into a formula that has a reference to another workbook. For example a formula like this =IF(ISERROR(W!Var1),0,W!Var2) It will prompt me to open this workbook, I am assuming so that it can evaluate the formula. So my question is this. Is there a way for me to handle these situations on the fly, so if there is a workbook reference needed it will prompt me and then save it to memory? Because if I have more than one cell that contains these formulas it will prompt me to open the referenced workbook for every cell that contains the link. Alternatively, is there a way that I can just push my formula into the cell without having excel evaluate it?
So in my code I have this line which works for any value that doesn't contain a workbook reference. TheRange.RefersToRange.FormulaR1C1 = RangeFormula
Any help is greatly appreciated.
I understand that you refer to Worksheets (each of the "tabs" in a given Excel file), the Workbook is the whole file. The popping-up message appears when the referred Worksheet cannot be found. Example: range.Value = "=sheet5!A3" (in a Workbook having just "sheet1", "sheet2" and "sheet3"). If you want to avoid this message (although bear in mind that the Worksheet is not there and thus the calculations will be wrong anyway), you can write:
excelApp.DisplayAlerts = False
Where excelApp is the Excel.Application you are currently using.