Excel: Check Sheet Dependencies within a Workbook? - excel

I'm in the process of refactoring a huge workbook woth a lot of legacy parts, redundant computations, cross-dependencies etc.
Basically, I'm trying to remove unneeded sheets and implement some proper information flow within the workbook. Is there a good way to extract the dependencies between the sheets (with VBA)?
Thanks
Martin

You can use ShowPrecedents and NavigateArrow.
here is some pseudocode
for each oCell in oSht containing a formula
ocell.showprecedents
do until nomoreprecedents
i=i+1
Set oPrec = oCell.NavigateArrow(True, 1, i)
If not oPrec.Parent Is oSht Then
' off-sheet precedent
endif
loop
next ocell

I came up with a little sub to do this. It moves all the sheets into seperate workbooks and prints out the dependencies. The advantage over using showPrecedents is that it captures all links including names, embedded forms/diagramms etc.
Word of warning: Moving worksheets isn't undo-able, save your workbook before running this and close (without saving) and re-open afterwards.
Sub printDependencies()
' Changes workbook structure - save before running this
Dim wbs As VBA.Collection, wb As Workbook, ws As Worksheets
Dim i As Integer, s As String, wc As Integer
Set ws = ThisWorkbook.Worksheets
Set wbs = New VBA.Collection
wbs.Add ThisWorkbook, ThisWorkbook.FullName
For i = ws.Count To 2 Step -1
ws(i).Move
wc = Application.Workbooks.Count
wbs.Add Application.Workbooks(wc), Application.Workbooks(wc).FullName
Next
Dim wb As Workbook
For Each wb In wbs
For Each s In wb.LinkSources(xlExcelLinks)
Debug.Print wb.Worksheets(1).Name & "<-" & wbs(s).Worksheets(1).Name
Next
Next
End Sub
The code isn't very polished or user-friendly, but it works.

You can follow the steps at "Find external references that are used in cells" topic of the following link:
Find external references in a worbook
But instead of enter the "[" you should enter the name of the sheet you're trying to find its dependencies. It will display a large list of every single cell referencing the sheet, but at the end it works. Haven't find the way to group by Sheet.

Related

VBA: multiple workbooks open with similarly named tabs: how to refer to specific workbook?

Say I have two workbooks open with two tabs both named "Tab1". If in VBA, I want to refer to something on a tab, one would say "Tab1!" followed by what one is referring to, e.g. "Tab1!R1C1", etc.
However, how does the code know what Tab1 I am referring to in this case?
If it's worth mentioning, the code I am running is inside one of the workbooks. So will it always default back to that workbooks tab 1? If not, what does it do, and if yes, how would I make it refer to the tab 1 of the other workbook?
For context, I need to do this in the case of calling the method "ChangePivotCache" and give it a SourceData.
Without the code it's difficult to say definitively, but lets assume that you're using a range object. When you put "Tab1!R1C1" into that range object it uses the workbook of it's parent to make the determination. Most (if not all) methods that take a range argument like that will require that some parent of theirs is a workbook or worksheet.
In general if you're working with multiple workbooks best practice is to assign an object to them as you open them. That object can then be used to anchor any range calls you make. Either by calling directly from it or making children from it.
For example:
set workbook_code_is_in = Thisworkbook
set another_workbook = Application.Workbooks.Open(...)
set active_workbook = Activeworkbook
The first line will set workbook_code_is_in to the workbook that the code is located in.
The second line will set another_workbook to whatever you open.
The third line will set active_workbook to whatever workbook currently has focus. This is dangerous and prone to errors. It's very possible for a user to click a different workbook while a script is running and make the Activeworkbook an unintended one.
Referring to a Second Workbook
If you only have two workbooks open, then you refer to the workbook containing this code with ThisWorkbook and you can refer to the other workbook by using a For Each...Next loop to compare the workbooks' names.
Option Explicit
Sub ReferToSecondWorkbook()
Dim wb1 As Workbook: Set wb1 = ThisWorkbook ' workbook containing this code
If Workbooks.Count <> 2 Then
MsgBox "You need to have only two workbooks open.", vbCritical
Exit Sub
End If
Dim wb2 As Workbook
For Each wb2 In Workbooks
If StrComp(wb2.Name, wb1.Name, vbTextCompare) <> 0 Then ' different
Exit For
' Else ' it is 'wb1'
End If
Next wb2
Dim ws1 As Worksheet: Set ws1 = wb1.Worksheets("Tab1")
Dim ws2 As Worksheet: Set ws2 = wb2.Worksheets("Tab1")
Debug.Print "First: ", ws1.Name, wb1.Name
Debug.Print "Second: ", ws2.Name, wb2.Name
End Sub

How can I add sheets from an excel file to another?

So I am trying to write a Macro for Excel, that adds 2 worksheets from an excel file to a new one.
Therefore, I try this:
Sub addfile()
Dim sheet1 As Worksheet
Dim sheet2 As Worksheet
Set sheet1 = Sheets.Add(Type:="C:\Users\Helge\AppData\Roaming\Microsoft\Templates\page1.xltx")
Set sheet2 = Sheets.Add(Type:="C:\Users\Helge\AppData\Roaming\Microsoft\Templates\page2.xltx")
End Sub
When I test it, it imports the first page, but the 2nd page gives me a Runtime error 1004.
Why does this happen?
And is there another way to get 2 sheets from one excel file to another via vba?
Much to my surprise this version of your code actually worked for me.
Sub addfile()
Dim Sheet1 As Worksheet
Dim Sheet2 As Worksheet
Set Sheet1 = Sheets.Add(Type:=Environ("Userprofile") & "\OneDrive\Desktop\Template1.xltx")
Set Sheet2 = Sheets.Add(Type:=Environ("Userprofile") & "\OneDrive\Desktop\Book2.xlsx")
Debug.Print Sheet1.Name, Sheet2.Name
End Sub
The reason for my surprise is that Sheet1 and Sheet2 are the default CodeName for the first and second worksheets in any workbook. Therefore there is a conflict of naming between the Sheet1 in the workbook and the Sheet1 you declare which should come to the surface not later than Debug.Print Sheet1.Name. In fact, it may have. I didn't check which name was printed. But the code didn't crash. Since it crashes on your computer, perhaps you have an older version of Excel. Try to stay clear of variable names that Excel also uses. Or there is something wrong with the path & file name, which is hard to tell in that syntax and therefore kept me fooled for quite some time too.
In fact, I discovered the above only after finding out that my Desktop was on OneDrive and not before I had written the function below which is designed to avoid the use of Sheets.Add. It also has some extras such as being able to specify the sheet to take from the template (you could have one template with 2 or more sheets). You can specify an index number or a sheet name. And the function will give a name to the copy, too, if you specify one.
Private Function AddWorksheet(ByVal Template As String, _
TabId As Variant, _
Optional ByVal TabName As String) As Worksheet
Dim Wb As Workbook
Dim Path As String
Dim FileName As String
Set Wb = ThisWorkbook ' change to suit
' make sure the path ends on "\"
Path = "C:\Users\Helge\AppData\Roaming\Microsoft\Templates\"
With Workbooks.Open(Path & Template)
.Sheets(TabId).Copy After:=Wb.Sheets(Wb.Sheets.Count)
.Close
End With
Set AddWorksheet = ActiveSheet
If Len(TabName) Then ActiveSheet.Name = TabName
End Function
You can call the function from a sub routine like this:-
Sub AddWorksheets()
Dim Tab1 As Worksheet
Dim Tab2 As Worksheet
Application.ScreenUpdating = False
Set Tab1 = AddWorksheet("Page1.xltx", 1, "New Tab")
Set Tab2 = AddWorksheet("Page2.xltx", "Sheet1", "Another new Tab")
Application.ScreenUpdating = True
End Sub
Please observe the difference between the two function calls.

Workbook.close causes sub to exit

I have a set of versioned Excel documents that I am trying to get to auto-update when there is a new version available. What fails is that the .close method is not just closing one of the workbooks but also exiting the sub.
The process:
The sub gets called from Worksheet_Activate and immediately checks to see if an upgrade is needed. If needed, it collects all of the names of the sheets (except the "Count" sheet which is a copy from a template), creates a new workbook with the same sheets as the old one, copies the data over to the proper sheets, closes the old workbook, deletes the old workbook, saves the new workbook with the same name as the old workbook.
Pretty straight forward and it worked great until it didn't. I'm not sure why, but now when the wkbFrom.Close command is executed it also exits the procedure.
I've been digging around and the only answer I could find that seemed to address my issue was to give some delay before/after the close so that Excel will have time to finish and not collide with itself. So I tried putting in a 5 second delay before the close command but to no avail.
Excel doesn't crash, it's still up and running properly. I checked the Event Viewer and Excel is not throwing any errors. The sub simply closes the workbook and then exits the sub.
Here is the full code for the sub.
Sub UpgradeHWWorkbook(Optional HWSheetVersion As Double)
'--------------------------------
'This sub upgrades a hardware tracking
'workbook to the newest version based on
'version in the variable HWSheetVersion
'--------------------------------
'Before anything else, Check to see if upgrade is needed.
'If sheet version is equal or larger than the plugin version
'OR the name of the sheet is wrong, exit without upgrading
'---------------------------------------------------------------------
If HWSheetVersion >= HWPlugInVersion Or _
Not ActiveSheet.CodeName Like "BaseHWSheet_*" Then
Exit Sub
End If
'---------------------------------------------------------------------
'VAR declarations----------------
Dim wkbFrom As Workbook 'Holds the original workbook
Dim wkbTo As Workbook 'Holds the new workbook
Dim sWKB As Workbook 'Holds Workbook where Count sheet is kept
Dim sWKS As Worksheet 'Holds Count sheet
Dim wks As Worksheet 'Holds worksheets
Dim wksNames() As String 'Holds the names of all the worksheets
Dim wkbFromName As String 'Holds the name of the original workbook
Dim wkbFromPath As String 'Holds the path of the original workbook
Dim wkbToPath As String 'Holds the path where the new workbook will be saved
Dim rng As String 'Holds the range of cells that will be copied
Dim x As Byte 'Holds counter
Dim wksName As Variant 'Holds the name of the current worksheet
'--------------------------------
'Sub Settings--------------------
Set wkbFrom = ActiveWorkbook 'Set the active workbook as the one that the data comes from
wkbFromPath = wkbFrom.Path 'Grabs the path of the original workbook
wkbFromName = wkbFrom.Name 'Grab the original workbook name
wkbToPath = wkbFrom.FullName 'Grab the path path and name in another var so we don't have to do it by hand
ReDim wksNames(0) 'Starts off the array that will hold the worksheet names
x = 0 'Flush the counter
rng = "A2:D18" 'The range of cells that will be copied and pasted
Application.DisplayAlerts = False 'Turn off annoying pop-ups
Set sWKB = Workbooks("StockroomAddins.xlam") 'Workbook with Count sheet to copy to new workbook
Set sWKS = sWKB.Worksheets("Count") 'Count sheet to copy to new workbook
'--------------------------------
'Get all of the worksheet names (except Count) in the workbook
'-----------------------------------------------------------------------
For Each wks In wkbFrom.Worksheets 'itenerate through the book
If Not wks.Name = "Count" Then 'If the worksheet isn't the "Count" sheet...
wksNames(x) = wks.Name 'add the sheet name to the array wksName()
x = 1 + UBound(wksNames) 'Increase the array by 1
ReDim Preserve wksNames(x) 'Increase the size of the array by 1
End If
Next wks
'-----------------------------------------------------------------------
'Create new workbook & add Count sheet
'-----------------------------------------------------------------------
Set wkbTo = Workbooks.Add 'Create the new workbook
wkbTo.Activate 'Make sure new book is active book
sWKS.Copy Before:=Sheets("Sheet1") 'Add the Count sheet to workbook
'-----------------------------------------------------------------------
'Iterate through the sheets in the original workbook, add sheets with the same name to the new book, copy data from the old sheet to the new sheet
'-----------------------------------------------------------------------
For Each wksName In wksNames 'Loop through all of the worksheet names and...
If Not wksName = "" Then 'If it isn't blank...
Call NewHardwareTrackingSheet(wksName, wkbTo) 'Call the sub that creates a new tracking sheet
wkbFrom.Worksheets(wksName).Range(rng).Copy 'Copy the data from the old sheet
wkbTo.Worksheets(wksName).Range(rng).PasteSpecial _
Paste:=xlPasteValues 'Paste the data (Values only) into the new sheet
End If
Next wksName
wkbTo.Worksheets("Sheet1").Delete 'Delete the default "Sheet 1" that every new workbook has
wkbFrom.Close Savechanges:=False 'close the original workbook
'-----------------------------------------------------------------------
'Delete the old workbook and save the new one in the same place with the same name as the old one
'-----------------------------------------------------------------------
Kill wkbToPath 'Kill the original
wkbTo.SaveAs Filename:=wkbToPath, FileFormat:=52 'Save the new as the original
Application.DisplayAlerts = True 'Turn annoying pop-ups back on
'-----------------------------------------------------------------------
'Clean up-------------------------------------
Set wkbFrom = Nothing: Set wkbTo = Nothing: Set sWKB = Nothing
Set wks = Nothing: Set sWKS = Nothing
'---------------------------------------------
End Sub
Any ideas on what I've messed up? I figured that since it worked at one point and now doesn't, that I've probably messed up the code somewhere but I'm not seeing it.
OK, I found my answer and I feel a bit stupid about it. Thanks to the folks who asked me some questions because they caused the thought process that worked.
It seems that the spreadsheet I was using to test the code must have gotten corrupted. I tried it on a couple of other files and it worked correctly. No wonder I couldn't find a code issue: there isn't one.
Goes to show the old adage "Measure twice, cut once." I should have tested on multiple files and not assumed my single test file was right.
Much thanks to those who read, thought about, and commented on my post. It is appreciated.
EDIT: Or not......
Came in this morning and it's not working again. There's got to be something in my code that is causing the issue on some and not on others. TBH, I have no idea what it is.
This is really causing me to bang my head on the wall.
OK, so I think I've figured this out.
I am calling this Sub to check the version each time a sheet is activated with this:
Public Sub Worksheet_Activate()
Application.Run "StockroomBarcodeSheets.UpgradeHWWorkbook", HWSheetVersion
End Sub
To do some other testing, I set up another sub inside my plug-in that just called the UpgradeHWWorkbook sub with a fake HWSheetVersion so I could force a workbook to upgrade. Lo, and behold, this setup worked perfectly every time.
So, when I call from the Worksheet_Activate() it exits on the .close command. When I call it from a sub inside the add-in, it works perfectly.
Because the UpgradeHWWorkbook is in a plug-in I thought that the restriction upon closing the calling workbook wouldn't come into affect. I was wrong.

How can I calculate an entire workbook, but not all open workbooks?

Is there a way to calculate entire workbook but not all open workbooks in VBA?
I know about
worksheet.Calculate
but it will only do 1 sheet. I also know about
Application.CalculateFull
But I think this recalculate all open workbooks at the same time.
Is it possible to do only 1 workbook?
Edit:
I get the approach:
For Each wks In ActiveWorkbook.Worksheets
wks.Calculate
Next
but this is very dangerous if sheets have links between them, then the order of calculation is important. I know I could find the exact sequence and apply it, problem is that on very large workbooks this can get somewhat tricky
After some investigation and testing, selecting all sheets first and calculating afterward works :
Application.Calculation = xlManual
ThisWorkbook.Sheets.Select
ActiveSheet.Calculate
this calculate all selected sheet at the same time creating the right order of calculation
The answer by Steven G doesn't actually work, it just looks like it does.(I can't reply to that post as I don't have 50+ rep, which is annoying).
It still calculates the sheets in 1 directional order. I tested this by creating 5 sheets and having them all + 1 to another cell, on each sheet in different directions. Using the select sheets then calculate method will give the wrong result.
As it requires that the sheets are visible, elow is function I built to replace the calculate function using that method. (Requires dictionaries reference library). Do not use though.
EXAMPLE, METHOD DOES NOT WORK
Function Calculate_Workbook(Optional Wb As Workbook)
'Application.calculate will calculate all open books, which can be very slow.
'Looping through all sheets in workbook to calculate can be risky due to formula referencing a cell thats not yet calculated, calculation order may be important.
Dim DicShtVisible As New Dictionary
DicShtVisible.CompareMode = TextCompare
Dim Asht As Worksheet, AWkbk As Workbook 'Previously selected books
Dim Sht As Worksheet
Set Asht = ActiveSheet
Set AWkbk = ActiveWorkbook
If Wb Is Nothing Then Set Wb = ThisWorkbook
Wb.Activate
'Unhide all sheets as can't select very hidden stuff
For Each Sht In Wb.Sheets
DicShtVisible.Add Sht.Name, Sht.Visible
Sht.Visible = xlSheetVisible
Next Sht
'Select all sheets and calculate the lot
Wb.Sheets.Select
ActiveSheet.Calculate
'Reapply visibility settings
Dim Key As Variant
For Each Key In DicShtVisible.Keys
Wb.Sheets(Key).Visible = DicShtVisible.Item(Key)
Next Key
'Reset selections
AWkbk.Activate
Asht.Select
End Function
As far as I can see there is no solution, the only answer is to either know the calculation order of your workbook and manually calculate sheets in that order, or accept using Calculate and that it will calculate all open workbooks.
I would love to be proven wrong however. It's such an annoying and stupid issue.
None that I know about, Steven.
You actually lose time doing Application.Worksheets(1).Calculate
I ran a timing test: 3 workbooks open in the same instance of Excel.
One with 8200 volatile functions, 8000 in one worksheet on 4 non-contiguous ranges, 200 in other worksheets.
Two other workbooks with a combined total of 100 functions. The results:
Application.Calculate - 4.41 ms
ThisWorkbook.Worksheets(1).Calculate - 52.05 ms for each worksheet
ThisWorkbook.Worksheets(1).Range("R1").Calculate (repeated 4 times) - 84.64 ms
It's counter-intuitive, but true.
You also lose time declaring Dim wks as Worksheet. Unless you do For ... Each referring to it as ThisWorkbook.Worksheets(1) - is faster
Also, be careful referring to ActiveWorkbook - when you code runs your primary workbook might not be active one. ThisWorkbook (the one with code) is safer.
ThisWorkbook will use only the workbook that the code resides in
For Each wks In ThisWorkbook.Worksheets
wks.Calculate
Next wks
Another option is to create a new instance of Excel, open the workbook is that instance and then calculate that. This will leave any workbooks in the original instance uncalculated, potentially saving you time if another workbook is very slow to calculated (100k+ formulas for instance).
Dim xlApp As Excel.Application
Dim wbTest As Workbook
Set xlApp = New Excel.Application
Set wbTest = xlApp.Workbooks.Open("c:\temp\test.xlsx")
' do a bunch of stuff in wbTest
xlApp.Calculate
Whether this is better depends on your circumstances. Opening a second instance of Excel involves a little extra time and probably more memory (although I haven't tested this and it may not be a significant amount).
You may have to change more code to take account of the fact this workbook is in a different instance and there may be issues with some functions (for example if you check if a workbook is already open by spooling through all the workbooks in the original instance of Excel).
Just use Calculate
eg:
Sub Cal_()
Calculate
End Sub
We can use
Application.CalculateBeforeSave = True
and then save the workbook? May be more finessed to trap the current value, set if not true, save, reset if was not true.
Try this:
Sub CalcBook()
Dim wks As Worksheet
Application.Calculation = xlManual
For Each wks In ActiveWorkbook.Worksheets
wks.Calculate
Next
Set wks = Nothing
End Sub

Copy sheet and get resulting sheet object?

Is there any easy/short way to get the worksheet object of the new sheet you get when you copy a worksheet?
ActiveWorkbook.Sheets("Sheet1").Copy after:=someSheet
It turns out that the .Copy method returns a Boolean instead of a worksheet object. Otherwise, I could have done:
set newSheet = ActiveWorkbook.Sheets("Sheet1").Copy after:=someSheet
So, I wrote some 25 lines of code to get the object. List all sheets before the copy, list all sheets after, and figure out which one is in the second list only.
I am looking for a more elegant, shorter solution.
Dim sht
With ActiveWorkbook
.Sheets("Sheet1").Copy After:= .Sheets("Sheet2")
Set sht = .Sheets(.Sheets("Sheet2").Index + 1)
End With
I believe I have finally nailed this issue - it's been driving me nuts, also! It really would have been nice if MS made Copy return a sheet object, same as the Add method...
The thing is, the index which VBA allocates a newly copied sheet is actually not determined... as others have noted, it very much depends on hidden sheets. In fact, I think the expression Sheets(n) is actually interpreted as "the nth visible sheet". So unless you write a loop testing every sheet's visible property, using this in code is fraught with danger, unless the workbook is protected so users cannot mess with sheets visible property. Too hard...
My solution to this dilemma is:
Make the LAST sheet visible (even if temporary)
Copy AFTER that sheet. It MUST have index Sheets.Count
Hide the former last sheet again, if required - it will now have
index Sheets.Count-1
Move the new sheet to where you really want it.
Here's my code - which now seems to be bullet-proof...
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("Template").Copy After:=.Sheets(Sheets.Count)
Set sh=.Sheets(Sheets.Count)
if not last_is_visible then .Sheets(Sheets.Count-1).Visible = False
sh.Move After:=.Sheets("OtherSheet")
End With
In my case, I had something like this (H indicating a hidden sheet)
1... 2... 3(H)... 4(H)... 5(H)... 6... 7... 8(H)... 9(H)
.Copy After:=.Sheets(2) actually creates a new sheet BEFORE the next
VISIBLE sheet - ie, it became the new index 6. NOT at index 3, as you might expect.
Hope that helps ;-)
Another solution I used would be to copy the sheet to a place where you know its index, aka first. There you can easily have a reference to it for whatever you need, and after that you can move it freely to where you want.
Something like this:
Worksheets("Sheet1").Copy before:=Worksheets(1)
set newSheet = Worksheets(1)
newSheet.move After:=someSheet
UPDATE:
Dim ThisSheet As Worksheet
Dim NewSheet As Worksheet
Set ThisSheet = ActiveWorkbook.Sheets("Sheet1")
ThisSheet.Copy
Set NewSheet = Application.ActiveSheet
Updated with suggestions from Daniel Labelle:
To handle possible hidden sheets, make the source sheet visible, copy it, use the ActiveSheet method to return the reference to the new sheet, and reset the visibility settings:
Dim newSheet As Worksheet
With ActiveWorkbook.Worksheets("Sheet1")
.Visible = xlSheetVisible
.Copy after:=someSheet
Set newSheet = ActiveSheet
.Visible = xlSheetHidden ' or xlSheetVeryHidden
End With
I realise this post is over a year old, but I came here looking for an answer to the same issue regarding copying sheets and unexpected results caused by hidden sheets. None of the above really suited what I wanted mainly because of the structure of my workbook. Essentailly it has a very large number of sheets and what is displayed is driven by a user selecting the specific functionality, plus the order of the visible sheets was importnat to me so i didnt want to mess with those. So my end solution was to rely on Excels default naming convention for copied sheets, and explictly rename the new sheet by name. Code sample below (as an aside, my workbook has 42 sheets and only 7 are permanently visible, and the
after:=Sheets(Sheets.count) put my copied sheet in the middle of the 42 sheets, depending on what sheets are visible at the time.
Select Case DCSType
Case "Radiology"
'Copy the appropriate Template to a new sheet at the end
TemplateRAD.Copy after:=Sheets(Sheets.count)
wsToCopyName = TemplateRAD.Name & " (2)"
'rename it as "Template"
Sheets(wsToCopyName).Name = "Template"
'Copy the appropriate val_Request to a new sheet at the end
valRequestRad.Copy after:=Sheets(Sheets.count)
'rename it as "val_Request"
wsToCopyName = valRequestRad.Name & " (2)"
Sheets(wsToCopyName).Name = "val_Request"
Case "Pathology"
'Copy the appropriate Template to a new sheet at the end
TemplatePath.Copy after:=Sheets(Sheets.count)
wsToCopyName = TemplatePath.Name & " (2)"
'rename it as "Template"
Sheets(wsToCopyName).Name = "Template"
'Copy the appropriate val_Request to a new sheet at the end
valRequestPath.Copy after:=Sheets(Sheets.count)
wsToCopyName = valRequestPath.Name & " (2)"
'rename it as "val_Request"
Sheets(wsToCopyName).Name = "val_Request"
End Select
Anyway, posted just in case its useful to anyone else
This question is really old, but as there were some activity here not so long time ago and it still gave me all the answers I needed 10 years later, I'd like to share the way I did it.
After reading this thread, I found Tigregalis'answer really interesting, even if I prefer Ama's solution. But none of them was reflecting original Excel behavior with the choice of copying before/after or to a new workbook. As I needed it, I wrote down my own function, and to make it still closer from Excel's one, I made it able to handle Sheets and not just Worksheets.
For those interested, here is my code :
Function CopySheet(ByVal InitSh As Object, Optional ByVal BeforeSh As Object, Optional ByVal AfterSh As Object) As Object
'Excel doesn't provide any reliable way to get a pointer to a newly copied sheet. This function allows to make it
'Arguments: - InitSh : The sheet we want to copy
' - BeforeSh : The sheet before the one we want the copy to be placed
' - AfterSh : The sheet after the one we want the copy to be placed
'Return : - Returns the newly copied sheet. If BeforeSh and AfterSh are not givent to the sub, the sheet is created in a new workbook. In the case both are given, BeforeSh is used
' To beknown : if the InitSh is not visible, the new one won't be visible except if InitWks is the first of the workbook !
Dim isBefore As Boolean
Dim isAfter As Boolean
Dim Wkb As Workbook
'If there is before or after, we need to know the workbook where the new sheet is copied, if not we need to set up a new workbook
If Not BeforeSh Is Nothing Then
isBefore = True
Set Wkb = BeforeSh.Parent
ElseIf Not AfterSh Is Nothing Then
isAfter = True
Set Wkb = AfterSh.Parent
Else
Set Wkb = Application.Workbooks.Add(xlWBATWorksheet)
End If
'To be able to find the new worksheet, we need to make sure the first sheet of the destination workbook is visible and make the copy before it
Dim FirstWksVisibility As XlSheetVisibility
FirstWksVisibility = Wkb.Sheets(1).Visible
Wkb.Sheets(1).Visible = xlSheetVisible
InitSh.Copy before:=Wkb.Sheets(1)
'Restore the initial visibility of the first worksheet of the workbook, that is now the sheet number 2 as we copied one in front of it
Wkb.Sheets(2).Visible = FirstWksVisibility
'Finaly, move the sheet accordingly to otpional arguments BeforeWks or AfterWks
Dim TempSh As Object
Set TempSh = Wkb.Sheets(1)
If isBefore Then
TempSh.Move before:=BeforeSh
ElseIf isAfter Then
TempSh.Move after:=AfterSh
Else
'If no optional arguments, we made a new workbook and we need to erase the blank worksheet that was created with it if the new sheet is visible (we cant if it's not visible)
If TempSh.Visible = xlSheetVisible Then
Dim Alert As Boolean
Alert = Application.DisplayAlerts
Application.DisplayAlerts = False
Wkb.Sheets(2).Delete
Application.DisplayAlerts = Alert
End If
End If
Set CopySheet = TempSh
End Function
I tried to test my code extensively with worksheets and charts, and I think it does what it was designed for. The only thing to note is that copied sheet won't be visible if the source one was not, EXCEPT if the source one was the first sheet of the workbook.
This should be a comment in response to #TimWilliams, but it's my first post so I can't comment.
This is an example of the problem #RBarryYoung mentioned, related to hidden sheets. There is a problem when you try to put your copy after the last sheet and the last sheet is hidden. It seems that, if the last sheet is hidden, it always retains the highest index, so you need something like
Dim sht As Worksheet
With ActiveWorkbook
.Sheets("Sheet1").Copy After:=.Sheets(.Sheets.Count)
Set sht = .Sheets(.Sheets.Count - 1)
End With
Similar situation when you try to copy before a hidden first sheet.
Based on Trevor Norman's method, I've developed a function for copying a sheet and returning a reference to the new sheet.
Unhide the last sheet (1) if not visible
Copy the source sheet (2) after the last sheet (1)
Set the reference to the new sheet (3), i.e. the sheet after the last sheet (1)
Hide the last sheet (1) if necessary
Code:
Function CopySheet(ByRef sourceSheet As Worksheet, Optional ByRef destinationWorkbook As Workbook) As Worksheet
Dim newSheet As Worksheet
Dim lastSheet As Worksheet
Dim lastIsVisible As XlSheetVisibility
If destinationWorkbook Is Nothing Then Set destinationWorkbook = sourceSheet.Parent
With destinationWorkbook
Set lastSheet = .Worksheets(.Worksheets.Count)
End With
' store visibility of last sheet
lastIsVisible = lastSheet.Visible
' make the last sheet visible
lastSheet.Visible = xlSheetVisible
sourceSheet.Copy After:=lastSheet
Set newSheet = lastSheet.Next
' restore visibility of last sheet
lastSheet.Visible = lastIsVisible
Set CopySheet = newSheet
End Function
This will always insert the copied sheet at the end of the destination workbook.
After this, you can do any moves, renames, etc.
Usage:
Sub Sample()
Dim newSheet As Worksheet
Set newSheet = CopySheet(ThisWorkbook.Worksheets("Template"))
Debug.Print newSheet.Name
newSheet.Name = "Sample" ' rename new sheet
newSheet.Move Before:=ThisWorkbook.Worksheets(1) ' move to beginning
Debug.Print newSheet.Name
End Sub
Or if you want the behaviour/interface to be more similar to the built-in Copy method (i.e. before/after), you could use:
Function CopySheetTo(ByRef sourceSheet As Worksheet, Optional ByRef beforeSheet As Worksheet, Optional ByRef afterSheet As Worksheet) As Worksheet
Dim destinationWorkbook As Workbook
Dim newSheet As Worksheet
Dim lastSheet As Worksheet
Dim lastIsVisible As XlSheetVisibility
If Not beforeSheet Is Nothing Then
Set destinationWorkbook = beforeSheet.Parent
ElseIf Not afterSheet Is Nothing Then
Set destinationWorkbook = afterSheet.Parent
Else
Set destinationWorkbook = sourceSheet.Parent
End If
With destinationWorkbook
Set lastSheet = .Worksheets(.Worksheets.Count)
End With
' store visibility of last sheet
lastIsVisible = lastSheet.Visible
' make the last sheet visible
lastSheet.Visible = xlSheetVisible
sourceSheet.Copy After:=lastSheet
Set newSheet = lastSheet.Next
' restore visibility of last sheet
lastSheet.Visible = lastIsVisible
If Not beforeSheet Is Nothing Then
newSheet.Move Before:=beforeSheet
ElseIf Not afterSheet Is Nothing Then
newSheet.Move After:=afterSheet
Else
newSheet.Move After:=sourceSheet
End If
Set CopySheetTo = newSheet
End Function
It is correct that hidden worksheets cause the new worksheet index to be non-sequential on either side of the source worksheet. I found that Rachel's answer works if you're copying before. But you'd have to adjust it if you're copying after.
Once the model is visible and copied, the new worksheet object is simply the ActiveSheet whether you copy the source before or after.
As a preference, you could replace:
Set newSheet = .Previous with Set newSheet = Application.ActiveSheet.
Hope this is helpful to some of you.
As already mentioned here, copy/paste the sheet to the very left (index = 1), then assign it to a variable, then move it where you would like.
Function CopyWorksheet(SourceWorksheet As Worksheet, AfterDestinationWorksheet As Worksheet) As Worksheet
Dim DestinationWorkbook As Workbook
Set DestinationWorkbook = AfterDestinationWorksheet.Parent
Dim FirstSheetVisibility As XlSheetVisibility
FirstSheetVisibility = DestinationWorkbook.Sheets(1).Visible
DestinationWorkbook.Sheets(1).Visible = xlSheetVisible
SourceWorksheet.Copy Before:=DestinationWorkbook.Sheets(1)
DestinationWorkbook.Sheets(2).Visible = FirstSheetVisibility
Dim NewWorksheet As Worksheet
Set NewWorksheet = DestinationWorkbook.Sheets(1)
NewWorksheet.Move After:=AfterDestinationWorksheet
Set CopyWorksheet = NewWorksheet
End Function
I had the same requirement and came to this thread while looking for an answer. While checking out various options, found that, a easy way to access the new sheet is, using the chain of references that Excel stores (sample below). It seems like Excel maintains a linked list kind of thing w.r.t the sheet references.
'Example:
ActiveWorkbook.Sheets("Sheet1").Copy After:=someSheet
set newSheet = someSheet.Next
Similarly for the sheet inserted 'before' another sheet...
ActiveWorkbook.Sheets("Sheet1").Copy Before:=someSheet
set newSheet = someSheet.Previous
Works even if the source sheet is hidden. If the source sheet is hidden, the worksheet is copied, but the new sheet remains hidden too!
I've been trying to create a reliable generic "wrapper" function for the sheet.Copy method for re-use across multiple projects for years.
I've tried several of the approaches here and I've found only Mark Moore's answer to be a reliable solution across all scenarios. Ie the one using the "Template (2)" name to identify the new sheet.
In my case, any solution using the "ActiveSheet method" was useless as in some instances the target workbook was in a non-Active or hidden Workbook.
Similarly, some of my Workbooks have hidden sheets intermixed with visible sheets in various locations; at the beginning, in the middle, at the end; and therefore I found the solutions using the Before: and After: options also unreliable depending on the ordering of the visible and hidden sheets, along with the additional factor when the source sheet is also hidden.
Therefore after several re-writes, I've ended up with the following wrapper function:
'***************************************************************************
'This is a wrapper for the worksheet.Copy method.
'
'Used to create a copy of the specified sheet, optionally set it's name, and return the new
' sheets object to the calling function.
'
'This routine is needed to predictably identify the new sheet that is added. This is because
' having Hidden sheets in a Workbook can produce unexpected results in the order of the sheets,
' eg when adding a hidden sheet after the last sheet, the new sheet doesn't always end up
' being the last sheet in the Worksheets collection.
'***************************************************************************
Function wsCopy(wsSource As Worksheet, wsAfter As Worksheet, Optional ByVal sNewSheetName As String) As Worksheet
Dim Ws As Worksheet
wsSource.Copy After:=wsAfter
Set Ws = wsAfter.Parent.Sheets(wsSource.Name & " (2)")
'set ws Name if one supplied
If sNewSheetName <> "" Then
Ws.Name = sNewSheetName
End If
Set wsCopy = Ws
End Function
NOTE: Even this solution will have issues if the source sheet's Name is more than 27 chars, as the maximum sheet name is 31, but that is usually under my control.
Old post but wasn't sure about unhiding sheets or adding suffixes to names.
This is my approach:
Sub DuplicateSheet()
Dim position As Integer
Dim wbNewSheet As Worksheet
position = GetFirstVisiblePostion
ThisWorkbook.Worksheets("Original").Copy Before:=ThisWorkbook.Sheets(position)
Set wbNewSheet = ThisWorkbook.Sheets(position)
Debug.Print "Duplicated name:" & wbNewSheet.Name, "Duplicated position:" & wbNewSheet.Index
End Sub
Function GetFirstVisiblePostion() As Integer
Dim wbSheet As Worksheet
Dim position As Integer
For Each wbSheet In ThisWorkbook.Sheets
If wbSheet.Visible = xlSheetVisible Then
position = wbSheet.Index
Exit For
End If
Next
GetFirstVisiblePostion = position
End Function
Wanted to share my simple solution to this with the following code
Sub copy_sheet(insheet As String, newsheet As String)
Application.DisplayAlerts = False
On Error Resume Next
ThisWorkbook.Sheets(newsheet).Delete
ThisWorkbook.Sheets(insheet).Copy before:=ThisWorkbook.Sheets(1)
For Each ws In ThisWorkbook.Worksheets
If (InStr(ws.Name, insheet) > 0 And InStr(ws.Name, "(") > 0) Then
ThisWorkbook.Sheets(ws.Name).Name = newsheet
Exit For
End If
Next
Application.DisplayAlerts = True
End Sub
Whenever you copy a sheet, the resulting "copied" sheet ALWAYS has the name of the original sheet, and a bracketed number. As long as none of your original sheets contain bracketed number names, this will work 100% of the time.
It copies the sheet, then loops through all sheet names looking for one that 1) contains the original name and 2) has a bracketed number, and then renames the sheet
I had the same problem as OP, but with the addition of some hidden and very hidden sheets.
Finding the last sheet by using something like
{set last_sheet = ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count)} does not work because Excel does not count the hidden worksheets, so the position number {last_sheet.Index + 1} is too high and makes an error.
Instead I made a loop to find the position:
Dim w as Workbook, s as Worksheet, template_sheet as worksheet, last_sheet as Worksheet, new_sheet as Worksheet
' find the position of the last sheet
For Each s in w.Workbooks
If s.Visible = xlSheetVisible then
Set last_sheet = s
End if
Next
' make the sheet to be copied visible, copy it and hide it again
w.Worksheets("template_sheet").Visible = xlHidden
w.Worksheets("template_sheet").Copy After:=last_sheet
w.Worksheets("template_sheet").Visible = xlVeryHidden
' reference the new sheet that was just added
Set new_sheet = Worksheets(last_sheet.index + 1)

Resources