Sub TurnAutoFilterOn()
'check for filter, turn on if none exists
If Not ActiveSheet.AutoFilterMode Then
ActiveSheet.Range("A1").AutoFilter
End If
End Sub
Works well and turns on the AutoFilter.
Function Req(ByVal MCode As String) As Integer
TurnAutoFilterOn
End Function
Doesn't work.
Function Req(ByVal MCode As String) As Integer
'check for filter, turn on if none exists
If Not ActiveSheet.AutoFilterMode Then
ActiveSheet.Range("A1").AutoFilter
End If
End Function
Doesn't work.
Is excel vba autofilters supposed to be working only under SUBs and not in Functions?
The above commenters are right regarding updating the workbook (i.e. any cells) from a function invoked by a cell - it is not allowed/supported.
Excel provides a workbook reclaculation model in which it can precompute inter-cell dependencies based on the cell formulas. This allows for a (relatively) efficient propagation of changes from their original sources to the cells that depend upon them. It propagates changes repeatedly (i.e. recursively) until they've been propagated to cells that are not referenced in other formulas, when the workbook relaculation is completed. It does NOT allow cell formulas to modify any cells in the workbook; if it were to support that it would effectively invalidate (or at least dramatically weaken) the pre-computed formula-based dependency analysis, and require another calculation model (that would likely much less efficient). (Circular cell references (direct or indirectly) are also problematic for this method, which makes history functions a bit tricky.)
However, what you can do is record some data in a VBA data structure to save for later use (in the example below, the very simple public gx, but such data structure can by almost of any complexity). You can then use that recorded data after a workbook recalculation using events. The Worksheet Change Event is way to run some code after the calculation (you write subroutine Worksheet_Calculate and put it in the worksheet), at a time when it will be OK to modify the cells. There is also a Workbook_SheetCalculation which goes in the code for ThisWorkbook, which might be of interest.
In "ThisWorkbook":
Private Sub Workbook_SheetCalculate(ByVal Sh As Object)
MsgBox "gx=" & gx
Application.Worksheets("Sheet1").Range("A1") = gx
End Sub
In "Module1":
Public gx As Long
Function MyFormula(x As Long) As String
gx = x
MyFormula = "hello"
End Function
In Sheet1, cell A5:
=MyFormula(A4)
You'll get a pop up in a context where gx was set to the number in A4 (showing the storing of data from the run of the formula), and, modifying a worksheet as well. Now, modify A4 with another number and you'll see the results because changing A4 triggers a recalculation.
(Note that you may also be interested in the Workbook_SheetChange event as an alternative to the SheetCalculate event.)
Related
I have an excel table where I'm using my custom made functions, made in VBA. I have to mention that I am using these functions inside cells, not in a macro. Everything works just fine, however, I am noticing a dramatic slowdown in calculation time. To be honest, I am using my function in about 200 cells, but still I don't see how it can reason such a memory hungry spreadsheet. Also my 2 functions are very basic, one is a plain vanilla isExist(value) search in a 5 cells range (no binary or other fast search algos). The other function is a modified dateDiff, so I can use it as a cell function (for some reason dateDiff does not show as a cell formula). Anyways, can someone tell me why a vba function would be so memory hungry when used in cell as a formula, and how can I optimize this?
Function existsInArray(array_to_search As range, value_to_exist As String) As Boolean
For Each value In array_to_search
If value_to_exist = value Then
existsInArray = True
Exit Function
End If
Next
existsInArray = False
End Function
Function dayOfTheYear(begining_of_year_date As Date, to_date As Date) As Integer
dayOfTheYear = CInt(DateDiff("d", begining_of_year_date, to_date)) + 1
End Function
I can't analyze completely without seeing how the UDFs are being used, but having code loop over ranges can be very slow. For example:
Public Function MyUdf(rng As Range) As Variant
Dim r As Range
For Each r In rng
' do something that calculates MyUdf
MyUdf = 1
Next r
End Function
will examine and process each cell in rng. If the user puts something like:
=MyUdf(A:A)
in a cell, it will process every cell in the entire column.
To limit the extent of the looping, you can use something like:
Public Function MyUdf2(rng As Range) As Variant
Dim r As Range, RNG2 As Range
Set RNG2 = Intersect(rng, rng.Parent.UsedRange)
For Each r In RNG2
' do something that calculates MyUdf
MyUdf2 = 1
Next r
End Function
That way you may end up processing thousands of cells rather than millions.
Anther possible speedup technique is to create VBA arrays to process the data rather than use cells directly.
Well, by default, UDF's (User Defined Functions) in Excel VBA are not volatile. They are only recalculated when any of the function's arguments change. A volatile function will be recalculated whenever calculation occurs in any cells on the worksheet.
Turn Off Automatic Calculation
vba performanceSet the Calculation mode to xlCalculationManual so that no calculations are carried out within the Excel Workbook until the Calculation mode is changed back to xlCalculationAutomatic or by running Application.Calculate:
Application.Calculation = xlCalculationManual
Turn Off Screen Updating
vba performancePrevent Excel from updating the Excel screen until this option is changed to True:
Application.ScreenUpdating = False
The XLSB format vs. XLSM
vba performanceA known way to improve Excel VBA speed and efficiency, especially fore large Excel files, is to save your Workbooks in binary XLSB format.
I tried Workbook_SheetCalculate Event and tried to trigger it, but it did not work, although I recalculated the worksheet!
How to trigger this Event?
here is an example, in the worksheet for the event have the following code:
Private Sub Worksheet_Calculate()
MsgBox "Calculating"
End Sub
Then in the sheet, in any cell, enter =RAND()
The formula causes a recalculation and triggers the event.
Or from a standard module use the following:
Public Sub Test()
'Application.Calculate ''could use this event for the workbook
With Worksheets("Sheet5") 'sheet containing the event code
.Calculate
End With
End Sub
The key seems to be that there is something in the sheet to calculate e.g. =RAND().
I remembered from another post, at some point, a link to the following Excel’s Smart Recalculation Engine
A quick extract says:
Excel normally only calculates the minimum number of cells possible.
Excel’s smart recalculation engine normally minimises calculation
time by tracking changes and only recalculating
Cells, formulae, values or names that have changed or are flagged as needing recalculation.
Cells dependent on other cells, formulae, names or values that need recalculation.
So, if you just had constants in the sheet, even if you issue a Worksheet.Calculate the msgbox wouldn't appear. You could test this by removing the =RAND() from the sheet and just putting 1 in the cell.
If I have two sheets each with a single non-volatile formula, and this in the workbook module:
Private Sub Workbook_SheetCalculate(ByVal Sh As Object)
Debug.Print Sh.Name
End Sub
I see both sheets names on calling:
Application.CalculateFull
or:
Application.CalculateFullRebuild
but no output with:
Application.Calculate
If I add a volatile formula to one of the sheets then I get that sheet when calling Application.Calculate.
If you're still having problems then you'd need to post a few more details including your event code and what types of formulas you have on your sheets.
I tried to use Solver on a working sheet, say sheet A. I also have some cells with "=rand()" on sheet A, or say any formula on the sheet using custom functions with Application.Volatile written inside. I'd like these volatile cells to stop recalculate when I am doing solver, so I used Application.Calculation = xlCalculationManual in my Solver program, but turned out after I ran the Solver, I found that those volatile cells have changed. Where have I done wrong?
There are 2 ways to avoid this:
Do not use solver! If you write an own sub to do the stuff you can use Range.Calculate while the calculation is manual to just calculate this cells (all other cells will be ignored).
Change the formulas and go with iteration options -> formulas -> enable iterative calculation. Now use a helper-cell which holds true/false (or 1/0 or whatever) and the change the formulas you do not want to calculate as follows(helper-cell A1 / formula A2):
=IF(A1,A2,[your old formula])
This way, as long as A1 is true it will output the value from before the calculation.
As far as I know, there is no other way, because solver does a Calculate each time a new cycle is tested.
EDIT:
Having volatile UDF which do not need to be volatile the whole time, you can use a simple trick (again a helper-cell) is needed:
A1: [volatile function like =NOW() or =RAND()]
A2: =MY_UDF(A1)
In Module:
Public Function MY_UDF(a As Variant) As Double
a = a
MY_UDF = Now
End Function
As long as A1 holds something volatile, your UDF will recalculate every time. But if you empty out A1 (or make it at leas non-volatile) there will be no change anymore to the value submitted to your UDF and this way excel/vba assumes that also no change will happen and just skip it for recalculation. This way you also can build up your own RAND-UDF (with different name of course) to just stop ALL volatile functions in your workbook as long as your helper-cell is non-volatile.
As a note: after making A1 (in this example) non-volatile, the first calculation afterwards will still run 1 time like it is volatile. Also changing A1 from one value to another (like 0 to 1) will run one time.
Here is a workaround that might help if you want to have random values in a spreadsheet that can be recalculated at will and whose volatility can be turned on and off.
First, create a 1-cell named range, say "trigger"
Then, put the following code in a standard code module:
Function SemiVolatileRand(x As Variant) As Double
SemiVolatileRand = x - x + Rnd()
End Function
Sub ReSample()
Randomize
Range("trigger").Value = Range("trigger").Value + 0.01
End Sub
Attach the ReSample sub to a button. Replace all occurrences of =RAND() by
=SemiVolatileRand(trigger). They will recalulcate whenever the button is pressed. Also, if you ever want to turn full volatility back on, just put the formula =RAND() in the trigger cell. (Getting full volatility in this last case seemed to require that my code does something with the dummy variable x, hence the somewhat poinless x - x).
Randomize reseeds the random number generator from the system clock. It should be called at least once per session. If you don't, then each time you open an Excel workbook which uses VBA rnd, you will get the same sequence of random values. You can verify this by making a blank workbook and in the ThisWorkbook code module put:
Private Sub Workbook_Open()
MsgBox Rnd()
End Sub
The message block will display the same value each time you open the workbook. On the other hand if you put the line Randomize before the MsgBox then you will get different values each time you open it.
Note that the workbook open event is a natural place to put the statement Randomize if you are planning to use rnd.
The reason I didn't put Randomize in the function itself was both to save CPU cycles and also because of a nagging concern that a certain percentage of the time you will be reseeding the random number generator with exactly the same system time. That might be impossible with modern architectures running recent versions of Excel, but e.g. does sometimes happen if you had Randomize Timer (which you sometimes encounter when reading other's code) since the timer function has 1 millisecond resolution irrespective of the system clock.
The code I have does have the drawback that if you bypass the button and just change the trigger cell then you could miss the reseeding. If this is a concern, 1 possibility would be like this:
Public Initialized as Boolean
Function SemiVolatileRand(x As Variant) As Double
If Not Initialized Then
Randomize
Initialized = True
End If
SemiVolatileRand = x - x + Rnd()
End Function
This will prevent the function from running if rnd isn't properly seeded.
Excel itself takes care of seeding automatically with the worksheet function Rand(), so it is strictly a VBA complication.
thanks in advance for any clarity you can offer.
In an Excel Workbook with many modules and worksheets, at the bottom of the VBA code for SHEET2, there is this Subroutine:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim TargetCells As Range
Set TargetCells = Range("B1000:B1029")
If Not Application.Intersect(TargetCells, Range(Target.Address)) Is Nothing Then
Call SpecificSubRoutine
End If
End Sub
My understanding of this code is that it watches the entire sheet for ANY changes. If ANYTHING is changed, anywhere on the sheet, it runs the If statement. The If statement fails in the event that any of the changes made to the sheet take place outside of the specified TargetCells range, but this Sub still tries to validate the If statement EVERY time ANYTHING is changed on the sheet.
Now, you might be able to guess that my problem is some stack overflow. (Run-time error '28': Out of Stack Space)
Whenever the Worksheet_Change Sub runs, if the changes to the sheet were made inside of the TargetCells range, it calls SpecificSubRoutine which populates cells, which triggers the Worksheet_Change Sub for every time SpecificSubRoutine populates ANY cell. (SpecificSubRoutine also calls different modules, which of course populate cells, which of course trigger the Worksheet_Change Sub)
Not so good.
Also, most of the subroutines throughout the application are wrapped in Application.ScreenUpdating = False / Application.ScreenUpdating = True, which I mistakenly thought would limit the number of times Worksheet_Change is called to once, immediately after Application.ScreenUpdating = True runs.
NOTE OF IMPORTANCE: Neither SpecificSubRoutine nor any of the Subroutines called by it populate cells in the TargetCells range. I'm not quite that dim...
Here are my questions:
Is there a way to narrow the scope of what triggers the Worksheet_Change Sub, so that only changes in the TargetCells range triggers it? (instead of changes anywhere in the sheet)
Is there a way to do what I mistakenly thought that Application.ScreenUpdating would do? (make changes to the sheet all in one bulk update, as opposed to triggering a change with nearly every step)
Also, as an extra curiosity, is there a way to have Worksheet_Change watch 2 specific ranges (instead of the whole sheet?) Knowing how to do this would be paramount, and would likely solve all of the problems on this sheet.
My intuition is to add an End to the last part of SpecificSubRoutine, or to the end of any/all of the Subroutines called by it, but I'm just not sure this will circumvent the looping through Worksheet_Change multiple times, since Application.ScreenUpdating doesn't bulk update like I thought.
Ideas?
Part 1: No - the event handler responds to all changes on the sheet: any filtering in how you respond to that change must occur in the handler itself.
Part 2: answered by #simoco
Part 3 (and incorporating simoco's suggestion):
Private Sub Worksheet_Change(ByVal Target As Range)
Application.EnableEvents=False
If Not Application.Intersect(Me.Range("B1000:B1029"), Target) Is Nothing Then
Call SpecificSubRoutine
End If
If Not Application.Intersect(Me.Range("D1000:D1029"), Target) Is Nothing Then
Call SomeOtherSpecificSubRoutine
End If
Application.EnableEvents=True
End Sub
As far as i know, Worksheet_Calculate is called when the value of any cell in a worksheet changes on Formula recalculation.
Is there a way so that i need a function to be called only when a specific cell changes on Formula Recalculation
To make something happen when a specific cell is changed, you need to embed the relevant selection change event within the file Worksheet_Change(byval target as Range). We can re-calculate a worksheet when your cell changes as follows:
Private Sub Worksheet_Change(byval target as range)
If target.address = Range("YourCell").Address Then Application.Calculate
End Sub
Now what you want to do is switch off calculations the rest of the time. If you only want to switch off calculations on the single sheet (and not your whole file), you will need to turn calculations off when it is activated, and on when deactivated e.g.
Private Sub Worksheet_Activate
Application.Calculation = xlCalculationManual
End Sub
Private Sub Worksheet_Deactivate
Application.Calculation = xlCalculationAutomatic
End Sub
Of course, your requirements to re-calculate may be considerably more complex than the example above. Firstly, you may open the file whilst on the sheet in question in which case you should use the Workbook_Open event to detect your sheet, and set calculations accordingly
Then you may have several cells that may require some sort of calculation. Presumably the reason you want to switch off calculations is that the file is running too slowly. If so, and you are identifying all the input cells, you could enter the outputs using code. One method would be to enter the formulas using this guide to entering formulas in Excel Visual Basic. You could then replace the formula with the calculated value e.g. Range("YourCell") = Range("YourCell").Value...so stopping the number of formulas in the sheet constantly growing
Let me see if I interpret your question correctly. You want to know if it is possible to only kickoff a macro if a particular cell (or group of cells) is changed.
The answer is Yes. To tweak Ed's code a little.
Private Sub Worksheet_Change(byval target as range)
If Not Intersect(target.address, Range("YourCells")) is Nothing Then
MyMacro()
End If
End Sub
I think your use of "function" is throwing people off. That's why Ed's answer is so elaborate.
Ok. It is possible that you stated your question correctly and you just want to gain efficiency. In that case, Ed's answer solves your immediate problem, but will cause the spreadsheet NOT to calculate automatically when you change other cells.