Creating a new spreadsheet from a template - excel

I am developing code which creates a copy of a template spreadsheet whenever text is input into any row within column A. The spreadsheet needs to be named after the text entered.
Currently I have the following code, the problem is that it does not name the new spreadsheet after the text I enter.
The code is as below:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim wsNew As Worksheet
If Target.Cells.Count > 1 Then Exit Sub
On Error Resume Next
If Not Intersect(Target, Range("A1:A10")) Is Nothing Then
Set wsNew = Sheets(Target.Text)
If wsNew Is Nothing Then
Worksheets("Template").Copy After:=Worksheets(Worksheets.Count)
End If
'name new sheet code here
End If
End Sub

Like this:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim wsNew As Worksheet
If Target.Cells.Count > 1 Then Exit Sub
On Error Resume Next
If Not Intersect(Target, Range("A1:A10")) Is Nothing Then
Set wsNew = Sheets(Target.Text)
If wsNew Is Nothing Then
Worksheets("Template").Copy After:=Worksheets(Worksheets.Count)
End If
'name new sheet
Worksheets(Worksheets.Count).Name = Target.Text
End If
End Sub
Edit:
User could empty the cell in A1:A10 which will create new tab called "Template (2)". You should also do check:
If Len(Target.Cells.Text) = 0 Then Exit Sub

I'd suggest something like this to create the sheet based on the template with the desired name - but after testing and cleansing the proposed sheet name first for invalid characters
Private Sub Worksheet_Change(ByVal Target As Range)
Dim wsNew As Worksheet
Dim strSht As String
If Target.Cells.Count > 1 Then Exit Sub
If Not Intersect(Target, Range("A1:A10")) Is Nothing Then
On Error Resume Next
Set wsNew = Sheets(Target.Text)
On Error GoTo 0
If wsNew Is Nothing Then
If ValidSheetName(Target.Value) Then
strSht = Target.Value
Else
strSht = CleanSheetName(Target.Value)
End If
End If
Worksheets("Template").Copy After:=Worksheets(Worksheets.Count)
ActiveSheet.Name = strSht
End If
End Sub
string cleaning code 1
Function ValidSheetName(strIn As String) As Boolean
Dim objRegex As Object
Set objRegex = CreateObject("vbscript.regexp")
objRegex.Pattern = "[\<\>\*\\\/\?|]"
ValidSheetName = Not objRegex.test(strIn)
End Function
string cleaning code 2
Function CleanSheetName(strIn As String) As String
Dim objRegex As Object
Set objRegex = CreateObject("vbscript.regexp")
With objRegex
.Global = True
.Pattern = "[\<\>\*\\\/\?|]"
CleanSheetName = .Replace(strIn, "_")
End With
End Function

Related

Using the Worksheet_Change event on a specific sheet only

So I have a dynamically created worksheet (generated by clicking a command button on a separate sheet) and I am trying to get a Worksheet_Change event to fire only on a specific range in that worksheet. My code is as follows:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim sh As Worksheet
Set sh = ThisWorkbook.ActiveSheet
If Not Intersect(Target, Range("A1:K10")) Is Nothing Then
If sh.Name Like "*SP Temp*" Then
Dim i As Variant, countOfS As Integer
countOfS = 0
For Each i In sh.Range("A1:K10")
If i.Value = "S" Then
countOfS = countOfS + 1
End If
Next i
sh.Range("D12").Value = countOfS
sh.Range("D13").Value = SCount - countOfS
' NOTE: SCount is a global variable set in another Sub
End If
End If
End Sub
The intent is to keep a running count of the number of "S" characters entered into cells in the range A1:K10. I have tried adding in Debug.Print statements after the If Not Intersect... statement, but it doesn't seem to fire, despite the values in the target range being altered. What am I doing wrong?
A Workbook SheetChange: Count and Write
ThisWorkbook Module
Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
' Invalidate.
If Not Sh.Name Like "*SP Temp*" Then Exit Sub ' wrong worksheet
Dim rg As Range: Set rg = Sh.Range("A1:K10")
If Intersect(rg, Target) Is Nothing Then Exit Sub ' no intersection
' Count.
Dim CountOfS As Long: CountOfS = Application.CountIf(rg, "s")
' Write
' Disable events to not re-trigger the event while writing
' since the same worksheet is being written to.
Application.EnableEvents = False
Sh.Range("D12").Value = CountOfS
Sh.Range("D13").Value = SCount - CountOfS
Application.EnableEvents = True
End Sub

How to Merge Two Worksheet_Change events into one

I am fairly new to VBA and struglling with the idea on how to merge both of these subs into one, as i need to enable dynamic filters for two separate Pivots.
Private Sub Worksheet_Change(ByVal Target As Range)
Dim xPTable As PivotTable
Dim xPFile As PivotField
Dim xStr As String
On Error Resume Next
If Intersect(Target, Range("L3:L4")) Is Nothing Then Exit Sub
Application.ScreenUpdating = False
Set xPTable = Worksheets("Summary").PivotTables("PivotTable1")
Set xPFile = xPTable.PivotFields("Machine")
xStr = Target.Text
xPFile.ClearAllFilters
xPFile.CurrentPage = xStr
Application.ScreenUpdating = True
End Sub
To combine with this
Private Sub Worksheet_Change(ByVal Target As Range)
Dim xPTable As PivotTable
Dim xPFile As PivotField
Dim xStr As String
On Error Resume Next
If Intersect(Target, Range("P16:P17")) Is Nothing Then Exit Sub
Application.ScreenUpdating = False
Set xPTable = Worksheets("Summary").PivotTables("PivotTable2")
Set xPFile = xPTable.PivotFields("Machine")
xStr = Target.Text
xPFile.ClearAllFilters
xPFile.CurrentPage = xStr
Application.ScreenUpdating = True
End Sub
Appreciate any help, thank you!
Rather than just Exiting if there is no intersection, flip it around and proceed if there is an intersection.
Your code, refactored along with a few other improvements
Private Sub Worksheet_Change(ByVal Target As Range)
Dim xPTable As PivotTable
Dim xPFile As PivotField
Dim xStr As String
Application.ScreenUpdating = False
If Target.CountLarge > 1 Then
' User changed >1 cells. What now?
Exit Sub
End If
' On Error Resume Next <~~ don't do this globally!
If Not Intersect(Target, Me.Range("L3:L4")) Is Nothing Then
On Error Resume Next '<~~ Keep it tight around a potential error
' If the Change event is on Sheet Summary, use Me instead
Set xPTable = Me.PivotTables("PivotTable1")
' If the Change Event is NOT on Sheet Summary, be explicit on the workbook
'Set xPTable = Me.Parent.Worksheets("Summary").PivotTables("PivotTable1")
On Error GoTo 0
ElseIf Not Intersect(Target, Me.Range("P16:P17")) Is Nothing Then
On Error Resume Next
Set xPTable = Me.PivotTables("PivotTable2")
On Error GoTo 0
End If
If Not xPTable Is Nothing Then
On Error Resume Next '<~~ in case Machine doesn't exist
Set xPFile = xPTable.PivotFields("Machine")
On Error GoTo 0
If Not xPFile Is Nothing Then
xStr = Target.Value ' .Text is dangerous. Eg it can truncate if the column is too narrow
xPFile.ClearAllFilters
xPFile.CurrentPage = xStr
End If
End If
Application.ScreenUpdating = True
End Sub
I think there are more options for refactoring.
Put the basic routine into a seperate sub in a modul. This sub can then be called from the _change-events of both sheets. Advantage: if you want to change the logic of the sub - you do it in one place, not two. Or maybe there will be a third sheet that wants to use the same logic. (DRY-principle: don't repeat yourself)
I like to "externalize" on error resume next if necessary into tryGet-functions. Thereby minimizing the risk of its usage (which is in this case ok)
This is the generic sub - based on chris neilsens suggestion plus the comments from VBasic2008
Maybe you adjust the name of the sub to be more precise in what you want to achieve.
Public Sub handleMachineField(Target As Range, RangeToCheck As Range, PTName As String)
On Error GoTo err_handleMachineField
Dim xPTable As PivotTable
Dim xPFile As PivotField
Dim xStr As String
Application.ScreenUpdating = False
If Target.CountLarge > 1 Then
' User changed >1 cells. What now?
Exit Sub
End If
If Not Intersect(Target, RangeToCheck) Is Nothing Then
Set xPTable = tryGetPivotTable(Target.Parent, PTName)
End If
If Not xPTable Is Nothing Then
Set xPFile = tryGetPivotField(xPTable, "Machine")
If Not xPFile Is Nothing Then
xStr = Target.Value ' .Text is dangerous. Eg it can truncate if the column is too narrow
Application.EnableEvents = False
xPFile.ClearAllFilters
xPFile.CurrentPage = xStr
Application.EnableEvents = True
End If
End If
exit_handleMachineField:
Application.ScreenUpdating = True
Application.EnableEvents = True
Exit Sub
err_handleMachineField:
MsgBox Err.Description
Resume exit_handleMachineField
End Sub
Public Function tryGetPivotTable(ws As Worksheet, PTName As String) As PivotTable
'in case pivot table does not exist no error is thrown
'calling sub has to check for nothing instead
On Error Resume Next
Set tryGetPivotTable = ws.PivotTables(PTName)
On Error GoTo 0
End Function
Public Function tryGetPivotField(pt As PivotTable, FieldName As String) As PivotField
'in case field does not exist no error is thrown
'calling sub has to check for nothing instead
On Error Resume Next
Set tryGetPivotField = pt.PivotFields(FieldName)
On Error GoTo 0
End Function
And this is how you would call it form the worksheet events:
Private Sub Worksheet_Change(ByVal Target As Range)
handleMachineField Target, Me.Range("L3:L4"), "PivotTable1"
End Sub
By the way: this is another advantage of putting the check into a sub. When reading the code in the change-event you immediately know what will happen - you don't have to read through all the code lines to understand what is going on.

VBA Vlookup Crashing

My VBA Vlookup code is crashing Excel and takes forever to execute. I need the VBA code and not the formula in the cell.
Private Sub Worksheet_Change(ByVal Target As Range)
Call lookup
End Sub
Sub lookup()
Dim srchres As Variant
Dim srch As Variant
Set sh1 = ThisWorkbook.Sheets("Customer")
Set sh4 = ThisWorkbook.Sheets("Invoice")
On Error Resume Next
srchres = Application.WorksheetFunction.VLookup(sh4.Range("A11:C11"), _
sh1.Range("B2:H99999"), 5, False)
On Error GoTo 0
If (IsEmpty(srchres)) Then
sh4.Range("A12") = CVErr(xlErrNA)
Else
sh4.Range("A12:C12").Value = srchres
End If
On Error Resume Next
srch = Application.WorksheetFunction.VLookup(sh4.Range("A11:C11"), _
sh1.Range("B2:H99999"), 6, False)
On Error GoTo 0
If (IsEmpty(srch)) Then
sh4.Range("A13:C13") = CVErr(xlErrNA)
Else
sh4.Range("A13:C13").Value = srch
End If
End Sub
ThisWorkbook's code for creating a drop-down list
Private Sub Worksheet_Change(ByVal Target As Range)
Dim MySheets As Variant
Dim srchres As Variant
Dim srch As Variant
Set sh1 = ThisWorkbook.Sheets("Customer")
Set sh4 = ThisWorkbook.Sheets("Invoice")
MySheets = Array("sh1", "sh4")
If IsNumeric(Application.match(Sh.Name, MySheets, 0)) Then
If Target.Address = "'sh4'!$A$11" Then
sh4.Range("A2:A9999").Cells(sh4.Range("A2:A9999").Rows.Count + 1, 1) = Target
End If
End If
End Sub

Trim Cells in entire sheet, Overflow Error

I have my code which loads 2 workbooks, and copies them into a master workbook. However I am getting an
overflow error
when i'm trying to trim all cells in the pasted sheets (too remove the spaces).
Does anyone know why this overflow error would occur when trimming excess blank spaces in a whole sheet? Specifically I am getting the error on this part Target = Target.Value .
Sub Load()
LoadDailyWorkbook
LoadLastWeeksWorkbook
End Sub
Sub LoadDailyWorkbook()
Const A1BJ200 As String = "A1:BJ200"
Const A1L3 As String = "A1:L3"
Dim masterWB As Workbook
Dim dailyWB As Workbook
'Set Current Workbook as Master
Set masterWB = Application.ThisWorkbook
'Set some Workbook as the one you are copying from
Set dailyWB = getWorkbook(Sheets("Control Manager").Range("O2"))
If Not dailyWB Is Nothing Then
With dailyWB
'Copy the Range from dailyWB and Paste it into the MasterWB
.Worksheets("Summary1").Range(A1BJ200).Copy masterWB.Worksheets("Summary").Range("A1")
TrimRange masterWB.Worksheets("Summary").Range(A1BJ200)
'repeat for next Sheet
.Worksheets("risk1").Range(A1BJ200).Copy masterWB.Worksheets("risk").Range("A1")
TrimRange masterWB.Worksheets("risk").Range(A1BJ200)
'repeat for CS sheet
.Worksheets("CS today").Range(A1L3).Copy masterWB.Worksheets("CS").Range("A1").Rows("1:1")
TrimRange masterWB.Worksheets("CS").Range(A1L3)
.Close SaveChanges:=False
End With
End If
End Sub
Sub LoadLastWeeksWorkbook()
Const A1BJ200 As String = "A1:BJ200"
Dim masterWB As Workbook
Dim lastweekWB As Workbook
'Set Current Workbook as Master
Set masterWB = Application.ThisWorkbook
''''''''''''Get Last Week Data''''''''''''''''''''''
Set lastweekWB = getWorkbook(Workbooks.Open(Sheets("Control Manager").Range("O3")))
If Not lastweekWB Is Nothing Then
With lastweekWB
'repeat for next risk Sheet
.Worksheets("risk2").Range(A1BJ200).Copy masterWB.Worksheets("risk_lastweek").Range("A1")
TrimRange masterWB.Worksheets("risk_lastweek").Range(A1BJ200)
TrimRange masterWB.Columns("A:BB")
.Close SaveChanges:=False
End With
End If
End Sub
Function getWorkbook(FullName As String) As Workbook
If Len(Dir(FullName)) = 0 Then
MsgBox FullName & " not found found", vbCritical, "File Not Found"
Else
Set getWorkbook = Workbooks.Open(FullName)
End If
End Function
Sub TrimRange(Target As Range)
Dim results As Variant
Set Target = Intersect(Target.Parent.UsedRange, Target)
If Target Is Nothing Then
Exit Sub
ElseIf Target.Count = 1 Then
Target.Value = Trim(Target.Value)
Exit Sub
Else
Target = Target.Value
Dim r As Long, c As Long
For r = 1 To UBound(results)
For c = 1 To UBound(results, 2)
results(r, c) = Trim(results(r, c))
Next
Next
Target.Value = results
End If
Target.Columns.EntireColumn.AutoFit
End Sub
Sub TrimRange(Target As Range)
Dim results As Variant
And yet, you do not set results before you use it.
For r = 1 To UBound(results)
So you are calling UBound on some thing that does not exist.
In addition, when I have changed formulas to values, I have used Target.Value = Target.Value instead of Target = Target.Value. I know the .Value is usually the default value, but I never trust implicit stuff to work all the time.

Generate sheets corresponding to row values (duplicate values exist)

I have a main worksheet (Install_Input) where sheet number, test section, and material are manually entered by user.
(Below: illustration of Install_Input ws: Range A1:C8)
Sheet# | TestSection | Material
.....1.....|..........A..........|.STEEL.|
.....2.....|..........B..........|.PLASTIC.|
.....3.....|..........C..........|.STEEL.|
.....5.....|..........G..........|.STEEL.|
.....2.....|..........F..........|.PLASTIC.|
.....2.....|..........A..........|.STEEL.|
.....5.....|..........D..........|.PLASTIC.|
I want to generate sheets within the current workbook that correspond to sheet numbers entered in Install_Input. The code I made will generate a new sheet for each value in MyRange, however, I would like for my code to skip over generating sheets that already exist. I tried using the "On Error Resume Next" and "On Error GoTo 0" commands to solve this problem, but they just generated unnamed sheets to compensate for those that already exist.
Sub Consolidate_Sheets()
Dim MyCell As Range
Dim MyRange As Range
Dim ws As Worksheet
Set MyRange = Sheets("Install_Input").Range("A2")
Set MyRange = Range(MyRange, MyRange.End(xlDown))
For Each MyCell In MyRange
If Sheets(Sheets.Count).Name <> MyCell.Value Then
'On Error Resume Next
Sheets.Add After:=Sheets(Sheets.Count)
Sheets(Sheets.Count).Name = MyCell.Value
'On Error GoTo 0
End If
Next MyCell
End Sub
You can use the following two functions:
Function getSheetWithDefault(name As String, Optional wb As Excel.Workbook) As Excel.Worksheet
If wb Is Nothing Then
Set wb = ThisWorkbook
End If
If Not sheetExists(name, wb) Then
wb.Worksheets.Add(After:=wb.Worksheets(wb.Worksheets.Count)).name = name
End If
Set getSheetWithDefault = wb.Sheets(name)
End Function
Function sheetExists(name As String, Optional wb As Excel.Workbook) As Boolean
Dim sheet As Excel.Worksheet
If wb Is Nothing Then
Set wb = ThisWorkbook
End If
sheetExists = False
For Each sheet In wb.Worksheets
If sheet.name = name Then
sheetExists = True
Exit Function
End If
Next sheet
End Function
To use it in your code:
Sub Consolidate_Sheets()
Dim MyCell As Range
Dim MyRange As Range
Dim ws As Worksheet
Set MyRange = Sheets("Install_Input").Range("A2")
Set MyRange = Range(MyRange, MyRange.End(xlDown))
For Each MyCell In MyRange
If Sheets(Sheets.Count).Name <> MyCell.Value Then
'On Error Resume Next
set ws = getSheetWithDefault(MyCell.Value)
'On Error GoTo 0
End If
Next MyCell
End Sub
You could implement a CheckSheet function like the one described in this SO answer that loops through all existing sheets and compares the name of each sheet with the passed-in value.

Resources