Using ArrayLists in VBA - excel

I am currently modifying a heavily moded excel schedule workbook. I have a list master list of therapists(Therapist Selector Sheet) that when a check box is checked and the magic button is pressed I want to the list to check for names in the target sheet(All Therapists) in the specified range.
If the therapist has become unchecked then the program then the program erased the necessary data for those rows and sorts everything. That part is working fine.
Next I want the program to put names that aren't already there in the next open spot. I am trying to do this by first populating an ArrayList with all the names that have a check by them.
If the program finds a duplicate I have it remove the duplicate from the array. Then when it is done removing duplicates it loops through the names and prints each one in the first cell in the column with a hypen (all empty cells are turned into hyphens).
I can't get this to work. I keep getting a
runtime error -2146233079 80131509
Does anyone see a better way to handle this? Or at least see where I am going wrong?
The first sub calls the second sub to clear and sort:
Private Sub AddDailyTherapists(PasteToRange As range, TrueFalseRange As range, StartCell As range, SortRange As range)
Call ClearUnselectedTherapists(PasteToRange, TrueFalseRange, StartCell, SortRange)
Dim Names As Object
Set Names = CreateObject("System.Collections.ArrayList")
For Each cel In TrueFalseRange
If cel.value = True Then
Names.Add cel.Parent.Cells(cel.Row, 4).value
End If
Next cel
For Each n In PasteToRange
For Each nm In Names
If nm = n.value Then
Names.Remove nm
End If
Next nm
Next n
StartCell.Activate
For Each nm In Names
Do While (ActiveCell.value <> "-")
ActiveCell.Offset(rowOffset:=1, columnOffset:=0).Activate
Loop
ActiveCell.value = nm
Next nm
End Sub
Public Sub ClearUnselectedTherapists(PasteToRange As range, TrueFalseRange As range, StartCell As range, SortRange As range)
Sheets("All Therapists").Activate
StartCell.Activate
For Each cell In TrueFalseRange
If cell.value = False Then
Name = cell.Parent.Cells(cell.Row, 4).value
For Each cel In PasteToRange
If Name = cel.value Then
cel.value = "-"
cel.Offset(0, 1).range("A1:R1").Select
Selection.ClearContents
Exit For
End If
Next cel
End If
Next cell
With ActiveWorkbook.Worksheets("All Therapists").Sort
.SetRange SortRange
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
End Sub
The modified spreadsheet:
The sheet with the checkboxes:

Private Sub AddDailyTherapists(PasteToRange As range, TrueFalseRange As range, StartCell As range, SortRange As range)
'
Call ClearUnselectedTherapists(PasteToRange, TrueFalseRange, StartCell, SortRange)
Dim Names(0 To 11) As String
i = 0
For Each cel In TrueFalseRange
If cel.value = True Then
Names(i) = cel.Parent.Cells(cel.Row, 4).value
i = i + 1
End If
Next cel
For Each n In PasteToRange
For j = 0 To UBound(Names)
If Names(j) = n.value Then
Names(j) = ""
End If
Next j
Next n
StartCell.Activate
For k = 0 To UBound(Names)
Do While (ActiveCell.value <> "-")
ActiveCell.Offset(rowOffset:=1, columnOffset:=0).Activate
Loop
If Names(k) <> "" Then
ActiveCell.value = Names(k)
End If
Next k
End Sub

Related

How to apply a condition to "used range" in whole column as a loop in excel using VBA?

I am beginner at VBA, I am stuck plz help. In this image(linked at the end of paragraph), I am trying to insert line above the cells which contains different name than the name of upper cell. Plz tell me if there is an easier way to do this or how to apply the given if else condition to whole "G" Column...
Still I am adding my code below if you don't need the image...
Sub ScanColumn()
'Application.ScreenUpdating = False
Dim varRange As Range
Dim currentCell As String
Dim upperCell As String
Dim emptyCell As String
currentCell = ActiveCell.Value
bottomCell = ActiveCell.Offset(1, 0).Value
emptyCell = ""
Dim intResult As Integer
intResult = StrComp(bottomCell, currentCell)
Dim emptyResult As Integer
emptyResult = StrComp(currentCell, emptyCell)
'I want to apply below condition to whole G column in used range
If emptyResult = 0 Then
ActiveCell.Select
ElseIf intResult = 0 Then
ActiveCell.Offset(1, 0).Select
Else
ActiveCell.Offset(1).EntireRow.Insert
ActiveCell.Offset(2, 0).Select
End If
End Sub
Here you have, just call the function "evaluateColumn" and pass the parameters, as example the "trial" sub.
Function evaluateColumn(column As String, startRow As Long, wsh As Worksheet)
Dim lastRow As Long
lastRow = wsh.Range(column & wsh.Rows.Count).End(xlUp).Row
Dim i As Long: i = startRow
Do While i < lastRow
If wsh.Cells(i, column).Value <> wsh.Cells(i + 1, column).Value And wsh.Cells(i, column).Value <> "" And wsh.Cells(i + 1, column).Value <> "" Then
wsh.Range(column & i + 1).EntireRow.Insert shift:=xlShiftDown, CopyOrigin:=xlFormatFromLeftOrAbove
i = i + 1
lastRow = lastRow + 1
End If
i = i + 1
Loop
End Function
Sub trial()
evaluateColumn "G", 2, ThisWorkbook.Worksheets("Sheet2")
End Sub
As you can see from the difference between my answer and the one below, your question isn't entirely clear. My code is an event procedure. It will run automatically, as you select a cell within the used range of column G.
If the value of the selected cell is the same as the cell below it the next row's cell will be selected.
If there is a value in either of the two cells, a blank row will be inserted and that row's cell selected. (If you want another row enable the row below the insertion.)
If either of the above conditions are true, do nothing and proceed with the selection the user made.
In order to let this code work it must be installed in the code sheet of the worksheet on which you want the action. It will not work if you install it in a standard code module, like Module1.
Option Explicit
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim TriggerRange As Range
Dim Off As Long ' offset from Target for selection
' if more than one cell is selected choose the first cell
If Target.Cells.CountLarge > 1 Then Set Target = ActiveCell
Set TriggerRange = Range(Cells(2, "G"), Cells(Rows.Count, "G").End(xlUp))
' this code will run only if a cell in this range is selected
' Debug.Print TriggerRange.Address(0, 0)
If Not Application.Intersect(Target, TriggerRange) Is Nothing Then
Application.EnableEvents = False
With Target
If .Value = .Offset(1).Value Then
Off = 1
ElseIf WorksheetFunction.CountA(.Resize(2, 1)) Then
Rows(.Row).Insert
' Off = 1 ' or -1 to change the selection
End If
.Offset(Off).Select
End With
Application.EnableEvents = True
End If
End Sub

Keep column creation to a limit in VBA

I have the following VBA code I compiled based on some stuff I found online.
It works (mostly) as I intend it to, as on change in column H it creates a new column to the right of the last column, in the active row, with the latest value.
However, I'd like, if possible, the following:
to keep only the latest 5 changes;
currently, it somehow always ignores the fact that the last column of the active row is empty and just creates a new column after so my updates always look like this, even if each of them is the first update for each row (I can expand if this wasn't clear enough).
Here's the VBA code I have now:
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Range("H:H")) Is Nothing Then
ActiveCell.Offset(-1, 0).Activate
a = Sheets("SORTIES").Cells(ActiveCell.Row, Columns.Count).End(xlToLeft).Column + 1
Column = Split(Cells(1, a).Address, "$")(1)
Sheets("SORTIES").Range(Column & ActiveCell.Row).Value = ActiveCell.Value
ActiveCell.Offset(0, 0).Select
End If
End Sub
Please, try the next code. It determines the last empty column inside the table and uses a Static dictionary variable, to keep records for all the cells in the table, for H:H column. It needs a reference to "Microsoft Scripting Runtime". To add this reference you need to be in VBE (Visual Basic for Applications Editor) and go Tools (menu) -> References..., scroll down until find "Microsoft Scripting Runtime" library, check it and press OK.
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
Static dictSt As New Dictionary 'it needs a reference to "Microsoft Scripting Runtime"
If Target.cells.count > 1 Then Exit Sub
If Target.Address = "$E$1" And Target.Value = "x" Then dictSt.RemoveAll: Target.Value = "": Exit Sub 'clear the dictionary
If Not Intersect(Target, Range("H:H")) Is Nothing Then
Dim lstO As ListObject: Set lstO = ActiveSheet.ListObjects(1) 'use your table namem if there is not only one
If dictSt.count = 0 Then 'load the dictionary for first time
'load the dictionary referenced cells
Dim arrRng As Range, i As Long, cel As Range
Set arrRng = Intersect(lstO.DataBodyRange, Range("H:H"))
For Each cel In arrRng.cells
dictSt(cel.Address) = 0
Next
End If
If Not Intersect(Target, lstO.DataBodyRange) Is Nothing Then
Dim lastEmptRng As Range
Set lastEmptRng = cells(lastListCol(Target, lstO)(0), lastListCol(Target, lstO)(1))
'operate the change in the dictionary and use the last history value after 5 records
If dictSt(Target.Address) >= 5 Then
Dim arrVal As Variant, arrUpdate As Variant, El
arrVal = Range(Target.Offset(0, 1), cells(Target.row, lastEmptRng.Column - 1)).Value
ReDim arrUpdate(1 To 1, 1 To UBound(arrVal, 2))
For i = 1 To UBound(arrVal, 2) - 1
arrUpdate(1, i) = arrVal(1, i + 1)
Next
arrUpdate(1, 5) = Target.Value
With Application
.EnableEvents = False
.Calculation = xlCalculationManual
Range(Target.Offset(0, 1), cells(Target.row, lastEmptRng.Column - 1)).Value = arrUpdate
.Calculation = xlCalculationAutomatic
.EnableEvents = True
End With
Else
lastEmptRng.Value = Target.Value
dictSt(Target.Address) = dictSt(Target.Address) + 1
End If
Target.Select
End If
End If
End Sub
Private Function lastListCol(tg As Range, lstO As ListObject) As Variant
'this piece of code search the last empty column (of the Target row) inside the table!
Dim listRow As Long, fRng As Range
listRow = tg.row - lstO.DataBodyRange.row + 1
Set fRng = lstO.DataBodyRange.rows(listRow).Find(What:="*", _
After:=lstO.DataBodyRange.cells(listRow, 1), LookIn:=xlValues, _
searchorder:=xlByColumns, SearchDirection:=xlPrevious)
If Not fRng Is Nothing Then
lastListCol = Array(tg.row, fRng.Column + 1) ' Stop
Else
lastListCol = Array(tg.row, lstO.DataBodyRange.cells(listRow, 1).Column)
End If
End Function
The dictionary can be reset by typing "x" in cell "E1"!
Edited:
Now, it keeps the last records for all recording history.

Edit (adjacent) cells with Find()

I'm writing a small macro for searching and sorting barcodes.
The idea is that barcodes are scanned into cell C1, then the macro is suppose to count the amount of times the same code is scanned. If the barcode is not already in the list (column B:B) it should add the new barcode in the list (column B:B).
I've managed utilised the Find() syntax, however I can't manage to edit any cells with it. Only thing I am able to do is MsgBox " " Ive tried:
Range("a5").Value = 5
It doesn't work
This is the code I currently have:
Private Sub Worksheet_Change(ByVal Target As Range)
If Range("C1") = "" Then MsgBox "No input"
Dim barcodes As Range
Set barcodes = Range("B:B").Find(What:=Range("C1").Value, After:=Range("B2"), LookIn:=xlFormulas, LookAt:=xlWhole, SearchOrder:=xlByColumns, SearchDirection:=xlNext, MatchCase:=True, MatchByte:=True, SearchFormat:=False)
If Not barcodes Is Nothing And Not Range("C1") = "" Then
MsgBox "Found"
End If
If barcodes Is Nothing And Not Range("C1") = "" Then
MsgBox "New product"
End If
End Sub
For MsgBox "Found" I want instead a code that counts the amount of times the same barcode has been scanned in the adjacent cell to the right.
And for Msgbox "New product" I want to write a part that adds the new code to the list in this case Column B:B
The the below will A) verify that you don't have a match (using IsError, which returns boolean) to determine if you need to add a value and start the scan count at 1, or B) if you need to find the previous entry (using Match()) and add to the counter:
If IsError(Application.Match(Cells(1,3).Value,Columns(2),0)) Then
lr = cells(rows.count,2).end(xlup).row
Cells(lr+1,2).Value = Cells(1,3).Value
Cells(lr+1,1).Value = 1
Else
r = Application.match(Cells(1,3).Value,Columns(2),0)
cells(r,1).value = cells(r,1).value + 1
End If
Edit1:
Updated column #s for second subroutine per comment from OP, while stripping out the first subroutine and rewording.
With this code you will need a sheet called "DataBase" where you will store each scan and later will be the source for example for a pivot table:
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
Dim Barcode As String, i As Long, wsDB As Worksheet, LastRow As Long
Dim DictBarcodes As New Scripting.Dictionary 'You need to check the Microsoft Scripting Runtime reference for this to work
With Application
.EnableEvents = False
.ScreenUpdating = False
.Calculation = xlCalculationManual
End With
Set wsDB = ThisWorkbook.Sheets("DataBase")
With Target
If .Range("C1") = vbNullString Then MsgBox "No input"
On Error Resume Next
'loop through all the barcodes and store them into a dictionary
For i = 1 To .Rows.Count
If .Cells(i, 2) = vbNullString Then Exit For 'skip the loop once a blank cell is found
DictBarcodes.Add .Cells(i, 1), i 'this will raise an error if there are duplicates
Next i
'If the value doesn't exist we add it to the list
If Not DictBarcodes.Exists(.Cells(1, 3)) Then
LastRow = .Cells(.Rows.Count, 2).End(xlUp).Row + 1
.Cells(LastRow, 2) = .Cells(1, 3)
End If
End With
'Either it exists or not, store it to the data base to keep tracking
With wsDB
.Cells(1, 1) = "Barcode"
.Cells(1, 2) = "Date Scan"
LastRow = .Cells(.Rows.Count, 1).End(xlUp).Row + 1
.Cells(LastRow, 1) = .Cells(1, 3)
.Cells(LastRow, 2) = Now
End With
'Finally the output on the adjacent cell
Target.Cells(1, 4) = Application.CountIf(wsDB.Range("A:A"), Target.Cells(1, 3))
With Application
.EnableEvents = True
.ScreenUpdating = True
.Calculation = xlCalculationAutomatic
End With
End Sub

Connect newly added Sheet to existing one

This is my first post in Stack Overflow so any mistake I make please just ignore.
So i made an button which runs the macro of an application inputbox, the name you enter in the inputbox will create a new sheet with the name you entered, it also will create a table on the new sheet. The name you put on the inputbox are the clients that newly came so i will have specific sheet with table for every client that comes.
On the other hand I got the Workers which will receive incomes from clients, I Got 4 Workers which have their own Sheet and Table of Incomes and Outcomes.
Now the question i am getting to is that, is it possible to creade a code on VBA that will say: If on the new sheet (inside the table, specificly: K8:K23, K28:K43, K49:K64) the name of the Worker is inserted, copy the name of the client and paste it into the existing sheet of the Worker.
The code i tried but did not work: (Only Check the First Sub and the end of line, the between code is just a bunch of macro for table to be created, that parts work, the problem of my code which is located at the end is that it does nothing, and yes I did an commend to the codes on purpose)
Sub KerkimiKlientit()
Dim EmriKlientit As String
Dim rng As Range, cel As Range
Dim OutPut As Integer
retry:
EmriKlientit = Application.InputBox("Shkruani Emrin e Klientit", "Kerkimi")
If Trim(EmriKlientit) <> "" Then
With Sheets("Hyrjet").Range("B10:B200")
Set rng = .Find(What:=EmriKlientit, _
After:=.Cells(.Cells.Count), _
LookIn:=xlValues, _
LookAt:=xlWhole, _
SearchOrder:=xlByRows, _
SearchDirection:=xlNext, _
MatchCase:=False)
If Not rng Is Nothing Then
sheet:
Flag = 0
Count = ActiveWorkbook.Worksheets.Count
For i = 1 To Count
WS_Name = ActiveWorkbook.Worksheets(i).Name
If WS_Name = EmriKlientit Then Flag = 1
Next i
If Flag = 1 Then
ActiveWorkbook.Sheets(EmriKlientit).Activate
Exit Sub
Else
Sheets.Add(, Sheets(Sheets.Count)).Name = EmriKlientit
Call KrijimiTabeles(EmriKlientit)
Exit Sub
End If
Else
OutPut = MsgBox("Klienti nuk u gjet", vbRetryCancel + vbInformation, "Provoni Perseri")
If (OutPut = vbRetry) Then
GoTo retry:
ElseIf (OutPut = vbCancel) Then
Exit Sub
End If
Exit Sub
End If
End With
End If
If userInputValue = "" Then
OutPut = MsgBox("Rubrika e Emrit e zbrazet", vbRetryCancel + vbExclamation, "Gabim")
If (OutPut = vbRetry) Then
GoTo retry:
ElseIf (OutPut = vbCancel) Then
Exit Sub
End If
Else
GoTo retry:
End If
End Sub
Sub KrijimiTabeles(EmriKlientit As String)
'
' KrijimiTabeles Macro
'
'This was just an middle code, it was too long so I did not paste it. Not an important part tho.
'This is the part that does not work, it just does nothing for some reason, there are multiple codes here and I tried them all.
'Sub Formula(EmriKlientit As String, ByVal Target As Range)
'ActiveWorkbook.Sheets(EmriKlientit).Activate
'If Not Application.Intersect(Range("K8:K23"), Range(Target.Adress)) Is Nothing Then
'Call Formula1
'End If
'End Sub
'Dim LR As Long, i As Long
'Application.ScreenUpdating = False
'Dim Rng As Range
'For Each Rng In Range("K8:K23")
'Select Case Rng.Value
'Case "M"
'Worksheets(EmriKlientit).Range("K2").Copy
'Worksheets("Mustafa").Range("K6").PasteSpecial Paste:=xlPasteFormulas
'End Select
'Next Rng
'Application.ScreenUpdating = True
'For Each cel In Rng
'If cel.Value = "M" Then
'Worksheets(EmriKlientit).Range("K2").Copy
'Worksheets("Mustafa").Range("K6").PasteSpecial Paste:=xlPasteFormulas
'End If
'Next cel
'ActiveWorkbook.Sheets(EmriKlientit).Activate
'If Not Application.Intersect(Range("K8:K23"), Range(Rng.Adress)) Is Nothing Then
'With Sheets(EmriKlientit)
'With .Range("K8:K23")
'If .Text = "M" Then
'Worksheets(EmriKlientit).Range("K2").Copy
'Worksheets("Mustafa").Range("K6").PasteSpecial Paste:=xlPasteFormulas
'End If
'End With
'End With
'End If
'Flag = 0
'Count = ActiveWorkbook.Worksheets.Count
'For i = 1 To Count
'WS_Name = ActiveWorkbook.Worksheets(i).Name
'If WS_Name = EmriKlientit Then Flag = 1
'Next i
'If Flag = 1 Then
'ActiveWorkbook.Sheets(EmriKlientit).Activate
'For Each Cell In Sheets(EmriKllientit).Range("K8:K23")
'If Cell.Value = "M" Then
'Range("K2").Copy
'Worksheets("Mustafa").Range("K6").PasteSpecial Paste:=xlPasteFormulas
'End If
'Next
'End If
End Sub
Thank you
I hope I was clear enough,
Any help would be appreciated.
Welcome to StackOverflow - i agree that your question can be a bit more specific...
I think what you are trying to achieve is something between this lines:
Dim wsClient As Worksheet, wsMustafa As Worksheet
Dim i As Long
Dim fRow As Long, lRow As Long
Set wsClient = ActiveWorkbook.Sheets("Client")
Set wsMustafa= ActiveWorkbook.Sheets("Mustafa")
'you can assign this through better ways, but to start with...
fRow = 8
lRow = 23
For i = fRow To lRow
If wsClient.Range("K" & i).Value = "M" Then
wsMustafa.Range("K6").Value = wsClient.Range("K" & i).Value 'or .Formula if that's what you want
End If
Next i
Hope this helps, good luck.

How to run VBA code when cell contents are changed via formula

The code below works fine when I manually update column I. What I need is to know if there is a way to still have this code work when I have column I updated by a formula.
Private Sub Worksheet_Change(ByVal Target As Excel.Range)
With Target
If .Count > 1 Then Exit Sub
If Not Intersect(Range("I3:I30"), .Cells) Is Nothing Then
Application.EnableEvents = False
If IsEmpty(.Value) Then
.Offset(0, -1).ClearContents
Else
With .Offset(0, -1)
.NumberFormat = "m/d/yy h:mm"
.Value = Now
End With
End If
Application.EnableEvents = True
End If
End With
End Sub
Worksheet_Change does not fire in responce to a formula update.
See Excel help for Worksheet_Change
Occurs when cells on the worksheet are changed by the user or by an external link.
You could maybe achieve what you want with the Worksheet_Calculate event.
Assuming you want to put a time stamp next to the cells when those vall values change, try this (in addition to your Change event).
Note the use of the Static variable to track previous values, since Calculate event does nopt provide a Target parameter like Change does. This method may not be robust enough since Static's get reset if you break vba execution (eg on an unhandled error). If you want it more robust, consider saving previous values on another (hidden) sheet.
Private Sub Worksheet_Calculate()
Dim rng As Range, cl As Range
Static OldData As Variant
Application.EnableEvents = False
Set rng = Me.Range("I3:I30")
If IsEmpty(OldData) Then
OldData = rng.Value
End If
For Each cl In rng.Cells
If Len(cl) = 0 Then
cl.Offset(0, -1).ClearContents
Else
If cl.Value <> OldData(cl.Row - rng.Row + 1, 1) Then
With cl.Offset(0, -1)
.NumberFormat = "m/d/yy h:mm:ss"
.Value = Now
End With
End If
End If
Next
OldData = rng.Value
Application.EnableEvents = True
End Sub
Update
Tested routine on sample sheet, all works as expected
Sample file contains the same code repeated on 25 sheets, and range to time stamp is 10000 rows long.
To avoid repeating the code, use the Workbook_ events. To minimise run time use variant arrays for the loop.
Private Sub Workbook_SheetCalculate(ByVal Sh As Object)
Dim rng As Range
Dim NewData As Variant
Dim i As Long
Static OldData As Variant
Application.EnableEvents = False
Set rng = Sh.Range("B2:C10000") ' <-- notice range includes date column
NewData = rng
If IsEmpty(OldData) Then
OldData = rng.Value
End If
For i = LBound(NewData, 1) To UBound(NewData, 1)
If Len(NewData(i, 1)) = 0 And Len(NewData(i, 2)) > 0 Then
rng.Cells(i, 2).ClearContents
Else
If NewData(i, 1) <> OldData(i, 1) Then
With rng.Cells(i, 2)
.NumberFormat = "m/d/yy -- h:mm:ss"
.Value = Now
End With
End If
End If
Next
OldData = rng.Value
Application.EnableEvents = True
End Sub
Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
'Activate date population on cell change
With Target
If .Count > 1 Then Exit Sub
If Not Intersect(Sh.Range("B2:B10000"), .Cells) Is Nothing Then
Application.EnableEvents = False
If IsEmpty(.Value) Then
.Offset(0, 1).ClearContents
Else
'Populate date and time in column c
With .Offset(0, 1)
.NumberFormat = "mm/dd/yyyy -- hh:mm:ss"
.Value = Now
End With
End If
Application.EnableEvents = True
End If
End With
End Sub

Resources