Excel VBA Resize Table Copying Formulas & Shapes - excel

I have a table containing formulas and shapes on every row.
I want to resize the table based on a user's input from userform (Lets just call the value given TextBox1.Value) The user inputs a new desired table row size into the userform and clicks "OK"
Let's call the Table Table1, see code below:
Private Sub UserForm_Initialize()
Dim ob As ListObject
Dim count As Integer
Set ob = Sheets("Worksheet").ListObjects("Table1")
count = ob.Range.Rows.count - 1
TextBox1.value = count
End Sub
Private Sub OKButton_Click()
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Application.EnableEvents = False
Dim ob As ListObject
Dim count As Integer, i As Integer, j As Integer
Set ob = Sheets("Worksheet").ListObjects("Table1")
count = ob.Range.Rows.count - 1
If TextBox1.value < 2 Then
Unload Me
ElseIf TextBox1.value > count Then
ob.Resize ob.Range.Resize(TextBox1.value + 1)
ob.ListRows(count).Range.Select
Selection.AutoFill Destination:=ob.ListRows(count & ":" &_
TextBox1.value).Range,Type:=xlFillDefault
ob.ListRows(TextBox1).Range.Select
ElseIf TextBox1.value < count Then
ob.Range.Rows(TextBox1.value + 1 & ":" & count).Delete
End If
Application.CutCopyMode = False
Unload Me
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
Application.EnableEvents = True
End Sub
My issue is when the user inputs a value greater than the table's current row count.
The table resizes correctly, but there is an error when copying the rows.
"Run Time Error 9, Subscript out of Range"
The desire is to copy down the formulas and shapes to the newly created rows quickly.
Can anyone see what I'm doing wrong?

You can't reference multiple ListRows like ListRows(1:2) like you can with worksheet rows. That property doesn't support that argument syntax. Change that ElseIf to
ElseIf TextBox1.Value > count Then
ob.Resize ob.Range.Resize(TextBox1.Value + 1)
ob.ListRows(count).Range.Resize(Me.TextBox1.Value - count + 1).FillDown
and you will avoid that error.

Here is the result working correctly for anyone who may have use for it:
Private Sub UserForm_Initialize()
Dim ob As ListObject
Dim count As Integer, i As Integer, j As Integer
Set ob = Sheets("Worksheet").ListObjects("Table1")
count = ob.Range.Rows.count - 1
TextBox1.value = count
End Sub
Private Sub OKButton_Click()
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Application.EnableEvents = False
Dim ob As ListObject
Dim count As Integer, i As Integer, j As Integer, k As Integer, m As Integer
Set ob = Sheets("Worksheet").ListObjects("Table1")
count = ob.Range.Rows.count - 1
If TextBox1.value < 2 Then
Unload Me
ElseIf TextBox1.value > count Then
ob.Resize ob.Range.Resize(TextBox1.value + 1)
ob.ListRows(count).Range.Resize(Me.TextBox1.value - count + 1).FillDown
ElseIf TextBox1.value < count Then
Debug.Print "TextBox1:" & TextBox1.value & " count:" & count
ob.Range.Rows(TextBox1.value + 2 & ":" & count + 1).Delete
End If
Application.CutCopyMode = False
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
Application.EnableEvents = True
ActiveSheet.UsedRange
Unload Me
End Sub

Related

Dynamically update the count of selected CheckBox in Excel using VBA

I am trying to find out a way to update the count of the selected checkboxes in excel using VBA.
i.e as the user selects the checkbox, the count has to get updated across the relevant filed. For example, If I select first check box ABC/18-49. The count at the top for (18-49) should get updated to 3.
P.S: This is how I have created the checkboxes dynamically.
Sub Main()
Dim Rng As Range
Dim WorkRng As Range
Dim Ws As Worksheet
On Error Resume Next
Set Ws = ThisWorkbook.Sheets(1)
Ws.Range("A:A").Insert
Set WorkRng = Ws.Range("A2:A" & Ws.UsedRange.Rows.Count)
Application.ScreenUpdating = False
For Each Rng In WorkRng
With Ws.CheckBoxes.Add(Rng.Left, Rng.Top, Rng.Width, Rng.Height)
.Characters.Text = "Yes"
End With
Next
WorkRng.ClearContents
WorkRng.Select
Application.ScreenUpdating = True
End Sub
Try the next way, please:
Copy the next Subs in a standard module and run the first one. It will assign a specific macro to all check boxes from column A:A:
Sub AssingMacro()
Dim sh As Worksheet, s As Shape, chkB As CheckBox
Set sh = ActiveSheet
For Each s In sh.Shapes
If left(s.Name, 6) = "Check " And s.TopLeftCell.Column = 1 Then
s.OnAction = "CheckBoxesHeaven"
End If
Next
End Sub
Sub CheckBoxesHeaven()
Dim sh As Worksheet, chB As CheckBox
Set sh = ActiveSheet
Set chB = sh.CheckBoxes(Application.Caller)
If chB.Value = 1 Then
Debug.Print chB.TopLeftCell.Offset(0, 2).Value
If chB.TopLeftCell.Offset(0, 2).Value = "18-49" Then
sh.Range("C3").Value = sh.Range("C3").Value + 1
ElseIf chB.TopLeftCell.Offset(0, 2).Value = "50-64" Then
sh.Range("C1").Value = sh.Range("C1").Value + 1
Else
sh.Range("C2").Value = sh.Range("C2").Value + 1
End If
Else
If chB.TopLeftCell.Offset(0, 2).Value = "18-49" Then
sh.Range("C3").Value = sh.Range("C3").Value - 1
ElseIf chB.TopLeftCell.Offset(0, 2).Value = "50-64" Then
sh.Range("C1").Value = sh.Range("C1").Value - 1
Else
sh.Range("C2").Value = sh.Range("C2").Value - 1
End If
End If
End Sub
Assort the values in range "C1:C3" to match the appropriate check boxes value. In order to automatically do that, please use the next code:
Sub ResetCheckBoxesValues()
Dim sh As Worksheet, chkB As CheckBox, i As Long
Dim V50_64 As Long, V18_49 As Long, VLess18 As Long
Set sh = ActiveSheet
For Each chkB In sh.CheckBoxes
If chkB.TopLeftCell.Column = 1 Then
Select Case chkB.TopLeftCell.Offset(0, 2).Value
Case "50-64"
If chkB.Value = 1 Then V50_64 = V50_64 + 1
Case "18-49":
If chkB.Value = 1 Then V18_49 = V18_49 + 1
Case "<18":
If chkB.Value = 1 Then VLess18 = VLess18 + 1
End Select
End If
Next
sh.Range("C1:C3").Value = Application.Transpose(Array(V50_64, VLess18, V18_49))
End Sub
Start playing with check boxes selection. It will add a unit to the appropriate cell if checking and decrease it with a unit in case of unchecking.
Please, test it and send some feedback
It will not be "very" dynamic, make sure to click on a random Excel cell, to make the formula recalculate after updating the check on the checkbox.
But the formula works in Excel, with the checkboxes you have created:
Public Function CountCheckBoxes()
Dim chkBox As Shape
Dim counter As Long
With ThisWorkbook.Worksheets(1)
For Each chkBox In .Shapes
If InStr(1, chkBox.Name, "Check Box") Then
If .Shapes(chkBox.Name).OLEFormat.Object.Value = 1 Then
counter = counter + 1
End If
End If
Next chkBox
End With
CountCheckBoxes = counter
End Function
Probably you should think about a suitable workaround to avoid ThisWorkbook.Worksheets(1), depending on where the code is residing.

Is there away to have cell value add or delete copies of either cell ranges or worksheets without use of a button

I'm trying to make a cell value dictate how many copies should be made. I'm trying to make it if the cell value goes down that it will delete the sheets that are higher than the value. I currently have the adding working no problem just can't figure out how to make it delete copies when the value gets smaller. I figure I could make a button do a check just trying to make it more automated.
Sub CreateDistro()
Dim i As Long
Dim Num As Integer
Dim Name As String
Dim xActiveSheet As Worksheet
On Error Resume Next
Application.ScreenUpdating = False
Set ActiveSheet = ActiveSheet
Num = Range("C1")
If Num > 1 Then
For i = 1 To Num
Name = ActiveSheet.Name
xActiveSheet.Copy After:=ActiveWorkbook.Sheets(Name)
ActiveSheet.Name = "Distro-" & i
Next
End If
xActiveSheet.Activate
Application.ScreenUpdating = True
End Sub
Problem of the code below: it reacts on the Range("C1") of all sheets!
You might want to use a named range or limit the number of possible sheets(e.g. minimum number of sheets = 2, template to be copied is sheet 2,only sheet1 has the Worksheet_Change code.
Sheet1:
Option Explicit
Private Sub Worksheet_Change(ByVal target As Range)
Call ChangeSheets(target)
End Sub
Module1:
Option Explicit
Sub ChangeSheets(ByVal target As Range)
Dim iCt As Integer
Dim Num As Integer
Dim maxSh As Integer
'If Not Intersect(Target, Range("C1")) Is Nothing Then
' MsgBox ("C1: " & Target.Value)
'End If
If target.Value <= 0 Then
MsgBox "Minimum worksheet count = 1!" & vbCrLf & "Nothing to do!"
Application.EnableEvents = False
target.Value = 1
Application.EnableEvents = True
Application.DisplayAlerts = False
maxSh = Sheets.Count
For iCt = maxSh To 2 Step -1
Sheets(iCt).Delete
Next iCt
Application.DisplayAlerts = True
Exit Sub
End If
If Worksheets.Count = target.Value Then
MsgBox "Worksheet count = " & target.Value & vbCrLf & "Nothing to do!"
Exit Sub
End If
'add some sheets
If Worksheets.Count < target.Value Then
Num = target.Value - Worksheets.Count
For iCt = 1 To Num
ActiveSheet.Copy After:=Sheets(Sheets.Count)
Next iCt
Exit Sub
End If
'delete some sheets
If Worksheets.Count > target.Value Then
Num = Worksheets.Count - target.Value
Application.DisplayAlerts = False
maxSh = Sheets.Count
For iCt = 0 To Num - 1
Debug.Print maxSh - iCt; ": "; Sheets(maxSh - iCt).Name
Sheets(maxSh - iCt).Delete
Next iCt
Application.DisplayAlerts = True
Exit Sub
End If
End Sub

How to continue the sequence of the unique numbers in the excel sheet after closing the userform?

I am facing a problem in getting the sequence of the unique numbers(Serial number) when the userform is closed and opened later on. Firstly, when I fill the data in the userform everything is captured in the excel sheet perfectly with correct sequence; if I close the userform and run the code by filling the userform with new data the unique ID's are again starting from "1" but not according to the excel sheet row number which was previously saved.
Below is the code I tried:
Private Sub cmdSubmit_Click()
Dim WB As Workbook
Dim lr As Long
Set WB = Workbooks.Open("C:\Users\Desktop\Book2.xlsx")
Dim Database As Worksheet
Set Database = WB.Worksheets("Sheet1")
eRow = Database.Cells(Rows.Count, 1).End(xlUp).Offset(1, 0).Row
lr = Database.Range("a65536").End(xlUp).Row
With Sheets("Sheet1")
If IsEmpty(.Range("A1")) Then
.Range("A1").Value = 0
Else
Database.Cells(lr + 1, 1) = Val(Database.Cells(lr, 1)) + 1
End If
End With
Database.Cells(eRow, 4).Value = cmbls.Text
Database.Cells(eRow, 2).Value = txtProject.Text
Database.Cells(eRow, 3).Value = txtEovia.Text
Database.Cells(eRow, 1).Value = txtUid.Text
Call UserForm_Initialize
WB.SaveAs ("C:\Users\Desktop\Book2.xlsx")
End Sub
Private Sub Worksheet_Change(ByVal Target As Range)
Dim maxNumber
If Not Intersect(Target, Range("B:B")) Is Nothing Then
' don't run when more than one row is changed
If Target.Rows.Count > 1 Then Exit Sub
' if column A in the current row has a value, don't run
If Cells(Target.Row, 1) > 0 Then Exit Sub
' get the highest number in column A, then add 1 and write to the
' current row, column A
maxNumber = Application.WorksheetFunction.Max(Range("A:A"))
Target.Offset(0, -1) = maxNumber + 1
End If
End Sub
Private Sub UserForm_Initialize()
With txtUid
.Value = Format(Val(Cells(Rows.Count, 1).End(xlUp)) + 1, "0000")
.Enabled = False
End With
With txtProject
.Value = ""
.SetFocus
End With
End Sub
In this image if you see unique id's are repeating 1 and 2, but I need as 1,2,3,4....
I think this is where the issue is coming from. You need to re-calculate the last row every time the user form is Initialized.
Private Sub UserForm_Initialize()
Dim ws as Worksheet: Set ws = Thisworkbook.Sheets("Database")
With txtUid
.Value = Format(ws.Range("A" & ws.Rows.Count).End(xlUp) + 1, "0000")
.Enabled = False
End With
With txtProject
.Value = ""
.SetFocus
End With
End Sub
It's always risky to use row numbers or [max range value +1] as a sequence number.
Safer to use something like a name scoped to the worksheet, which has a value you can increment. Then the sequence is independent of your data.
E.g.
Function GetNextSequence(sht As Worksheet) As Long
Const SEQ_NAME As String = "SEQ"
Dim nm As Name, rv As Long
On Error Resume Next
Set nm = sht.Names(SEQ_NAME)
On Error GoTo 0
'add the name if it doesn't exist
If nm Is Nothing Then
Set nm = sht.Names.Add(Name:=SEQ_NAME, RefersToR1C1:="=0")
End If
rv = Evaluate(nm.Value) + 1
nm.Value = rv
GetNextSequence = rv
End Function

How to allow multiple successive undos in excel vba?

I have an excel workbook that needs to allow the user to undo multiple changes within a worksheet. I have searched online in every forum that I can think of and have not been able to find an answer for this. I realize that there is an issue with the undo issue in excel when macro's are run, and have been able to handle this using code derived from here.
This is my current process:
Create global variables to hold the initial state of the workbook, and the changes. Code is as follows:
Private Type SaveRange
Val As Variant
Addr As String
End Type
Private OldWorkbook As Workbook
Private OldSheet As Worksheet
Private OldSelection() As SaveRange
Private OldSelectionCount As Integer
Private InitialState() As SaveRange
Private InitialStateCount As Integer
Get the initial state of the workbook by building an array (InitialState) holding the values of all the cells in the Workbook_Open sub. Code is as follows:
Private Sub Workbook_Open()
GetInitialCellState
End Sub
Private Sub GetInitialCellState()
Dim i As Integer, j As Integer, count As Integer
Dim cellVal As String
Dim sampID As Range, cell As Range
Dim e1664 As Workbook
Dim rawData As Worksheet
Dim table As Range
Dim LastRow As Integer, LastCol As Integer
LastRow = Worksheets("Raw_Data").Range("A65536").End(xlUp).Row
LastCol = Worksheets("Raw_Data").UsedRange.Columns.count
Set e1664 = ThisWorkbook
Set rawData = e1664.Sheets("Raw_Data")
Set sampID = rawData.Range("SAMPLEID").Offset(1)
Set table = rawData.Range(sampID, "R" & LastRow)
i = 0
j = 0
count = 0
ReDim InitialState(i)
For i = 0 To (LastRow - sampID.Row)
For j = 0 To LastCol
ReDim Preserve InitialState(count)
InitialState(count).Addr = sampID.Offset(i, j).address
InitialState(count).Val = sampID.Offset(i, j).Value
count = count + 1
Next j
Next i
InitialStateCount = count - 1
End Sub
When a value is entered into a cell, store the value entered into another array (OldSelection) holding the value entered. This is done in the Workbook_Change sub. The important parts here are the Call SaveState(OldSelectionCount, Target.Cells.address, Target.Cells.Value) and Application.OnUndo "Undo the last action", "GI.OR.E1664.20150915_DRAFT.xlt!Sheet1.RevertState" pieces which are shown in numbers 4 and 5 below. Code is as follows:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim cell As Range, InWtRange As Boolean
Dim y As Integer, x As Integer, count As Integer
Dim LastRow As Integer
'This saves the changed values of the cells
Call SaveState(OldSelectionCount, Target.Cells.address, Target.Cells.Value)
try:
y = Me.Range("SampleID").Row
If Target.Column > 5 And Target.Column < 8 Then
If Range("A" & Target.Row).Value = Range("A" & Target.Row + 1).Value Then
If Range("A" & Target.Row + 1).Value <> "" Then
Range(Target.address).Offset(1).Value = Range(Target.address).Value
End If
End If
Else
'If initial pan weight add start date
If Target.Column = 8 Then
If Target.Cells.Text <> "" Then
If Not IsNumeric(Target.Cells.Value) Then
GoTo Finally
Else
Application.EnableEvents = False
Range("StartDate").Offset(Target.Cells.Row - y).Value = Format(Now(), "MM/DD/YY HH:NN:SS")
Application.EnableEvents = True
End If
Else
Application.EnableEvents = False
Range("StartDate").Offset(Target.Cells.Row - y).Value = ""
Application.EnableEvents = True
End If
End If
End If
LastRow = Worksheets("Raw_Data").Range("A65536").End(xlUp).Row
For Each cell In Target.Cells
'Debug.Print Target.Cells.Address
If cell.Value <> "" Then
If Not IsNumeric(cell.Value) Then GoTo Finally
Select Case cell.Column
Case 9, 11, 13
Application.EnableEvents = False
If CalcHEM(cell.Row - y, cell.Column) Then
End If
Application.EnableEvents = True
Case Else
'Do nothing yet
End Select
'Cells(Target.Row + 1, Target.Column).Select
End If
Next
'This will allow the changed values to be undone
Application.OnUndo "Undo the last action", "GI.OR.E1664.20150915_DRAFT.xlt!Sheet1.RevertState"
Finally:
If Application.EnableEvents = False Then Application.EnableEvents = True
Exit Sub
Catch:
MsgBox "An error has occurred in the code execution." & vbNewLine _
& "The message text of the error is: " & Error(Err), vbInformation, "TSSCalcs.AddQC"
Resume Finally
End Sub
The SaveState Sub will save add to the OldSelection array, any values that have changed. Code is as follows:
Private Sub SaveState(count As Integer, Addr As String, Val As Double)
Dim i As Integer
Dim cell As Range
If TypeName(Selection) <> "Range" Or Selection.count > 1 Then Exit Sub
ReDim Preserve OldSelection(count)
Set OldWorkbook = ActiveWorkbook
Set OldSheet = ActiveSheet
For Each cell In Selection
OldSelection(count).Addr = Addr
OldSelection(count).Val = Val
Next cell
OldSelectionCount = OldSelectionCount + 1
End Sub
The RevertState Sub will undo ONLY THE LAST ACTION! I am unable to allow more than the last entry to be undone. Code is as follows:
Private Sub RevertState()
Dim i As Integer, index As Integer
Dim prevItem As SaveRange
Dim address As String
OldWorkbook.Activate
OldSheet.Activate
Application.EnableEvents = False
address = OldSelection(OldSelectionCount - 1).Addr
OldSelectionCount = OldSelectionCount - 2
If OldSelectionCount <= 0 Then
ReDim OldSelection(0)
For i = 0 To InitialStateCount
If InitialState(i).Addr = address Then
prevItem.Val = InitialState(i).Val
index = i
End If
Next i
Range(InitialState(index).Addr).Formula = prevItem.Val
Else
ReDim Preserve OldSelection(OldSelectionCount)
For i = 0 To OldSelectionCount
If OldSelection(i).Addr = address Then
prevItem.Val = OldSelection(i).Val
index = i
End If
Next i
'OldSelectionCount = OldSelectionCount + 1
Range(OldSelection(index).Addr).Formula = prevItem.Val
End If
OldSelectionCount = OldSelectionCount + 1
Application.EnableEvents = True
End Sub
Does anyone know of a way to allow multiple undo's to be done?
Any help to solve this issue would be greatly appreciated!
After researching the Undo function on MSDN here, I found that the Application.Undo function only undoes the last action taken by the user. Instead of trying to get Microsoft's undo functionality to work, I have added my own undo and redo buttons which function the same as Microsoft's buttons. I have added two class modules: ActionState (holds the properties for workbook, worksheet, address and value of a cell)
ActionStates (a collection ActionState objects along with functions for adding, removing, getting an item, clearing the collection, counting, and properties for the CurrentState, and InitialState of the worksheet).
The new process is as follows:
Get the initial state of all the cells in the worksheet and add these to the undo stack array (see GetInitialCellStates() method within UndoFuntionality module).
When an item is added to a cell, add the address and value to the array (see SaveState() method within UndoFunctionality module) and update the index of the current state to the most recently added value. Repeat this step with any additional values.
When this is done, it enables the undo button.
If the undo button is pressed, it will decrement the index of the current state and enable the redo button (see RevertState() function within UndoFunctionality module).
If the redo button is pressed it will increment the index of the current state (see ProgressState() function within UndoFunctionality module).
The code for the ActionState class is as follows:
Private asAddr As String
Private asVal As Variant
Private asWorkbook As Workbook
Private asWorksheet As Worksheet
Private Sub Class_Initalize()
Set asWorkbook = New Workbook
Set asWorksheet = New Worksheet
End Sub
'''''''''''''''''''
' Addr property
'''''''''''''''''''
Public Property Get Addr() As String
Addr = asAddr
End Property
Public Property Let Addr(Value As String)
asAddr = Value
End Property
'''''''''''''''''''
' Val property
'''''''''''''''''''
Public Property Get Val() As Variant
Val = asVal
End Property
Public Property Let Val(Value As Variant)
asVal = Value
End Property
'''''''''''''''''''
' Wkbook property
'''''''''''''''''''
Public Property Get Wkbook() As Workbook
Set Wkbook = asWorkbook
End Property
Public Property Let Wkbook(Value As Workbook)
Set asWorkbook = Value
End Property
'''''''''''''''''''
' WkSheet property
'''''''''''''''''''
Public Property Get Wksheet() As Worksheet
Set Wksheet = asWorksheet
End Property
Public Property Let Wksheet(Value As Worksheet)
Set asWorksheet = Value
End Property
The code for the ActionStates class is as follows:
Private asStates As Collection
Private currState As Integer
Private initState As Integer
Private Sub Class_Initialize()
Set asStates = New Collection
End Sub
Private Sub Class_Termitate()
Set asStates = Nothing
End Sub
''''''''''''''''''''''''''''
' InitialState property
''''''''''''''''''''''''''''
Public Property Get InitialState() As Integer
InitialState = initState
End Property
Public Property Let InitialState(Value As Integer)
initState = Value
End Property
''''''''''''''''''''''''''''
' CurrentState property
''''''''''''''''''''''''''''
Public Property Get CurrentState() As Integer
CurrentState = currState
End Property
Public Property Let CurrentState(Value As Integer)
currState = Value
End Property
''''''''''''''''''''''''''''
' Add method
''''''''''''''''''''''''''''
Public Function Add(Addr As String, Val As Variant) As clsActionState
Dim asNew As New clsActionState
With asNew
.Addr = Addr
.Val = Val
End With
asStates.Add asNew
End Function
''''''''''''''''''''''''''''
' Count method
''''''''''''''''''''''''''''
Public Property Get count() As Long
If TypeName(asStates) = "Nothing" Then
Set asStates = New Collection
End If
count = asStates.count
End Property
''''''''''''''''''''''''''''
' Item method
''''''''''''''''''''''''''''
Public Function Item(index As Integer) As clsActionState
Set Item = asStates.Item(index)
End Function
''''''''''''''''''''''''''''
' Remove method
''''''''''''''''''''''''''''
Public Function Remove(index As Integer)
If TypeName(asStates) = "Nothing" Then
Set asStates = New Collection
End If
asStates.Remove (index)
End Function
''''''''''''''''''''''''''''
' Clear method
''''''''''''''''''''''''''''
Public Sub Clear()
Dim x As Integer
For x = 1 To asStates.count
asStates.Remove (1)
Next x
End Sub
These two classes are used in a new module called UndoFunctionality as follows:
Option Explicit
Public ActionState As New clsActionState
Public ActionStates As New clsActionStates
Public undoChange As Boolean
Public Sub SaveState(count As Integer, Addr As String, Val As Variant)
Dim i As Integer
Dim cell As Range
If TypeName(Selection) <> "Range" Or Selection.count > 1 Then Exit Sub
With ActionState
.Wkbook = ActiveWorkbook
.Wksheet = ActiveSheet
End With
If ActionStates.CurrentState < ActionStates.count Then
For i = ActionStates.CurrentState + 1 To ActionStates.count
ActionStates.Remove (ActionStates.count)
Next i
End If
For Each cell In Selection
ActionState.Addr = Addr
ActionState.Val = Val
Next cell
ActionStates.Add ActionState.Addr, ActionState.Val
ActionStates.CurrentState = ActionStates.count
End Sub
Public Sub RevertState()
Dim i As Integer, index As Integer
Dim prevItem As New clsActionState
Dim Address As String
'undoChange = True
With ActionState
.Wkbook.Activate
.Wksheet.Activate
End With
Application.EnableEvents = False
Address = ActionStates.Item(ActionStates.CurrentState).Addr
ActionStates.CurrentState = ActionStates.CurrentState - 1
For i = 1 To ActionStates.CurrentState
If ActionStates.Item(i).Addr = Address Then
prevItem.Val = ActionStates.Item(i).Val
index = i
End If
Next i
Range(ActionStates.Item(index).Addr).Formula = prevItem.Val
Application.EnableEvents = True
UndoButtonAvailability
RedoButtonAvailability
End Sub
Public Sub ProgressState()
Dim i As Integer, index As Integer
Dim nextItem As New clsActionState
Dim Address As String
With ActionState
.Wkbook.Activate
.Wksheet.Activate
End With
Application.EnableEvents = False
ActionStates.CurrentState = ActionStates.CurrentState + 1
With nextItem
.Addr = ActionStates.Item(ActionStates.CurrentState).Addr
.Val = ActionStates.Item(ActionStates.CurrentState).Val
End With
Range(ActionStates.Item(ActionStates.CurrentState).Addr).Formula = nextItem.Val
Application.EnableEvents = True
UndoButtonAvailability
RedoButtonAvailability
End Sub
Public Sub GetInitialCellStates()
Dim i As Integer, j As Integer, count As Integer
Dim cellVal As String
Dim sampID As Range, cell As Range
Dim e1664 As Workbook
Dim rawData As Worksheet
Dim table As Range
Dim LastRow As Integer, LastCol As Integer
ThisWorkbook.Worksheets("Raw_Data").Activate
If ActionStates.count > 0 Then
ActionStates.Clear
End If
LastRow = Worksheets("Raw_Data").Range("A65536").End(xlUp).Row
LastCol = Worksheets("Raw_Data").UsedRange.Columns.count
Set e1664 = ThisWorkbook
Set rawData = e1664.Sheets("Raw_Data")
Set sampID = rawData.Range("SAMPLEID").Offset(1)
Set table = rawData.Range(sampID, "R" & LastRow)
i = 0
j = 0
count = 0
For i = 0 To (LastRow - sampID.Row)
For j = 0 To LastCol
ActionState.Addr = sampID.Offset(i, j).Address
ActionState.Val = sampID.Offset(i, j).Value
ActionStates.Add ActionState.Addr, ActionState.Val
count = count + 1
Next j
Next i
ActionStates.InitialState = count
ActionStates.CurrentState = count
undoChange = False
UndoButtonAvailability
RedoButtonAvailability
End Sub
Public Sub UndoButtonAvailability()
Dim rawData As Worksheet
Set rawData = ThisWorkbook.Sheets("Raw_Data")
If ActionStates.CurrentState <= ActionStates.InitialState Then
rawData.Buttons("UndoButton").Enabled = False
rawData.Buttons("UndoButton").Font.ColorIndex = 16
Else
rawData.Buttons("UndoButton").Enabled = True
rawData.Buttons("UndoButton").Font.ColorIndex = 1
End If
End Sub
Public Sub RedoButtonAvailability()
Dim rawData As Worksheet
Set rawData = ThisWorkbook.Sheets("Raw_Data")
If ActionStates.CurrentState < ActionStates.count Then
rawData.Buttons("RedoButton").Enabled = True
rawData.Buttons("RedoButton").Font.ColorIndex = 1
Else
rawData.Buttons("RedoButton").Enabled = False
rawData.Buttons("RedoButton").Font.ColorIndex = 16
End If
End Sub
Sub UndoButton_Click()
Dim rawData As Worksheet
Set rawData = ThisWorkbook.Sheets("Raw_Data")
If rawData.Buttons("UndoButton").Enabled Then
RevertState
End If
End Sub
Sub RedoButton_Click()
Dim rawData As Worksheet
Set rawData = ThisWorkbook.Sheets("Raw_Data")
If rawData.Buttons("RedoButton").Enabled Then
ProgressState
End If
End Sub
The GetInitialStates method is used in the workbook_open event as follows:
UndoFunctionality.GetInitialCellStates
And the Worksheet_Change event within the worksheet is as follows:
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
Dim cell As Range, InWtRange As Boolean
Dim y As Integer, x As Integer, count As Integer
Dim LastRow As Integer
'This saves the changed values of the cells
Call SaveState(ActionStates.CurrentState, Target.Cells.Address, Target.Cells.Value)
try:
y = Me.Range("SampleID").Row
If Target.Column > 5 And Target.Column < 8 Then
If Range("A" & Target.Row).Value = Range("A" & Target.Row + 1).Value Then
If Range("A" & Target.Row + 1).Value <> "" Then
Range(Target.Address).Offset(1).Value = Range(Target.Address).Value
End If
End If
Else
'If initial pan weight add start date
If Target.Column = 8 Then
If Target.Cells.Text <> "" Then
If Not IsNumeric(Target.Cells.Value) Then
GoTo Finally
Else
Application.EnableEvents = False
Range("StartDate").Offset(Target.Cells.Row - y).Value = Format(Now(), "MM/DD/YY HH:NN:SS")
Application.EnableEvents = True
End If
Else
Application.EnableEvents = False
Range("StartDate").Offset(Target.Cells.Row - y).Value = ""
Application.EnableEvents = True
End If
End If
End If
LastRow = Worksheets("Raw_Data").Range("A65536").End(xlUp).Row
For Each cell In Target.Cells
If cell.Value <> "" Then
If Not IsNumeric(cell.Value) Then GoTo Finally
Select Case cell.Column
Case 9, 11, 13
Application.EnableEvents = False
If CalcHEM(cell.Row - y, cell.Column) Then
End If
Application.EnableEvents = True
Case Else
'Do nothing yet
End Select
End If
Next
UndoFunctionality.UndoButtonAvailability
UndoFunctionality.RedoButtonAvailability
Finally:
If Application.EnableEvents = False Then Application.EnableEvents = True
Exit Sub
Catch:
MsgBox "An error has occurred in the code execution." & vbNewLine _
& "The message text of the error is: " & Error(Err), vbInformation, "TSSCalcs.AddQC"
Resume Finally
End Sub
The only thing left is to add two buttons to the worksheet and assign the macro used to the UndoButton_Click() and RedoButton_Click() events which will run the RevertState() and ProgressState() methods.
I found a little trick using Application.OnTime. So it is possible to use Undo repeatedly.
The Repeat button is not the Redo button. You can find it in the Edit menu or put it on your ribbon.
I am using Excel 2003.
Here is a working sample. Put the code inside ThisWorkbook module.
Dim Undos As New Collection
Sub Change()
' push previous cell values to the end of your undo array
Undos.Add ActiveCell.Value
' change the cell values as you wish
ActiveCell.Value = "(" + ActiveCell.Value + ")"
PlanUndo
PlanRepeat
End Sub
Sub Undo()
' make sure the undo array is not empty
If (Undos.Count > 0) Then
' pop previous cell values from the end of your undo array
Dim Value
Value = Undos.Item(Undos.Count)
Undos.Remove Undos.Count
' revert the cell values
ActiveCell.Value = Value
End If
If (Undos.Count > 0) Then
PlanUndo
End If
PlanRepeat
End Sub
Function PlanUndo()
Application.OnTime Now, "ThisWorkbook.SetUndo"
End Function
Sub SetUndo()
Application.OnUndo "Undo last change", "ThisWorkbook.Undo"
End Sub
Function PlanRepeat()
Application.OnTime Now, "ThisWorkbook.SetRepeat"
End Function
Sub SetRepeat()
Application.OnRepeat "Repeat last change", "ThisWorkbook.Change"
End Sub

Finding and leaving only duplicates in spreadsheet

In Excel, I created a macro to find and leave only duplicated values across multiple columns within the current selection--removing any cells that were only found once. Well, at least that's what I thought I created anyway, but it doesn't seem to work. Here's what I've got:
Sub FindDupsRemoveUniq()
Dim c As Range
Dim counted() As String
For Each c In selection.Cells
Dim already_found As Boolean
already_found = Contains(counted, c.Text)
If Not (already_found) And WorksheetFunction.CountIf(selection, c) <= 1 Then
c.Delete Shift:=xlUp
ElseIf ("" <> c.Text) And Not (already_found) Then
If Len(Join(counted)) = 0 Then
ReDim counted(1)
Else
ReDim Preserve counted(UBound(counted) + 1)
End If
counted(UBound(counted) - 1) = c.Text
End If
Next c
End Sub
Private Function Contains(ByRef arr() As String, cell As String) As Boolean
Dim i As Integer
Contains = False
If Len(Join(arr)) = 0 Then
Exit Function
End If
For i = LBound(arr) To UBound(arr)
If cell = arr(i) Then
Contains = True
Exit Function
End If
Next
End Function
I had to do this because I had ~180k items across multiple columns, and I had to find anything that was duplicated, and under which column those duplicates are showing in. However, when it completes, it seems that most of the singular instances are still there. I can't figure out why this isn't working.
EDIT: This is what my code ended up looking like based on #brettdj's solution below:
Sub FindDupsRemoveUniq()
Dim lRow As Long
Dim lCol As Long
Dim total_cells As Long
Dim counter As Long
Dim progress_str As String
Dim sel
sel = selection.Value2
total_cells = WorksheetFunction.Count(selection)
counter = 0
progress_str = "Progress: "
Application.EnableEvents = False
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False
Application.StatusBar = progress_str & "0 of " & total_cells & " : 0% done"
For lRow = 1 To UBound(sel, 1)
For lCol = 1 To UBound(sel, 2)
counter = counter + 1
Application.StatusBar = progress_str & counter & " of " & total_cells & " : " & Format(counter / total_cells, "0%")
If WorksheetFunction.CountIf(selection, sel(lRow, lCol)) < 2 Then
sel(lRow, lCol) = vbNullString
End If
Next lCol
Next lRow
selection = sel
Application.StatusBar = "Deleting blanks..."
selection.SpecialCells(xlCellTypeBlanks).Delete Shift:=xlUp
Application.StatusBar = "Done"
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub
I tried to speed things up with a few optimizations, though I'm not sure how much they helped. Also, the status bar updates ended up being rather pointless too since Excel got so bogged down. It seemed to give up updating after ~300 iterations. Nonetheless, it did work.
I would suggest using an array, same approach otherwise as simoco
This approach removes the cell contents but doesn't shift the cells up as I wasn't clear that you wanted this
Sub Kill_Unique()
Dim X
Dim lngRow As Long
Dim lngCol As Long
X = Selection.Value2
For lngRow = 1 To UBound(X, 1)
For lngCol = 1 To UBound(X, 2)
If Application.CountIf(Selection, X(lngRow, lngCol)) < 2 Then X(lngRow, lngCol) = vbNullString
Next lngCol
Next lngRow
Selection.Value2 = X
End Sub
If you want delete all cells with unique values from selection, try this one:
Sub test()
Dim rngToDelete As Range, c As Range
For Each c In Selection
If WorksheetFunction.CountIf(Selection, c) = 1 Then
If rngToDelete Is Nothing Then
Set rngToDelete = c
Else
Set rngToDelete = Union(rngToDelete, c)
End If
End If
Next
If Not rngToDelete Is Nothing Then rngToDelete.Delete Shift:=xlUp
End Sub

Resources