I have a use case where there's a cell for user input, and a bunch of other cells with formulas dependent on this cell. My intention is to sanitize the data before the formulas are calculated. These formulas can be quite expensive, so I'd like to prevent having them calculated on garbage data if possible. So I wanted to have some vba code check the input and clean it up before the formula calculation is fired. I set up a Worksheet_Change event to handle the user input, however, unfortunately, it would seem that dependent cells are calculated even before Worksheet_Change() is entered. Anyway I can achieve what I want?
You could try this:
First, switch off auto-calculate by choosing Manual from the Calculation Options on the Formulas menu.
Then add this line at the end of your cleaning-up code, to cause the workbook to calculate:
Application.Calculate
This will update all those formulas with the new cleaned value, before Excel does the calculation (since auto-calc is switched off).
If you are worried about the user switching auto-calc back on, hence slowing down the calculations, you could add this further line to switch it off again as soon as possible:
Application.Calculation = xlCalculationManual
Note: maybe this is anti-social because I think the auto-calc option in Excel is instance-wide, so would affect the users' other spreadsheets. You could perhaps get round this with some code in the Workbook_Open event, to remember the state of the Application.Calculation property, then switch it off (as above), and then some code in the Workbook_Close event to restore it again. It may be advisable for the users to make sure they use your workbook alone in an Excel instance, to be sure of no inconvenience to them!
Edit: just on the last note, better to use Workbook_Activate and Workbook_Deactivate instead of Workbook_Open and Workbook_Close.
With that, here is the code, to go into your ThisWorkbook object.
Public previousCalculationState As XlCalculation
Private Sub Workbook_Activate()
previousCalculationState = Application.Calculation
Application.Calculation = xlCalculationManual
End Sub
Private Sub Workbook_Deactivate()
Application.Calculation = previousCalculationState
End Sub
Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
Debug.Print "before clean: G6 = " & Range("G6").Value ' this shows that G6 has the old value, before calc
' your cleaning code to go here, which can use Sh and Target to find out which cell changed
Application.Calculate
Application.Calculation = xlCalculationManual
Debug.Print "after clean: G6 = " & Range("G6").Value ' this shows that G6 has the new value, after calc
Debug.Print
End Sub
This code includes some Debug.Prints, which assume you have a formula in cell G6 on each sheet, and which prove that your cleaning code is happening before the formulas are calculated.
Related
I want to suspend calculations when a certain worksheet is activated and then reset calculation to whatever it was originally set to when the sheet is deactivated.
It works if I have one view open.
If I have two (or more) windows open in the same workbook (View/New Window), the Worksheet_Activate and Worksheet_Deactivate events do not trigger when you move between views, so calculations remain set to manual when the user moves to the alternate view.
I did notice the Workbook_WindowActivate and Workbook_WindowDeactivate events, but can't make that work either.
The code below is in the worksheet module.
I need a "Worksheet_GotFocus" event, or for the Worksheet_Activate event to trigger when the View is activated.
Or perhaps I can hang the code off the Selection_Change event so that as soon as the user clicks within the sheet in question, it triggers the code, but not sure how to handle the deactivation in that case.
Option Explicit
Dim vCalc As Variant
Private Sub Worksheet_Activate()
'save auto calc setting - suspend auto calcs
vCalc = Application.Calculation
If Application.Calculation <> xlCalculationManual Then _
Application.Calculation = xlCalculationManual
End Sub
Private Sub Worksheet_Deactivate()
'force calculation of UDF's (leave related)
If Range("rfRLeaveChanged") = "X" Then
'blah blah blah
End If
'reset auto calc (if applicable)
If Application.Calculation <> vCalc Then _
Application.Calculation = vCalc
End Sub
OK,I figured it out. I have to trigger the code in both the sheet and the ThisWorkbook modules. In ThisWorkbook I trigger it on the Workbook_WindowActivate event. If Windows.Count > 1 and the active sheet is the sheet in question, I change calculations to manual, or for any other sheet I reset calculations to the users previous setting.
I have a large workbook and I am trying to lighten the calcs when pressing F9 as it takes almost 1.30min.
Hence, I would like to deactivate the calculations of two heavy sheets and to have a macro that runs the formulas only when needed.
I have this code so far (1 for each sheet):
Sub OneTimeOnlyCalc()
Sheets("Sheet1").EnableCalculation = True
Sheets("Sheet1").EnableCalculation = False
End Sub
The problem is when I press F9, the calcs freeze at 14% as if these two sheets were blocking the rest of the calcs.
Have you ever tried something like this? What would you recommend to work around this?
You can use the next code to disable F8 key function and allocate to it another procedure (in ThisWorkbook module):
Private Sub Workbook_Activate()
Application.OnKey "{F9}", "SpecialCalculation"
End Sub
You can solve in the same way Shift + F9, Ctrl + Alt + F9 ("{+F9}"), allocating them the same below procedure, "^%F9}"...
Then create your own Calculation function:
Sub SpecialCalculation()
Dim sh As Worksheet
For Each sh In Worksheets
If sh.Name <> "sheet1 to be excluded name" And _
sh.Name <> "sheet2 to be excluded name" Then
sh.Calculate
End If
Next
End Sub
If you need to reverse to the default behavior, use this piece of code in the Deactivate workbook event:
Private Sub Workbook_Deactivate()
Application.OnKey "{F9}"
End Sub
Edited: Changed the events to be used following the discussion with #PEH. He was right. It is better to keep this special behavior of Calculation function only for the workbook keeping these specific events, configured as suggested.
I have an excel sheet that became very slow.
For some reason, wherever I am in the document, if I set Calculation to manual and refresh the current sheet after any change, it is fast enough and serves my purpose.
This is not very comfortable however.
I would like the current sheet (and not the whole document) to be refreshed whenever a cell is changed. This should be done whatever sheet I'm on. How can I do that?
EDIT: let it be clear that I'm not asking for clues on how to make my workbook faster, it was just contextual info. I'm interested in autorefresh only.
Charles Williams has extensive information, techniques and code on calculation on his website http://decisionmodels.com. Quoting from this page:
Another method uses the worksheet.enablecalculation property. When
this property is changed all the formulae on the worksheet are flagged
as uncalculated, so toggling the property to false and then back to
true for all sheets in all open workbooks will cause the next
calculation to be a full calculation.
> Dim oSht as worksheet Application.Calculation=xlCalculationManual
>
> for each oSht in Worksheets oSht.enablecalculation=false
> osht.enablecalculation=true next osht
>
> Application.calculate
You can also use this method for a single worksheet to do a full sheet
calculate.
You can easily work this technique into the Worsheet_Change event, like this:
Private Sub Worksheet_Change(ByVal Target As Range)
Application.Calculation = xlCalculationManual
Me.EnableCalculation = False
Me.EnableCalculation = True
Me.Calculate
End Sub
This will do a full calculation of the current sheet only and leave all other sheets uncalculated.
I believe the Calculation event can happen at the Application, Worksheet or Range level (but not the Workbook level.)
If you have calculation mode set to manual but want the current worksheet (only) updated when you enter something, you could add a worksheet change event (putting the code in the relevant sheet.)
Private Sub Worksheet_Change(ByVal ChangedRng As Range)
ChangedRng.Worksheet.Calculate
End Sub
This will recalculate just the worksheet and not the rest of the Workbook or any other workbooks.
As the provided answers don't fully satisfy me here is my own, borrowing from their appreciated help. It's possible that I've missed some nuance from teylyn's answer. I'll gladly edit if it's relevant.
There doesn't seem to be a way to handle change events at the APPLICATION level.
So all changes must be handled at the SHEET level.
This can be done by copying and pasting this bit of code in EVERY Sheet object of the workbook (and remember to copy and paste it it whenever you add a new sheet):
Private Sub worksheet_change(ByVal Target As Range)
Me.Calculate
End Sub
However because of the tedious copy pastes we had to do, we may want to turn this feature off easily without going back to each sheet object.
We may also want to avoid entering a loop of hell if we do mass change on one sheet (say through a macro, a drag & drop, or a ctrl+H).
To do this we add a new sheet called WS_refresh where we set 3 values :
in A2: Yes/No to enable/disable our new feature
in A4: time of latest refresh
in A6: min interval to allow refresh, in sec (I've set it to 1)
Now when we change values in several cells in a short amount on time, autorefresh will only work on the first change, avoiding previously mentioned loops of hell. If you like to live dangerously set A6 to 0.
This is what should be copied and pasted in every sheet object:
Private Sub worksheet_change(ByVal Target As Range)
auto_refresh = ThisWorkbook.Worksheets("WS_refresh").Range("A2")
If auto_refresh = "Yes" Then
last_refresh = ThisWorkbook.Worksheets("WS_refresh").Range("A4")
refresh_interval_sec = ThisWorkbook.Worksheets("WS_refresh").Range("A6")
refresh_interval_tv = TimeValue("0:00:" & refresh_interval_sec)
If Application.Calculation = xlCalculationManual And Now() > last_refresh + refresh_interval_tv Then
ThisWorkbook.Worksheets("WS_refresh").Range("A4") = Now()
Me.Calculate
End If
End If
End Sub
I am running a macro that opens a file referencing the one I am working in, pastes the relevant items as values into a separate sheet and makes a workbook out of that sheet.
The reason why I am doing this is because there are several thousand countifs, averageifs, and processor-intensive ilk.
The program runs from start to finish, just fine. The issue is that only a few of the items are calculated before the copy/paste operation and so I get a lot of #VALUE errors on the copy of the sheet with the formulas--even though the formulas are calculating correctly on further inspection.
I suspect the correct course of action is to delay the run until the sheet finishes calculating. Any and all help would be appreciated.
EDIT: I've tried all manner of application.calculations and nothing seems to be working. The links and items calculate normally if I open manually and let the processor do its thing. The only items that calculate are the ones that contain "COUNTA" somewhere in it. Is it possible that the application calculation methods don't work with Countifs and the like?
Shouldn't be that hard to do - the Worksheet object has a Calculate property that fires after it calculates. You can add a custom property to the worksheet that exposes a flag that you set after it is done calculating. In the worksheet code that has the time consuming calculation...
Option Explicit
Private can_copy As Boolean
Public Property Get CopyOK()
CopyOK = can_copy
End Property
Private Sub Worksheet_Calculate()
can_copy = True
End Sub
Private Sub Worksheet_Activate()
can_copy = False
End Sub
Private Sub Worksheet_Change(ByVal Target As Range)
can_copy = False
End Sub
'For volitile functions.
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
can_copy = False
End Sub
...and in the calling code:
Dim book As Workbook
Set book = Application.Workbooks.Open("C:\foobar.xlsm")
Do While Not book.Worksheets("Sheet1").CopyOK
DoEvents
Loop
'Do your thing...
Note that I likely missed some events that would trigger a recalculation, but this should cover the scenario of just opening it.
So, I found a means for this to work:
Do Until Application.CalculationState = xlDone
Application.Calculate
While Application.CalculationState <> xlDone
MsgBox Application.CalculationState
DoEvents
Wend
Loop
It was a solution I sort of applied from Siddharth Rout : Wait until Application.Calculate has finished
Thank you everyone for your help!
I have two macros in a worksheet. The first one check whether certain cells are addressed and have certain values then runs another macro. The following code is used for this:
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Range("K10:K1000")) Is Nothing And Target.Value = "Trapezoidal roof 0.6mm and above" Or Target.Value = "LightBox ballasted" Then
Application.ScreenUpdating = False
Call PPAPricePerkWp
End If
End Sub
This works fine on it's own.
The second macro is run when a button is clicked. This macro copies and pastes cells/rows to other parts of the spreadsheet.
When the macro is run I get the error Runtime error 7 - out of memory and it breaks on the above bit of code.
Is there another way I can check whether cells in a certain column are addressed and have certain values and won't lead to the above error?
you might want to disable events before you call your subroutine, so that the Worksheet_Change is not being triggered every time you change a cell
Application.EnableEvents = False
Don't forget to turn it back on when you are finished