Excel macro "Entirerow.Hidden" How to make it run faster? [duplicate] - excel

I have working code to hide/unhide rows depending on the corresponding cell value.
This is a list of materials and there is a 'finalize' button. You press the button and any row where quantity = 0 should be hidden.
There are 400+ lines and I can see the lines disappear. It is processing roughly 20 lines per second which makes it over 20 seconds to do the list. The list will double every few months.
Is there another method that will hide the lines faster?
Hide:
Public Sub HideRows()
Dim cell As Range
For Each cell In ActiveSheet.Range("H18:H469")
cell.EntireRow.Hidden = (cell.Value = 0 And cell.Value <> "")
Next cell
End Sub
Unhide:
Public Sub UnhideRows()
Dim cell As Range
For Each cell In ActiveSheet.Range("H18:H469")
If (cell.Value = 0 And cell.Value <> "") Then cell.EntireRow.Hidden = False
Next cell
End Sub

I was just typing up as appeared in comments. Use Union to gather qualifying ranges and hide in one go. I am not sure why you are doing a double test. Wouldn't = 0 be sufficient? Or as #Marcucciby2 queries, did you intend an Or?
And as mentioned in other answer, you can do some optimization by switching of things like ScreenUpdating, pageBreaks and switch to manual calculation mode.
If possible, get rid of that ActiveSheet reference and use the actual workbook and worksheet references.
Option Explicit
Public Sub HideRows()
Dim cell As Range, unionRng As Range
For Each cell In ActiveSheet.Range("H18:H469")
If cell.Value = 0 Then
If Not unionRng Is Nothing Then
Set unionRng = Union(unionRng, cell)
Else
Set unionRng = cell
End If
End If
Next
If Not unionRng Is Nothing Then unionRng.EntireRow.Hidden = True
End Sub

Turn off screen updating and manual calculations at the start of your code. Be sure to turn if back on at the end of your code.
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
'...your code...
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True

Related

Build Excel function: Unmerge, calculate, re-merge. Problem: Function starts to run recursive before finishing

My main goal is to be able to autofilter merged cells in one column.In the picture below I want row 7-9 to disappear when I remove "6" from the autofilter menu. But as I have figured, I need the value "6" to be held in all the cells "L7:L9" in order for Excel to do so.
The number 6 is calculated by adding "Num1" and "Num2" (2 * 3) by the following function I have placed in "L7":
Function Exposure(arg1 As Range, arg2 As Range) As Variant
Application.EnableEvents = False
Application.Calculation = xlManual
If Application.ThisCell.Offset(, -1).Value <> "-" And Application.ThisCell.Offset(, -2).Value <> "-" Then
Exposure = Left(Application.ThisCell.Offset(, -1).Value, 1) * Left(Application.ThisCell.Offset(, -2).Value, 1)
End If
If Exposure = 0 Then Exposure = "-"
Application.Calculation = xlAutomatic
Application.EnableEvents = True
End Function
I put the following formula inside the merged cell "L7":=Exposure(K7;J7). Then formula is dragged down."Num1" and "Num2" are controlled by valdiation fields, drop-down menu.
My plan was to unmerge after calculating the Exposure Variant, fill the same value in the remaining rows, then re-merge the same area. So I wrote this stand alone Sub:
Sub WorkingSub(rng As Range)
'Set rng = ActiveCell.MergeArea
rng.UnMerge
For i = 2 To rng.Cells.Count
rng.Cells(i).Value = rng.Cells(1).Value 'This line triggers recursion
Next i
rng.Offset(rng.Cells.Count).Copy 'Copies format from below
rng.PasteSpecial Paste:=xlPasteFormats 'Paste that keeps the values even after merging
End Sub
Which works on its own, but not when called inside the function above. After setting the first value, the function triggers "something", debug show the the function starting over, skipping the rng.PasteSpecial Paste:=xlPasteFormats code.
So my question to you guys is how do i write my function(s) to stop "recursing" and let me unmerge during the function call?
Or am I attacking this the wrong way? What would you do?
I am stuck with merged cells for lots of reasons, this is just one part of many inside this spreadsheet.
An interesting problem. You can capture the filter event through trapping a change in a calculation and then processing the rows of the table for visibility. I've made some assumptions for the initial table range assignment which may need some alteration.
The If Not VisRange Is Nothing Then is actually redundant as the prior line will throw a fit if an empty range is assigned, but I just kept it in. In order to get around having a null range, keep the header range in the initial MergedTableRange so there will always be a row visible
Within a cell either somewhere in the same worksheet or a 'dummy' worksheet
=SUBTOTAL(103,Sheet1!A3:H10) 'Or other table range
In the worksheet module code
Private Sub Worksheet_Calculate()
Dim ws As Worksheet: Set ws = Worksheets("Sheet1")
Dim MergedTableRange As Range: Set MergedTableRange = ws.Range("A2").CurrentRegion
Dim Cell As Range
Dim VisRange As Range: Set VisRange = MergedTableRange.SpecialCells(xlCellTypeVisible)
If Not VisRange Is Nothing Then
For Each Cell In VisRange
If Not Application.Intersect(Cell.MergeArea, VisRange).Address = Cell.MergeArea.Address Then
Cell.Rows.Hidden = True
End If
Next Cell
End If
End Sub
I came up with a different approach. Maybe there's a downside I'm missing. But my few test runs have succeeded.
I allready have a hidden sheet named "Template" where the formats for each new "#" is stored. So whenever the user wants to insert a new row, the template have the merged and the non-merged cells ready and insert is done through copy paste.
In that same sheet I made 2 merged rows in column 2, 3 merged cells in column 3 and so on:
This way I'm able to copy the correct number of merged rows to paste after filling the unmerged rows with their correct values.
I came to the conclusion that I could catch a Worksheet_change on the "Num1" and "Num2" columns instead of catching and canceling an autofilter call.
So I added:
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Target.Worksheet.Range("J:J")) Is Nothing Then
Call UnMergeMerge(Cells(Target.Row, "L").MergeArea)
End If
If Not Intersect(Target, Target.Worksheet.Range("K:K")) Is Nothing Then
Call UnMergeMerge(Cells(Target.Row, "L").MergeArea)
End If
End Sub
And the UnMergeMerge sub ended up being:
Sub UnMergeMerge(rng As Range)
Application.EnableEvents = False
Application.ScreenUpdating = False
rng.UnMerge
For i = 2 To rng.Cells.Count
rng.Cells(i).Value = rng.Cells(1).Value
Next i
With Sheets("Template")
.Range(.Cells(8, rng.Cells.Count), .Cells(8 + rng.Cells.Count, rng.Cells.Count)).Copy
End With
rng.PasteSpecial Paste:=xlPasteFormats
Application.EnableEvents = True
Application.ScreenUpdating = True
End Sub
Still not sure it's the fastest and best approach...Do you guys still believe catching, undoing and running a different autofilter would be more effective?

Use Excel VBA to update column cell contents

This is a simple answer but one I cannot find.
I have two columns of data.
Column A (1) has yes/no data validation list options in every cell.
Column B also has data validation with say 6 strings of text options in every cell.
But I only want each the corresponding cell (column B) to update in the same row as column A
e.g A20 toggled, then B20 is updated. Like so
A20 is selected “Yes” from the dropdown option and B20 is updated with the string “complete” which is one of the states you can select in the dropdown boxes manually in every cell in column B.
I had some code but I would have to write an argument for every cell and then two macros for every yes / no.
This is code that works for one cell only but this is not ideal for many cells but it works
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Range("A20")) Is Nothing Then
Select Case Range("A20")
Case "Yes": Macro_001
Case "No": Macro_002
End Select
End If
End Sub
Sub Macro_001()
Application.Calculation = xlManual
Application.ScreenUpdating = False
Sheets("August 2020").Select
Sheets("August 2020").Range("B20").Select
ActiveCell.FormulaR1C1 = "Complete"
Application.ScreenUpdating = True
Application.Calculation = xlAutomatic
End Sub
Sub Macro_002()
Application.Calculation = xlManual
Application.ScreenUpdating = False
Sheets("August 2020").Select
Sheets("August 2020").Range("B20").Select
ActiveCell.FormulaR1C1 = ""
Application.ScreenUpdating = True
Application.Calculation = xlAutomatic
End Sub
There much be an easier way with .range perhaps
Thanks in advance
In the developer tab click view code, choose the sheet you want the macro to run on, make sure the upper left drop down says worksheet and the upper right says Change (I'll assume your sheet has headers):
Private Sub Worksheet_Change(ByVal Target As Range)
Dim KeyCells As Range
Dim ChangeCell As Range
Dim numrows As Long
numrows = Cells(Rows.Count, 1).End(xlUp).Row
Set TriggerCells = Range("A1")
Set KeyCells = Range("B2:B" & numrows)
If Application.Intersect(TriggerCells, Range("A1")) = "[Column Header Text]" Then
If Not Application.Intersect(KeyCells, Range(Target.Address)) Is Nothing Then
If Target.Value = "Yes" Then
Range("B" & Target.Row).Value = "Completed"
End If
End If
End If
End Sub
Try that, see if it works for you.

How to dynamically lock and unlock a cell in Excel?

What should i do to dynamically lock/unlock my cell in excel? For example, if i create a new document, by default all cells are unlock but i entered a data on that cell it will be lock. I tried this, which i found here Lock empty cells and unlock free cells
Sub test()
Dim rngTemp As Range
For Each rngTemp In Range("A1:XFD1048576").Cells
With rngTemp
If .Value > 0 Or Len(.Value) > 0 Then
.Locked = False
End If
End With
Next
End Sub
but it's not working on my case. I am using 2007 excel version. Do i still need to save the code or Alt + Q is enough?
EDIT: As per #JvdV's answer I tried the following:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim rng As Range
With Sheet1
.Unprotect
.Cells.Locked = True
.Cells.SpecialCells(xlCellTypeBlanks).Locked = False
.Protect
End With
End Sub
But this returns an error Run-time error '1004' No cells were found on .Cells.SpecialCells(xlCellTypeBlanks).Locked = False.
If you really are intested in those cells, you can simply refer to a worksheet's cells. Also, no need to loop through those cells individually, for example:
Sub test()
Dim rng As Range
With Sheet1 'Change according to your sheet's CodeName
.Unprotect
.Cells.Locked = False
.Cells.SpecialCells(xlCellTypeBlanks).Locked = True
.Protect
End With
End Sub
Where .Cells.Locked = False unlocks all cells and .Cells.SpecialCells(xlCellTypeBlanks).Locked = True locks all cells blank cells (Note: a ="" value through formulas is considered a value and will stay unlocked)
Both Unprotect and Protect are needed to have full effect of your changes.
If this is code you want to run each time a value is changed, you'll have to look into the Worksheet_Change event. And if your goal is to have empty cells unlocked and cells that contain a value locked, just swap around the True and False.
EDIT (as per your comments)
If this is something you like to run on every next selection of cells, try the following (error handler included since you not using the whole worksheet nomore)
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Sheet1.Unprotect 'Change according to your sheet's CodeName
With Target
.Cells.Locked = True
On Error Resume Next
.Cells.SpecialCells(xlCellTypeBlanks).Locked = False
On Error GoTo 0
End With
Sheet1.Protect
End Sub
If you looking for an alternative where you loop through your target range, you can implement the suggestion by #M.Schalk
As an addition to the (correct) answer above, here is my suggestion for a Worksheet_Change event, as you requested in the comments. This will have to be placed in the workbook-specific code module:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim cll As Range
On Error Resume Next
For Each cll In Target.Cells
With cll
If .Value2 <> vbNullString Then
.Locked = True
Else
.Locked = False
End If
End With
Next
End Sub
It's important to note, that (at least in my version of Excel) the .Locked property of a cell only has an effect when the sheet is protected. To change the value of the .Locked property however, the sheet must not be protected. To incorporate this you might want to use something like this:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim cll As Range
On Error GoTo Handler
Me.Unprotect
For Each cll In Target.Cells
With cll
If .Value2 <> vbNullString Then
MsgBox cll.Value2
.Locked = True
Else
MsgBox "NullString"
.Locked = False
End If
End With
Next
Handler:
Me.Protect
End Sub
This will lead to every cell becoming un-changeable once a value is entered, while still letting the user enter values in all empty cells. To change existing values you will need to manually unprotect the sheet. You might use something like the code provided in the answer above to restore a desired state after the sheet was unprotected and changed.

Loop through first cells of selected merged ranges only

I am trying to give the user the option to do simple arithmetic operations on selected cells.
The thing is that most cells are merged ranges.
I got the following already but the problem with it is that it loops through all cells while I only want it to only affect those cells that are not merged or only to the first cells of merged ranges.
Sub test()
Application.ScreenUpdating = False
Dim cel As Range
Dim selectedRange As Range
myValue = InputBox("Enter")
Set selectedRange = Application.Selection
For Each cel In selectedRange.Cells
On Error Resume Next
cel.Value = Evaluate(cel.Value & myValue)
Next cel
End Sub
Although VBasic2008's answer works, it's not totally correct. The problem is that each cell in merged range always returns True for MergedCells property. This means that excessive processing is done in a loop (i.e. incrementing a value) for cells other than top-left cell. To fix this situation, you should test each cell for the need to process. You can do this in several ways:
You can compare the address of a cell with the top-left cell address (Option 1 in code).
You can test the length of cell's value. If it's zero, then it's not top-left cell, so you skip it (Option 2 in code).
Code:
Sub IncrValues()
Dim rng As Range, myValue%
myValue = InputBox("Enter")
For Each cell In Selection
If cell.MergeCells Then
'// Option 1:
If cell.Address = cell.MergeArea(1).Address Then
cell.Value = cell.Value + myValue
End If
'// Option 2:
'If Len(cell) > 0 Then
' cell.Value = cell.Value + myValue
'End If
Else
cell.Value = cell.Value + myValue
End If
Next
End Sub
The MergeCells Property
Using the MergeCells property in an If statement, you check if a cell is not merged, then execute the following statement(s), otherwise the statement(s) after Else.
In the following example, the range I3:M12 is selected and 5 is entered as myValue. The first table is the state of the second table before.
The Code
Sub test()
Dim cel As Range
Dim selectedRange As Range
Dim myValue As Double
Application.ScreenUpdating = False
myValue = InputBox("Enter")
Set selectedRange = Application.Selection
For Each cel In selectedRange.Cells
If Not cel.MergeCells Then
' If not merged cell.
cel.Value = Evaluate(cel.Value & myValue)
Else
' if merged cell.
cel.Value = Evaluate(cel.Value + myValue)
End If
Next cel
Application.ScreenUpdating = True
End Sub
Count
We can expand the previous tables by adding a COUNT column,
where it is more obvious how the merged cells are being 'ignored' in Excel i.e. all cells except the first cell of a merged area will not be counted (or summed up, or ...).
The following shows the difference between counting the cells in VBA and in Excel.
Sub MergeTest()
With Range("J3:J12")
Debug.Print .Cells.Count
Debug.Print WorksheetFunction.Count(.Cells)
End With
With Range("J3:N12")
Debug.Print .Cells.Count
Debug.Print WorksheetFunction.Count(.Cells)
End With
End Sub
The results in the Immediate window are
10,
9,
50,
46,
which shows how VBA will count every cell, but Excel will exclude all cells of a merged area except the first.
In VBA Help search for the MergeArea property for some further info.

Hide/show row code speed

Goal: Efficiently show/hide rows based on the data in the row.
Create a helper column that determines whether or not
a row should be hidden.
Have the formula in the helper
column return an error or a number.
Hide the helper column and write
code to execute the hiding/showing.
Question: Which one of the following methods would you expect to be faster? Column B is the helper column and will always be contiguous.
Sub SetRowVisibility1()
Dim rowsToCheck As Range
With ActiveSheet
Set rowsToCheck = .Range(Range("B7"), Range("B7").End(xlDown))
End With
Dim needToShow As Range, needToShow_Showing As Range
Dim needToHide As Range, needToHide_Showing As Range
Set needToShow = rowsToCheck.SpecialCells(xlCellTypeFormulas, xlNumbers)
Set needToHide = rowsToCheck.SpecialCells(xlCellTypeFormulas, xlErrors)
On Error Resume Next
Set needToShow_Showing = needToShow.Offset(0, 1).SpecialCells(xlCellTypeVisible)
Set needToHide_Showing = needToHide.Offset(0, 1).SpecialCells(xlCellTypeVisible)
On Error GoTo 0
If Not needToHide_Showing Is Nothing Then
needToHide_Showing.EntireRow.Hidden = True
End If
If Not needToShow Is Nothing Then
If needToShow.Count <> needToShow_Showing.Count Then
needToShow.EntireRow.Hidden = False
End If
End If
End Sub
Sub SetRowVisibility2()
Dim rowsToCheck As Range
With ActiveSheet
Set rowsToCheck = .Range(Range("B7"), Range("B7").End(xlDown))
End With
Dim needToShow As Range, needToHide As Range
Dim cell As Range
For Each cell In rowsToCheck
If IsError(cell.Value) And (cell.EntireRow.Hidden = False) Then
If needToHide Is Nothing Then
Set needToHide = cell
Else
Set needToHide = Union(needToHide, cell)
End If
End If
If Not IsError(cell.Value) And (cell.EntireRow.Hidden = True) Then
If needToShow Is Nothing Then
Set needToShow = cell
Else
Set needToShow = Union(needToShow, cell)
End If
End If
Next cell
If Not needToHide Is Nothing Then needToHide.EntireRow.Hidden = True
If Not needToShow Is Nothing Then needToShow.EntireRow.Hidden = False
End Sub
there is a different way and that is to use th auto filter feature - after all VBA has an A in it - use the features of the application wherever possible
so this bit of code is pretty short and sweet - assumes that the data is a contiguous block in columns a and b and assumes no other error handling in play. the resume next line allows for the filter to be already turned on.
Sub showHideRange()
Dim testrange
testrange = Range("A1").CurrentRegion.Address
On Error Resume Next
testrange.AutoFilter
ActiveSheet.Range(testrange).AutoFilter Field:=2, Criteria1:="show"
End Sub
If you do not wish to show the user what's happening, would it not be better to perform the calculation in VBA itself, rather than in a hidden column? Granted, that would seem to lock you into option 2, which I suspect is the slower option ... most of my VBA experience is in older versions of Excel, so I've not had the pleasure of working with some of the newer features, and the tasks I've done that involved processing rows of data were done row-by-row.
I guess one possible issue with the first sub is that if there is a problem with the worksheet or the values you're using to determine hiding/showing, the process will fail. If you check row-by-row and there is a row that causes problems, you could skip over that row and process the other ones correctly.

Resources