I want to execute an VBA event handler in an Excel 10 worksheet whenever the user adds a new row into a list (the kind of lists that Worksheet.ListObjects() returns instances of) on that worksheet, for example by entering data under the last row of the list (this expands the list by adding a new row to the list).
How do I do that? Among other things, I want to set defaults for specific cells of the new row.
My current idea is to handle Worksheet_Change, and check if the Target parameter is within the .Range of the ListObject I am interested in.
However, how would I find out if the user is creating a new row with his/her cell change, and differentiate that from edits of existing cells in the list?
I probably am just a little bit stupid here. I´d expect there would be a list event I could trap, but I cannot find any.
I think you are right, there are no Events for ListObject's. Using Worksheet_Change seems the right way to go. To detect New Row vs Existing Row edit you will need to roll you own method.
I would suggest tracking the number of rows in the ListOjects in order to detect when they change. In order to do this, try adding a hidden named range for each ListOject to hold the current number of rows. Populate them on file open, and test them on Worksheet_Change.
This will add or update hidden named ranges on file open (add to Workbook module)
Private Sub Workbook_Open()
Dim oList As ListObject
Dim sh As Worksheet
Dim nm As Name
Dim strName As String
For Each sh In Me.Worksheets
For Each oList In sh.ListObjects
'oList.ListRows.Count
strName = oList.Name & "Rows"
Set nm = Nothing
On Error Resume Next
Set nm = Me.Names(strName)
On Error GoTo 0
If nm Is Nothing Then
Set nm = Me.Names.Add(strName, CStr(oList.ListRows.Count))
Else
nm.RefersTo = CStr(oList.ListRows.Count)
End If
nm.Visible = False
Next oList, sh
End Sub
This will detect what type of change was made. I've made it a WorkBook level event, so only one is needed for all sheets. (add to Workbook module)
Private Sub Workbook_SheetChange(ByVal sh As Object, ByVal Target As Range)
Dim oList As ListObject
Dim nm As Name
Dim strName As String
For Each oList In sh.ListObjects
strName = oList.Name & "Rows"
If Not Application.Intersect(Target, oList.DataBodyRange) Is Nothing Then
Set nm = Nothing
On Error Resume Next
Set nm = Me.Names(strName)
On Error GoTo 0
If nm Is Nothing Then
Set nm = Me.Names.Add(strName, CStr(oList.ListRows.Count))
nm.Visible = False
End If
If oList.ListRows.Count <> Val(Replace(nm.Value, "=", "")) Then
nm.RefersTo = CStr(oList.ListRows.Count)
MsgBox "List " & oList.Name & " changed" & vbCrLf & "New Line"
Else
MsgBox "List " & oList.Name & " changed" & vbCrLf & "Existing Line"
End If
End If
Next
End Sub
Note: this does not handle the case where the name of an existing ListObject is changed. This is left as an exercise for the reader
Related
I am writing a VBA code where I need to find if sheet name given by user through inputbox is available or not in a workbook containing many sheets.
But if the sheet name is not available then the inputbox pops up again to enter the sheet name so it can search again.
I have written 1st part of the code which is working fine but I need help with the 2nd part(if the sheet name is not available). Let me know if this is possible in for loop or I have to use other loop?
Sub callbyinputbox()
Dim pendworkbook As Workbook
Dim sht As Worksheet
Dim entername As String
Set pendworkbook = Workbooks("pend_app_new.xlsx")
entername = InputBox("Enter name", "Search Sheet")
For Each sht In pendworkbook.Worksheets
If sht.Name = entername Then
pendworkbook.Sheets(entername).Activate
Exit Sub
End If
Next sht
MsgBox ("You entered " & entername & vbNewLine & "Sheet by this name is not available")
end sub
Here is your code:
entername = InputBox("Enter name", "Search Sheet")
For Each sht In pendworkbook.Worksheets
If sht.Name = enterDate Then
pendworkbook.Sheets(entername).Activate
Exit Sub
End If
Next sht
You store the response from the user in a variable: entername.
You then loop through all the sheets and check if the name matches a variable called enterDate.
Change this to entername and it will then have something to match against, and the If block will run.
Check out using Option Explicit - this would have highlighted this issue for you.
UPDATE:
This is probably breaking an unwritten rule somewhere, but a simple Do Until False loop, which will permanently run (until the Exit Sub condition is reached and breaks the loop) will keep asking until a valid sheetname is input.
Alternatively, you could use a For.. Next loop. That way, you could set a maximum number of prompts before giving up..
Note: I have made this comparison case insensitive - to give the user a better chance of inputting a correct sheet name.
Sub callbyinputbox()
Dim pendworkbook As Workbook
Dim sht As Worksheet
Dim entername As String
Set pendworkbook = Workbooks("pend_app_new.xlsx")
Do Until False
entername = InputBox("Enter name", "Search Sheet")
For Each sht In pendworkbook.Worksheets
If LCase(sht.Name) = LCase(entername) Then
pendworkbook.Sheets(entername).Activate
Exit Sub
End If
Next sht
MsgBox ("You entered " & entername & vbNewLine & "Sheet by this name is not available")
Loop
End Sub
All the while, I have attempted to correct your code and explain the reasoning. For that reason, I have tried to keep as much of your code as possible and just steer you toward your goal. If I was writing this from scratch, I would use the approach suggested by Ike.
To handle all usecases (no input given, existing sheetname given, non-existing sheetname given) - you can use this code:
Public Sub activateSheetByUserInput()
Dim pendworkbook As Workbook
Dim sht As Worksheet
Dim entername As String
Set pendworkbook = Workbooks("pend_app_new.xlsx")
retry:
entername = InputBox("Enter name", "Search Sheet")
If LenB(entername) = 0 Then
Exit Sub
ElseIf tryGetWorksheetByName(pendworkbook, entername, sht) = True Then
sht.Activate
Else
'give the user the option to cancel the process
If vbCancel = MsgBox("You entered " & entername & vbNewLine & "Sheet by this name is not available", vbCritical + vbRetryCancel) Then
Exit Sub
Else
GoTo retry
End If
End If
End Sub
'function returns true if worksheet was found - plus the ws itself
Private Function tryGetWorksheetByName(ByVal wb As Workbook, ByVal strName, ByRef sht As Worksheet) As Boolean
On Error Resume Next 'one of the rare cases where it is valid to use on error resume next
Set sht = wb.Worksheets(strName)
If Err = 0 Then tryGetWorksheetByName = True
On Error GoTo 0
End Function
Another way would be to supply a list of valid sheet names and ask them to select one.
Add a userform and place a listbox on it. I've left the default names, but would be better to name the controls to something relevant.
Add this code to the form:
Private Sub UserForm_Initialize()
Dim pendworkbook As Workbook
Set pendworkbook = Workbooks("pend_app_new.xlsx")
Dim shts() As Variant
ReDim shts(0 To 0, 0 To pendworkbook.Worksheets.Count - 1)
Dim x As Long
For x = 1 To pendworkbook.Worksheets.Count
shts(0, x - 1) = pendworkbook.Worksheets(x).Name
Next x
ListBox1.List = Application.WorksheetFunction.Transpose(shts)
End Sub
Private Sub ListBox1_Click()
MsgBox "You clicked " & ListBox1.Value
End Sub
I have a workbook with one worksheet Sheet1. On that Sheet I have one table with its default name Table1.
When I copy the worksheet Right-Click > Move or Copy in the same workbook I get sheet Sheet1 (2).
The Table on this sheet is automatically named Table13.
I do some processing in that copied sheet and subsequently remove it. Leaving the workbook with only its original Sheet1.
Each time I make a copy of Sheet1 the table in the copied sheet is incremented by one.
Also if I remove the sheet and add a new one. It keeps incrementing.
I use the workbook and Sheet1 as a template and I create via a macro a lot of copies.
The new Table Name has now Incremented to Table21600.
I found out that Excel will give an overflow when I reach approximately Table21650.
So, I need a way to reset the Name counter of the added table.
Does anyone know how to achieve this?
You can access (and alter) the names of each table ("ListObject") from your macro-code as shown in this example:
Sub ListAllListObjectNames()
Dim wrksheet As Worksheet
Dim lstObjct As ListObject
Dim count As Integer
count = 0
For Each wrksheet In ActiveWorkbook.Worksheets
For Each lstObjct In wrksheet.ListObjects
count = count + 1
lstObjct.Name = "Table_" & CStr(count)
Debug.Print wrksheet.Name, ": ", lstObjct.Name
Next
Next
End Sub
Reset Table 'Counter'
Allthough the 'counter' will not stop incrementing, when you close
the workbook and open it the next time, it will again start from
Table13.
In the Immediate window CRTL+G you will see the table name
before and after the renaming. When done testing just out comment the
lines containing Debug.Print.
The First Code
' Copies a sheet and renames all its tables.
Sub CopySheetWithTable(Optional SheetNameOrIndex As Variant = "Sheet1", _
Optional NewTableName As String = "Tbl")
Dim MySheet As Worksheet
Dim MyCopy As Worksheet
Dim MyTable As ListObject
Dim i As Long
Set MySheet = ThisWorkbook.Worksheets(SheetNameOrIndex)
'MySheet.Copy MySheet ' Before e.g. Sheet1)
MySheet.Copy , MySheet ' After e.g. Sheet1
Set MyCopy = ActiveSheet
For Each MyTable In MyCopy.ListObjects
i = i + 1
Debug.Print "Old Table Name = " & MyTable.Name
MyTable.Name = NewTableName & i
Debug.Print "Old Table Name = " & MyTable.Name
Next
End Sub
Usage
Copy the previous and the following sub into a module. Run the
following sub to copy a new worksheet. Adjust if you want it before
or after the sheet to be copied.
You don't need to copy the worksheet manually anymore.
The Second Code
' You can create a button on the worksheet and use this one-liner in its code.
Sub CopySheet()
CopySheetWithTable ' Default is CopySheetWithTable "Sheet1", "Tbl"
End Sub
Delete all Sheets After Worksheet
This is just a testing tool.
' Deletes all sheets after the selected sheet (referring to the tab order).
Sub DeleteSheetsAfter(DeleteAfterSheetNameOrIndex As Variant) 'Not tested.
Dim LastSheetNumber As Long
Dim SheetsArray() As Variant
Dim i As Long
' Try to find the worksheet in the workbook containing this code.
On Error Resume Next
LastSheetNumber = _
ThisWorkbook.Worksheets(DeleteAfterSheetNameOrIndex).Index
If Err.Number <> 0 Then
MsgBox "There is no Sheet '" & DeleteAfterSheetNameOrIndex & "' " _
& "in (this) workbook '" & ThisWorkbook.Name & "'."
Exit Sub
End If
With ThisWorkbook
ReDim SheetsArray(.Sheets.Count - LastSheetNumber - 1)
For i = LastSheetNumber + 1 To .Sheets.Count
SheetsArray(i - LastSheetNumber - 1) = i
Next
End With
Application.DisplayAlerts = False
ThisWorkbook.Sheets(SheetsArray).Delete
Application.DisplayAlerts = True
MsgBox "Deleted " & UBound(SheetsArray) & " worksheets after worksheet '" _
& ThisWorkbook.Worksheets(DeleteAfterSheetNameOrIndex).Name & "'.", _
vbInformation, "Delete Successful"
End Sub
Sub DeleteAfter()
DeleteSheetsAfter "Sheet1"
End Sub
First time programmer here, started teaching myself VBA a few days ago to write this. The goal is to have the code be able to reference two workbooks that are not constants. One is selected by the user and the other is running the macro. I have defined the workbooks in a sub statement previous, but when I try to reference it in a sub statement further down the line I get error '9' "subscript out of range." I have tried using call but it also came up with undefined errors (it could be that I don't understand the 'call' statement).
If you have an additional moment to look over my formula and make sure it is formatted correctly that would be a big help as well. I just know it is going to be a huge problem when I get there.
P.S. I just noticed that I have been spelling reference wrong in my code this entire time. Go ahead, laugh.
'''
Sub Openfile()
Dim FileToOpen As Variant, wbRefrence As Workbook
Dim wbOracle As Workbook
Set wbOracle = ThisWorkbook
FileToOpen = Application.GetOpenFilename(FileFilter:="Excel Workbooks (*.xls*),*.xls*", Title:="Open Database File")
If FileToOpen = False Then
MsgBox "No file selected, cannot continue." 'If the user does not open a file this message is displayed
Exit Sub 'If no file is selected the program stops running
End If
Set wbRefrence = Workbooks.Open(FileToOpen)
Workbooks.Open (FileToOpen) 'If a file is selected it opens that file.
Call LoopTest1
End Sub
Sub LoopTest1()
Dim BlankCell As Boolean
Dim i As Long
'Loop until a blank cell is encountered
Do While BlankCell = False
i = i + 1
If Cells(i, "C").Value = "" Then
BlankCell = True 'When it reaches a blank cell BlankCell will now be true which ends the do while formula.
End If
Application.Workbooks("wbOracle").Sheets("Cancel Requisition Lines").Range("C16").Select
'Formula for "do while" condition
Selection.Formula = "=IF(INDEX(['wbRefrence']Sheet1!'A2000:M2000',MATCH(1,(['wbRefrence']Sheet1!'D:D'=['wbOracle']'Cancel Requisition Lines'!'C16')*(['wbRefrence']Sheet1!'E:E'=['wbOracle']'Cancel Requisition Lines'!'I16')*(['wbRefrence']Sheet1!'F:F'=['wbOracle']'Cancel Requisition Lines'!'J16'),0),9)>=['wbOracle']'Cancel Requisition Lines'!M:M, ""materials supplied"","""")"
Loop
End Sub
'''
You've got a great start to your code, so here are a few things to help get you on your way...
Always use Option Explicit.
Try to define your variables as close as possible to its first use (your current code is short enough to not matter much, its just a habit to get into).
The Call usage has been deprecated and it's not needed. If you want to call a function or sub, just use the name of that routine.
Also, if you have a sub call that is by itself in a single statement, the parens are NOT required to enclose the parameters. If you're making the call in a compound or assignment statement, you MUST use the parens.
A good habit is to always make it clear what workbook, worksheet, and range you are referencing with a fully qualified reference every single time. This one thing trips up so many VBA users.
For example, in your LoopTest1 code, you are referring to Cells. Without any qualifying reference, the VBA code assumes you are referring to the currently active worksheet (whichever and whereever that is). So define some intermediate variables and make it clear (see example below).
To help clear up any confusion in your LoopTest1 sub, I added some parameters so that you can work on any two workbooks you choose.
My own preference is to build up a complicated formula in a separate string variable so that I can examine it in the debugger and make sure it's exactly right. So you can see I defined a formulaText string and built up your formula.
I "corrected" a few things I found in the formula (but I cannot tell you it will work), including:
Using the FullName property of both workbooks in the formula (so you never have it hard-coded)
Using the Name property of the worksheet (so you never have it hard-coded)
Properly arranging the single-tick marks for the proper workbook/worksheet reference (overall, you were using too many single-ticks in your formula)
Only you can determine if the formula is actually what you want and if it works. But that might be a separate question :)
Option Explicit
Sub Openfile()
Dim wbOracle As Workbook
Set wbOracle = ThisWorkbook
Dim FileToOpen As Variant
FileToOpen = Application.GetOpenFilename( _
FileFilter:="Excel Workbooks (*.xls*),*.xls*", _
Title:="Open Database File")
If FileToOpen = False Then
MsgBox "No file selected, cannot continue."
Exit Sub
End If
Dim wbReference As Workbook
Set wbReference = Workbooks.Open(FileToOpen)
Workbooks.Open FileToOpen
LoopTest1 wbOracle, wbReference, "Cancel Requisition Lines"
End Sub
Sub LoopTest1(ByRef wbOracle As Workbook, _
ByRef wbReference As Workbook, _
ByVal oracleSheetName As String)
Dim wsOracle As Worksheet
Set wsOracle = wbOracle.Sheets(oracleSheetName)
Dim wsReference As Worksheet
Dim referenceCell As Range
Set wsReference = wbReference.Sheet1
Set referenceCell = wsReference.Range("C1")
Dim formulaText As String
Do While Not IsEmpty(referenceCell)
formulaText = "=IF(INDEX('[" & wbReference.Name & _
"]Sheet1'!A2000:M2000,MATCH(1,(['" & wbReference.FullName & _
"]Sheet1'!D:D=['" & wbOracle.FullName & _
"]" & wsOracle.Name & "'!C16)*('[" & wbReference.FullName & _
"]Sheet1!E:E=[" & wbOracle.FullName & _
"]" & wsOracle.Name & "'!'I16')*([" & wbReference.FullName & _
"]Sheet1!F:F=[" & wbOracle.FullName & _
"]" & wsOracle.Name & "'!'J16'),0),9)>=[" & wbOracle.FullName & _
"]" & wsOracle.Name & "'!M:M, ""materials supplied"","""")"
wsOracle.Range("C16").Formula = formulaText
Set referenceCell = ReferenceCell.Offset(1, 0)
Loop
End Sub
I am trying to find the sheet name that has a specific table name on it. For example:
Set sheetNM = ActiveWorkbook.Names("ratetable").RefersToRange.Parent.Name
Something like that, but would pull the name of the sheet, so I can activate that sheet in order to pull information from it.
This is not something I recommend but as you are referencing the ActiveWorkbook, you can drop the ActiveWorkbook and retrieve it simply as,
dim pws as worksheet, sws as string
sws = range("ratetable").parent.name
set pws = range("ratetable").parent
debug.print sws & " - " & pws.name
While a structured table (aka ListObject object) is listed in the Formulas ► Name Manager, it does not have all of the properties of a defined name. Unfortunately, everything you can do with a name you cannot always do with a ListObject as a ListObject's parent is the Worksheet object, not the workbook.
You can use error trapping to find the sheet containing a table with a given name:
Function FindTableSheet(TableName As String) As String
Dim ws As Worksheet
Dim LO As ListObject
Dim shName As String
For Each ws In Sheets
On Error Resume Next
Set LO = ws.ListObjects(TableName)
If Err.Number = 0 Then
FindTableSheet = ws.Name
Exit Function
Else
Err.Clear
End If
Next ws
FindTableSheet = "Not Found"
End Function
To test it, I named one of my sheets "Data" and added a table called "ratetable" to that sheet. I didn't, however, create any table called "table tennis". I then ran:
Sub test()
Debug.Print FindTableSheet("ratetable")
Debug.Print FindTableSheet("table tennis")
End Sub
With the output:
Data
Not Found
I know this post is old, but for what it's worth, I think the OP was on the right track (looking for the parent name) with the initial code that you originally posted. Calling the table's parent works for me:
ActiveSheet.ListObjects("TableName").Parent.Name
This question already has answers here:
Can I Get the Source Range Of Excel Clipboard Data?
(3 answers)
Closed 2 years ago.
I know about Application.CutCopyMode, but that only returns the state of the CutCopyMode (False, xlCopy, or xlCut).
How do I return the address of the currently copied range in Excel using VBA? I don't need the currently selected range (which is Application.Selection.Address). I need the address of the range of cells with the moving border (marching ants) around it.
In other words, if you select a range of cells, hit CTRL+C, and then move the selection to another cell, I need the address of the cells that were selected when the user hit CTRL+C.
Thanks!
As far as I know you can't do that with vba. You can however code your own copy sub and store the source in a global variable.
Something like this:
Option Explicit
Dim myClipboard As Range
Public Sub toClipboard(Optional source As Range = Nothing)
If source Is Nothing Then Set source = Selection
source.Copy
Set myClipboard = source
End Sub
10 years later you still can't refer directly to a copied Range
(shown by the "marching ants border" aka "dancing border", "moving border").
But you can get its address by copying the cells as link to a temporary worksheet. There you can collect the desired range's address.
Private Sub ThereAreTheMarchingAnts()
Dim rngCopied As Range ' the copied range with the marching ants border
Dim rngSelected As Range ' the selected range
Dim tmpWorksheet As Worksheet ' a temporary worksheet
Dim c As Range ' a cell for looping
' Exit, if nothing was copied (no marching ants border):
If Not (Application.CutCopyMode = xlCopy Or Application.CutCopyMode = xlCut) Then Exit Sub
' Exit, if no range is selected (just for demonstration)
If Not TypeName(Selection) = "Range" Then Exit Sub
' remember selected Range:
Set rngSelected = Selection
' add a temporary sheet and paste copied cells as link:
Set tmpWorksheet = ActiveWorkbook.Sheets.Add
tmpWorksheet.Paste link:=True
' go through all pasted cells and get the linked range from their formula:
For Each c In tmpWorksheet.UsedRange
If rngCopied Is Nothing Then
Set rngCopied = Range(Mid(c.Formula, 2))
Else
Set rngCopied = Union(rngCopied, Range(Mid(c.Formula, 2)))
End If
Next c
' delete the temporary worksheet without asking:
Application.DisplayAlerts = False
tmpWorksheet.Delete
Application.DisplayAlerts = True
' show the addresses:
MsgBox "Copied Range: " & rngCopied.Address(0, 0, xlA1, True) & vbLf & _
"Selected Range: " & rngSelected.Address(0, 0, xlA1, True)
End Sub
The code also works with multiranges and also if the copied range and the selected range are on different sheets.
When you copy a Range, the address is copied to the Clipboard along with other formats. You can check that with Clipboard Viewer application.
So if you need the copied Range, get it from Clipboard. It will be something like> $A2:$B5 or similar
The only way i can think of doing this is tracking the last range selected with a global variable and then waiting until you think a copy action is done. Unfortunately neither is easy.
The following is a quick attempt that has two problems;
If you copy the same data twice it
isn't updated
If a copy or paste is
fired from another app, the results
may vary.
This is one of those last hope tricks when tracking events that don't really exist. Hope this helps.
''# Add a reference to : FM20.dll or Microsoft Forms 2.0
''# Some more details at http://www.cpearson.com/excel/Clipboard.aspx
Option Explicit
Dim pSelSheet As String
Dim pSelRange As String
Dim gCopySheet As String
Dim gCopyRange As String
Dim gCount As Long
Dim prevCBText As String
Dim DataObj As New MSForms.DataObject
Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, _
ByVal Target As Excel.Range)
CopyTest
pSelSheet = Sh.Name
pSelRange = Target.Address
''# This is only so you can see it working
gCount = gCount + 1
application.StatusBar = gCopySheet & ":" & gCopyRange & ", Count: " & gCount
End Sub
Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Source As Range)
CopyTest ''# You may need to call CopyTest from other events as well.
''# This is only so you can see it working
gCount = gCount + 1
application.StatusBar = gCopySheet & ":" & gCopyRange & ", Count: " & gCount
End Sub
Sub CopyTest()
Dim curCBText As String
Dim r As Range
DataObj.GetFromClipboard
On Error GoTo NoCBData
curCBText = DataObj.GetText
On Error Resume Next
''# Really need to test the current cells values
''# and compare as well. If identical may have to
''# update the gCopyRange etc.
If curCBText <> prevCBText Then
gCopySheet = pSelSheet
gCopyRange = pSelRange
prevCBText = curCBText
End If
Exit Sub
NoCBData:
gCopySheet = ""
gCopyRange = ""
prevCBText = ""
End Sub
Oh and excuse the wierd comments ''# they're just there to help the syntax highlighter of SO.
I think you can use this method
https://learn.microsoft.com/en-us/office/vba/api/Excel.Application.OnKey
This method assigns a function to the hot key Ctrl+C, every time this combination is used, the function will be triggered and you can get the address of the range.