Looping over checkboxes with VBA in Excel very slow - excel

I have an Excel Sheet with about 4500 checkboxes (I know, it sounds stupid, but it is for a customer, please do not ask...).
Just wrote the VBA Sub below to uncheck all the boxes together. So far it works, but it is terribly slow, it takes more than 5 minutes until all boces are unchecked and while the Sub is running, the whole Excel Applikation grays out freezes. I know, 4500 Checkboxes is quiet a lot, but I wonder that it is really enough to bring Excel in such a trouble....Has anyone an idea?
Best
Michael
Sub DeselectAll()
Application.EnableCancelKey = False
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False
Application.EnableEvents = False
Dim wksA As Worksheet
Dim intRow As Integer
Set wksA = Worksheets("Companies")
For intRow = 1 To 4513
wksA.CheckBoxes("Checkbox_" & intRow).Value = False
Next
End Sub

Without selection:
Sub DeselectAll()
With Worksheets("Companies").CheckBoxes
.Value = xlOff
End With
End Sub

Just don't loop.
This is a good example of when Selection can help:
To set all checkboxes:
Sub dural()
ActiveSheet.CheckBoxes.Select
Selection.Value = xlOn
End Sub
To uncheck all checkboxes:
Sub dural2()
ActiveSheet.CheckBoxes.Select
Selection.Value = xlOf
End Sub
( tested on Forms-type checkboxes )

The best answer I thumbs up for is #EvR solution. I am not trying to answer but offering an idea of a workaround.
I checked the time by adding 4000 ComboBox in blank sheet in a blank workbook with a simple 3 line loop (omg I forgot to off screen updating and calculations etc). It took around 10 minutes in my old laptop. I don’t have courage to repeat the ordeal again.
When I tried to use your piece of code with looping it is taking 3-4 seconds only and with #EvR’s solution without loop and selection is taking 1-2 seconds. These times are actual time taken with Debug.Print or writing to some cells. Actual drama unfolds after screen updates, calculations, events are enabled with the sheet active. It become highly unstable and any careless click etc cause excel to ‘not responding’ state for 2-5 mintues.
Though Customer and Boss are always right. Once in my life I succeeded to persuade someone in a similar approach of hundreds of buttons on a worksheet to something virtual. My Idea is to create virtual checkbox in the sheet. Proper cell sizing and border with validation of the cells to `=ChrW(&H2714)’ and ignore blank and a simple code like below can make it a pass-through type of work-around.
Public Prvsel As Range
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim isect, Cl As Range
Set isect = Application.Intersect(Target, Range("C1:C4000"))
If isect Is Nothing Then
Set Prvsel = Nothing 'Release multiple selection
Exit Sub
End If
If isect.Cells.Count > 1 Then
Set Prvsel = isect 'storing multiple selection for next click event
Else
If Target.Value = ChrW(&H2714) Then
Target.Value = ""
Else
Target.Value = ChrW(&H2714)
End If
If Not Prvsel Is Nothing Then
For Each Cl In Prvsel.Cells
Cl.Value = Target.Value
Next Cl
End If
End If
End Sub

Elaborating on #Ahmed AU solution.
Select/Deselect signal/ multiple virtual checkboxs
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim isect As Range
Dim Cl As Range
Dim Prvsel As Range
Set isect = Application.Intersect(Target, Range("C1:C4000"))
If isect Is Nothing Then
Set Prvsel = Nothing 'Release multiple selection
Exit Sub
End If
' Use WINGDING font Chr (254) for checked
' Use WINGDING font Chr (111) for uncheck
If isect.Cells.Count >= 1 Then
Set Prvsel = isect
For Each Cl In Prvsel.Cells
If Cl.Value = Chr(111) Then
Cl.Value = Chr(254)
Else
Cl.Value = Chr(111)
End If
Next Cl
End If
'Go to offset cell selection
Selection.Offset(0, 1).Select
End Sub

Related

Excel VBA: Show/Hide Pictures on yes/no dropdowns; troubleshooting simultaneous .Visible?

Forgive me, I know nothing about VBA and I had the crazy idea to make Xmas presents that digitally replicate a scratch-off bucket list or novelogue poster in Excel. After lots of frustration, I got as far as being able to turn on/off a picture with an individual dropdown ("Unread"/"Complete"). It's a big table of book titles with book covers hidden until each one is marked Complete, then that book cover should display.
The problem is that only one image will show at a time. If two books are marked complete, only the most recent changed to Complete will be visible. Is this because I have them all running in the same Sub? Do I need a separate Sub for every image (that would be 100 Subs)?
Snippet, let's say there were only 5 books:
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Range("B4")) Is Nothing Then
Shapes("Picture 1").Visible = Range("B4").Value = "Complete"
Else
Shapes("Picture 1").Visible = False
End If
If Not Intersect(Target, Range("D4")) Is Nothing Then
Shapes("Picture 2").Visible = Range("D4").Value = "Complete"
Else
Shapes("Picture 2").Visible = False
End If
If Not Intersect(Target, Range("F4")) Is Nothing Then
Shapes("Picture 3").Visible = Range("F4").Value = "Complete"
Else
Shapes("Picture 3").Visible = False
End If
If Not Intersect(Target, Range("H4")) Is Nothing Then
Shapes("Picture 4").Visible = Range("H4").Value = "Complete"
Else
Shapes("Picture 4").Visible = False
End If
If Not Intersect(Target, Range("J4")) Is Nothing Then
Shapes("Picture 5").Visible = Range("J4").Value = "Complete"
Else
Shapes("Picture 5").Visible = False
End If
End Sub
I'm sure there's some more efficient way to do this but I'm more concerned with getting the images to show independently of other images' visible values. Thanks!
EDIT: I'm sorry my explanation wasn't clear enough. I don't have enough reputation to share an image, unfortunately. There are 10 columns of book covers, and 10 rows. Every other column holds images (in between is spacer). See below for rows.
When marked "Unread" in the cell below, the image should be hidden. When marked "Complete" below, the image should be revealed.
To explain cell layout:
B2 holds the image
B3 holds the title
B4 holds the data validated dropdown (that VBA checks for B2's image value of visible)
Spreadsheet layout:
B2:T2 have the first 10 images and thus,
B4:T4 have the first 10 data validated dropdowns
B7:T7 next dropdowns
B10:T10 " "
B13:T13, then B16:T16, B19:T19, B22:T22, B25:T25, B28:T28, B31:T31
https://i.imgur.com/RKC5AzG.png
I'm not 100% sure if I understood your question correctly but I tried just putting the repetitive parts of your code into a loop. Also, it will now not turn pictures invisible again.
If you are unfamiliar with the Cells property I used, you can find more information on it here.
Also, I split up this piece of code:
Shapes("Picture 1").Visible = Range("B4").Value = "Complete"
How was that supposed to work? I guess you wanted the picture to be visible and the cell to show "Complete"...
The picture name inside the loop is created by string concatenation from "Picture " & i
Private Sub Worksheet_Change(ByVal Target As Range)
Dim i as Long
Dim totalNumberOfPictures as Long
totalNumberOfPictures = 8 'Input number of pictures you have here
For i=1 To totalNumberOfPictures
If Not Intersect(Target, Cells(4, 2 * i)) Is Nothing Then
Shapes("Picture " & i).Visible = True
Cells(4, 2 * i) = "Complete"
End If
Next i
End Sub
Or if you meant every picture where the assigned cells value is changed to "Complete" should be visible, you can try this:
Edit: Updated to address the added detail in your question...
Private Sub Worksheet_Change(ByVal Target As Range)
Dim i as Long
Dim totalNumberOfPictures as Long
Dim count as Long
Dim rowOffset as Long
totalNumberOfPictures = 100 'Input number of pictures you have here
count = 1
rowOffset = 0
For i=1 To totalNumberOfPictures
If Cells(4 + rowOffset, 2 * count) = "Complete" Then
Shapes("Picture " & i).Visible = True
Else
Shapes("Picture " & i).Visible = False
End If
If count = 10 Then ' 10 is the number of pictures per "row"
rowOffset = rowOffset + 3 ' 3 is the row difference between your dropdown rows
count = 0
End if
count = count+ 1
Next i
End Sub
Let me know if it works, or if this is not what you meant, what you actually want...
If it was my project, I’d do it a slightly different way. I’ll offer this as a suggestion – let me know if it doesn’t suit you & I’ll delete it.
Step 1 – rename the pictures to match the book titles. This will be tiresome to start with, but it means you won’t be tied to a loop based on “Picture #” because picture names can vary. Also makes the names more intuitive.
Step 2 – copy the code below into a standard module & run it when you’re on the sheet with the pictures. It will create a named, non-contiguous range. If you look at the code, it’s fairly obvious how you can adjust it over time to suit your changing needs. Simply add more ranges as you need and rerun it.
Sub MakeRangeName()
ActiveSheet.Range _
("B4:T4,B7:T7,B10:T10,B13:T13,B16:T16,B19:T19,B22:T22,B25:T25,B28:T28,B31:T31") _
.Name = "myRange"
End Sub
Step 3 – copy the code below to the Sheet code module where you have the pictures. It will run whenever there’s a change in the range you created in Step 2, but will only affect the actual picture of interest.
Private Sub Worksheet_Change(ByVal Target As Range)
On Error GoTo GetOut
Application.EnableEvents = False
Dim s As String
If Not Intersect(Range("myRange"), Target) Is Nothing Then
If ActiveCell.Offset(-1, 0) = "" Then GoTo Letscontinue '<< to ignore the inbetween columns
s = ActiveCell.Offset(-1, 0).Text
If ActiveCell.Value = "Complete" Then
ActiveSheet.Shapes(s).Visible = True
Else
ActiveSheet.Shapes(s).Visible = False
End If
End If
Letscontinue:
Application.EnableEvents = True
Exit Sub
GetOut:
MsgBox Err.Description
Resume Letscontinue
End Sub
I’ve tested it and it works for me. Let me know if you get stuck on anything.

How to apply code to all the following rows

I have this code but it only work for my first row.
It is suppose to look if the checkbox on B, C or D is checked, and if so, a date + username will automaticaly fill in F and G.
here is a picture of my table:
This is what my code looks like:
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
If Range("B2") Or Range("C2") Or Range("D2") = True Then
Range("G2").Value = Environ("Username")
Range("F2").Value = Date
Else
Range("F2:G2").ClearContents
End If
End Sub
Enter this code in a regular module, select all your checkboxes and right-click >> assign macro then choose ReviewRows.
This will run the check whenever a checkbox is clicked - a bit of overhead since all rows will be checked, but should not be a big deal.
Sub ReviewRows()
Dim n As Long
For n = 1 To 100 'for example
With Sheet1.Rows(n)
If Application.CountIf(.Cells(2).Resize(1, 3), "TRUE") > 0 Then
If Len(.Cells(6).Value) = 0 Then 'only enter if currently empty?
.Cells(6) = Date
.Cells(7) = Environ("Username")
End If
Else
.Cells(6).Resize(1, 2).ClearContents
End If
End With
Next n
End Sub
If you want to be more precise then Application.Caller will give you the name of the checkbox which was clicked, and you can use that to find the appropriate row to check via the linkedCell.
Sub ReviewRows()
Dim n As Long, shp As CheckBox, c As Range, ws As Worksheet
Set ws = ActiveSheet
On Error Resume Next 'ignore error in case calling object is not a checkbox
Set shp = ActiveSheet.CheckBoxes(Application.Caller) 'get the clicked checkbox
On Error GoTo 0 'stop ignoring errors
If Not shp Is Nothing Then 'got a checkbox ?
If shp.LinkedCell <> "" Then 'does it have a linked cell ?
With ws.Range(shp.LinkedCell).EntireRow
If Application.CountIf(.Cells(2).Resize(1, 3), "TRUE") > 0 Then
If Len(.Cells(6).Value) = 0 Then 'only enter if currently empty?
.Cells(6) = Date
.Cells(7) = Environ("Username")
End If
Else
.Cells(6).Resize(1, 2).ClearContents
End If
End With
End If 'has linked cell
End If 'was a checkbox
End Sub
However this appraoch is sensitive to the exact positioning of your checkbox
You have a long way to go!
Unfortunately, If Range("B2") Or Range("C2") Or Range("D2") = True Then is beyond repair. In fact, your entire concept is.
Start with the concept: Technically speaking, checkboxes aren't on the worksheet. They are on a layer that is superimposed over the worksheet. They don't cause a worksheet event, nor are they responding to worksheet events. The good thing is that they have their own.
If Range("B2") Or Range("C2") Or Range("D2") = True Then conflates Range with Range.Value. One is an object (the cell), the other one of the object's properties. So, to insert sense into your syntax it would have to read, like, If Range("B2").Value = True Or Range("C2").Value = True Or Range("D2").Value = True Then. However this won't work because the trigger is wrong. The Worksheet_Change event won't fire when when a checkbox changes a cell's value, and the SelectionChange event is far too common to let it run indiscriminately in the hope of sometimes being right (like the broken clock that shows the correct time twice a day).
The answer, therefore is to capture the checkbox's click event.
Private Sub CheckBox1_Click()
If CheckBox1.Value = vbTrue Then
MsgBox "Clicked"
End If
End Sub
Whatever you want to do when the checkbox is checked must be done where it now shows a MsgBox. You can also take action when it is being unchecked.

Excel VBA - Highlight Selected Cell

I am using this code to highlight the selected cell and it works fine. However, I was wondering if there is a better way of doing it without using On error resume next.
Also, If I use this statement does that mean other errors in the same event or procedures called by the event would not be catched?
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
If Not Intersect(Target, Union(Me.Range("range_name"), Me.Range("range_name2"), _
Me.Range("range_name3"))) Is Nothing Then
Static xLastRng As Range
On Error Resume Next
Target.Interior.ColorIndex = 6
xLastRng.Interior.ColorIndex = xlColorIndexNone
Set xLastRng = Target
End If
End Sub
Here is another approach, because right now you are inputing a fill color instead of conditional formatting. You might ruin other cells their format doing so.
What I done is for example use this conditional formatting rule on column C, D and E (you have other ranges so use them accordingly).
=AND(ROW()=CELL("ROW"),COLUMN()=CELL("COLUMN"))
This alone should do the trick, but it's some kind of glitch (too fast) for the screen to properly update the selected cell with a conditional format. Scrolling down and back up fixes this and you will see that the selected cell is formatted if it is within your ranges.
To counter this I used a forced waiting time on a selection change in the worksheet untill Excel is done calculating...
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Application.ScreenUpdating = False
If Not Application.CalculationState = xlDone Then
DoEvents
End If
Application.ScreenUpdating = True
End Sub
No you will notice that it will not glitch out :)
If the glitch doesn't happen on your side, you can leave out the VBA part.
Try this:
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Static xLastRng As Range
Dim rng As Range
Set rng = Application.Intersect(Target, Union(Me.Range("range_name"), _
Me.Range("range_name2"), _
Me.Range("range_name3")))
'clear previous range hilite first, since overlap
' between previous & new could occur
If Not xLastRange Is Nothing Then
xLastRng.Interior.ColorIndex = xlColorIndexNone
Set xLastRange = Nothing
End If
If Not rng Is Nothing Then
Target.Interior.ColorIndex = 6
Set xLastRange = rng
End If
End Sub
It's unclear from your question whether you'd want to clear any previous highlighting if a new selection falls outside of your checked ranges.
I like this one!!
http://www.cpearson.com/excel/RowLiner.htm
Simply point to the Excel AddIn and run it.
https://trumpexcel.com/excel-add-in/

Hide and Unhide Toggle button - running very slow

I have a workbook with 12 sheets and I have placed one Command button in each sheet to hide/unhide rows. For hiding specific rows, I have typed "A" in coloumn A, for every row that needs to be hidden. So code works but it runs forever, takes long time and very slow in hidding or unhiding the rows. In some sheets total number of rows to check are 100 and some sheets it is 750. Please advice the reason for slow running or Is there a better way I can make it work faster. Here is the code:-
Private Sub CommandButton1_Click()
Sheet2.Unprotect ("aaa")
Dim rng As Range
Dim iRow As Range
Dim hidden_status As Boolean
CommandButton1.Caption = "Show / Hide Guidelines"
On Error Resume Next
Set rng = Range("A1:A750")
For Each iRow In rng.Rows
If iRow.Text = "A" Then
With iRow.EntireRow
hidden_status = .Hidden
.Hidden = Not hidden_status
End With
End If
Next iRow
On Error GoTo 0
Sheet2.Protect ("aaa")
End Sub
Each time you hide a row, Excel then is stopping to update your screen (and potentially perform calculations). So to help really speed things up, disable all the screen updates and application events (including calculations) until you've finished hiding.
So with a Sub like this:
Private Sub SetUpdates(ByVal newState As Boolean)
With Application
.ScreenUpdating = newState
.EnableEvents = newState
.DisplayAlerts = newState
End With
End Sub
You can do something like this:
Private Sub CommandButton1_Click()
SetUpdates newState:=False
'--- hide your rows here
SetUpdates newState:=True
End Sub
You could use AutoFilter.
This procedure will hide any row that doesn't contain a value in column A on whichever sheet reference is passed to it. If the filter is already applied it will remove it.
Public Sub HideA(wrkSht As Worksheet)
With wrkSht
If .FilterMode Then
.ShowAllData
Else
.Range("A1", .Cells(.Rows.Count, 1).End(xlUp)) _
.AutoFilter Field:=1, Criteria1:="=", VisibleDropDown:=False
End If
End With
End Sub
On each sheet that contains a button add this code for the button (renaming the procedure as required):
Private Sub CommandButton1_Click()
HideA ActiveSheet
End Sub
The one downside to this is that it doesn't include cell A1 in the filtering.

Where to put my code - VBA

I have a code running on my sheet. This code contains two subroutines. But I want to run this code in all my sheets and I was wondering what would be the best approach.
The total code that is running is the following:
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim Rng As Range
Set Rng = Range(Cells(3, 6), Cells(500, 7))
Dim Intersection
Set Intersection = Application.Intersect(Target, Rng)
If Target.Cells.Count = 1 Then
If Not Intersect(Target, [B2]) Is Nothing Then _
Range("E:E").Find(vbNullString, [E3], , , , xlNext).Select
End If
If Not Intersection Is Nothing Then
If IsNumeric(Selection.Value) And Selection.Value <> "" Then
If (GetAsyncKeyState(vbKeyRButton)) Then 'right mouse button
Selection.Value = (Selection.Value + 1)
Cells(Selection.Row, 1).Select
End If
End If
End If
End Sub
Private Sub Worksheet_BeforeRightClick(ByVal Target As Range, Cancel As Boolean)
Dim Rng As Range
Set Rng = Range(Cells(3, 6), Cells(500, 7))
Dim Intersection
Set Intersection = Application.Intersect(Target, Rng)
If Not Intersection Is Nothing Then
Cancel = True
End If
End Sub
If anyone could give me tips, it would be much appreciated!
Put your code in module. You can follow below link.
http://www.contextures.com/xlvba01.html
It is more complicated that just putting your code in another location. If you put your code in a module (which is the right move) you will need to tell it to run on other sheets too. You're code can be written to apply to any sheet in any open or closed workbook from a Worksheet objects code module, too. It is all about how it is written.
Are you using works like Me or ActiveSheet in your code? This is red flag that no matter where you place it the result will probably not be what you are looking for.
If you want a change event to be effective for all the sheets in a workbook, put the code in the ThisWorkbook module and use the Workbook_SheetChange event. This event will fire when any cell is changed in any sheet.
Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
End Sub

Resources