I tried to work a bit more with dynamic arrays in excel in combination with vba. My problem is that I cant return a table-column with vba. Here a minimal example of what I want to do:
I have two Tables TabFeb and TabMar (see image below). Each of them has a column costs which I want to sum up individually. The results shall be put into a new Table. This can be easily done in excel with =SUM(TabFeb[Costs]) and =SUM(TabMar[Costs]), respectively. My idea is now to write a VBA function which takes a string as input, in this example it will be the month, and returns the table acording to the input. After that it will be summed up and the result is given in a cell.
I tried the following:
Function Selectmon(mon As String) As Range
If mon = "Feb" Then
Set Selectmon = Worksheets("Sheet1").ListObjects("TabFeb").ListColumns("Costs").DataBodyRange
ElseIf mon = "Mar" Then
Set Selectmon = Worksheets("Sheet1").ListObjects("TabMar").ListColumns("Costs").DataBodyRange
End If
End Function
The problem of this idea is that this function just copy the table data. Hence, if I would change the input table data the sum would not change. One has to recalculate every cell by hand. Somehow I need VBA to return TabFeb[Costs] for the input "Feb". Does anyone have an idea how this can be done?
Example
It's really just a one-liner (unless you want to do some in-function error checking)
Function Selectmon(mon As String) As Range
Set Selectmon = Range("Tab" & mon & "[Costs]")
End Function
As implied by #ceci, this formula will not update with changes in the table. Depending on other particulars of your worksheet, you can have it update either by
embedding it in a worksheet change event code;
or by adding the line Application.Volatile to the function itself.
The latter method will force a recalculation when anything changes on the worksheet that might cause a recalculation.
The first method can limit the recalculation only when there has been a change in the data, but has other limitations.
One of the limitations of the Worksheet Change method is that it will only work on the relevant worksheet.
If you use the Workbook sheet change method, you won't have that limitation.
In either event you can limit your code to run only when the table has changed.
Here is one generalized method:
Option Explicit
Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
Dim LOS As ListObjects
Dim LO As ListObject
Set LOS = Sh.ListObjects
For Each LO In LOS
'could select relevant tables here
'Could also select relevant worksheets, if you like
'for example
Select Case LO.Name
Case "TabFeb", "TabMar", "TabApr"
If Not Intersect(LO.DataBodyRange, Target) Is Nothing Then
Application.EnableEvents = False
Application.Calculate
End If
End Select
Next LO
Application.EnableEvents = True
End Sub
And there is other code you could use to find the relevant formula and just update that formula -- probably not worth the effort.
Related
I have a formula assigned to a name in excel which is slow to calculate.
Let's say it's this:
Name: ImportantItems
Scope: Workbook
RefersTo: =FILTER(A1:A10000, complexCondition(A1:A10000))
I have some VBA macros which run a simulation that modifies A1:A10000 on every iteration, however the simulation only needs to access the ImportantItems array every 100 iterations. If
=FILTER(A1:A10000, complexCondition(A1:A10000))
... was a normal formula in a cell, I know that Excel would observe its precedent (A1:A10000) had changed and trigger a recalc every iteration. However I'm hoping named ranges not referred to in the spreadsheet anywhere - only via VBA - will be calculated on demand. FWIW my VBA code is just
Dim items As Variant 'read fancy filtered array of stuff into 1D array
items = Application.Transpose(Sheet1.Range("ImportantItems").Value)
My alternative is to refactor and move the ImportantItems code into VBA so I can control when it is calculated. Application.Calculations = xlManual is not an option without sprinkling my code with ...
Anyway this isn't meant to be an A/B question, I'm just wondering how the calculation engine works as I can't find documentation on it, and it will influence future design decisions.
Your hope/assumption is correct. As long as the named range is not referred to in the workbook in any range (other named ranges don't count) then the formula in the named range is not recalculated when the source range is changed.
The reason it doesn't recalculate is because the named range is storing a formula string (see Name object .RefersTo in VBA). Excel propagates the change (in source) only if the named range is referenced from a range at which point it runs an Evaluate on the formula string. When the source is resized (cut/insert/delete cells), the named range formula string is updated but in essence it's still a string.
To test, create the following named range:
Name: TestCalc
Scope: Sheet1
Refers to: =NamedRangeWatch(Sheet1!$A$1:$A$5)
Then add the following code in a standard .bas VBA module:
Option Explicit
Public Function NamedRangeWatch(ByVal rng As Range) As Range
Application.Volatile False
Debug.Print "NamedRangeWatch was called at " & Now
Set NamedRangeWatch = rng
End Function
Sub Test()
Dim i As Long
For i = 1 To 10
Sheet1.Range("A1:A5").Value2 = i
If i Mod 5 = 0 Then
Debug.Print Application.Sum(Sheet1.Names("TestCalc").RefersToRange)
'Or
'Debug.Print Application.Sum(Sheet1.Range("TestCalc").Value)
End If
Next i
End Sub
and then run the Test method. You should see something like:
which shows that the function inside the named range only gets called twice although the source range was changed 10 times.
I have a number of cells which have validation lists that are based on the use of the INDIRECT function. In this case there are two variables - one is the currently selected State (as reference through the named range dtState) and another based on a separate selection already made by the user in a different cell. The function in the Validation List for the cell is:
=INDIRECT("dtHdrPlates"&dtSize&dtState)
dtsize : could equal "70" or "90"
dtState : couldequal "VIC" or "QLD"
Naturally there is a named range for all combination of "dtHdrPlates" and the possible options of dtSize and dtState. The INDIRECT function in the Cell Validation list gives me a list that changes based on the selections in other cells.
dtHdrPlates70VIC
dtHdrPlates90VIC
dtHrdPlates70QLD
dthrdPlates90QLD
This works well thanks to advice previously obtained in this forum - thanks!
I am now trying to implement the use of a Combo Box over top of this cell, positioned, populated, made visible, activated and if relevant, dropped down by VBA Code; and removed once the use leaves the control.
Where I'm currently stuck is populating the ListFillRange of the ComboBox in the sheet - based on the INDIRECT function in the Validation.Formula1 of the cell the combo box is intended to supplant.
Sub BuildComboBox(rngCell as Range)
' This procedure checks to see if the Cell concerned is a list, and if so creates a combobox over top of the cell with the appropriate contents.
Dim rngCell as Range ' the cell concerned
Dim ws as Worksheet
Dim Dim cbo As OLEObject
If rngCell.Validation.Type = xlValidateList Then
Set ws = Excel.ActiveSheet
Set cbo = ActiveSheet.OLEObjects("ComboBox")
cbo.Left = rngCell.Left
cbo.Top = rngCell.Top
cbo.Width = rngCell.Width
cbo.Height = rngCell.Height
CBO.LISTFILLRANGE = RNGCELL.VALIDATION.FORMULA1
cbo.LinkedCell = rngCell.Address
If rngCell.Validation.ShowError = True Then
CBO.MATCHREQUIRED = TRUE
Else
CBO.MATCHREQUIRED = FALSE
End If
cbo.Visible = True
cbo.Activate
If rngCell.Value2 = "" Then
CBO.DROPDOWN
End If
End If
End Sub
I am stuck in three places in the above code (where the code is in CAPITALS).
What I would like to solve at the moment is how to get the indirect function taken from the cell validation list (rngCell.Validation.Formula1) and have it converted to the named range acceptable to cbo.ListFillRange - at the moment it comes across as =INDIRECT("dtHdrPlates"&sglLBWWallThickness&dtState) which does not work.
I've been playing with the EVALUATE function but just can't get it to parse the text coming from the Validation.formula1
If while you're looking at this you could also steer me towards how to change the status of an ActiveX ComboBox's MatchRequire property - and how to force it to DropDown, that would be lovely too.
Thanks, Regards, Ken
First time posting, apologies if I make any mistakes!
So, I'm having a pretty strange problem with my UDF. In my workbook, I have an invisible 'template' sheet named "Standard Phase Sheet", and a subroutine that a user can activate which copies that template sheet into a new, visible sheet that the user can then work with. There will be many copies of that template sheet throughout the workbook, but they will all have unique names.
My UDF is on that template sheet in several spots, and thus on every copy of the template sheet that a user makes. When working within one of these sheets, the UDF works just fine, and returns the values I'd expect.
However, when a user ADDS a new copy of the template sheet, SOMETIMES the UDF goes haywire and returns #VALUE errors in every place the UDF is being used.
Also, when a user DELETES one of the copies of the template sheet, the UDF ALWAYS goes haywire and returns #VALUE errors in every place the UDF is being used.
I'm not using ActiveSheet or anything like that, and I believe I'm correctly giving full references to the ranges I'm working with within the UDF. Any help will be appreciated, I'm in a bind here! Code for the UDF is below.
Also, because I'm sure I'll be asked the question, the neColumn variable within my code is a public variable that I use in several subroutines and UDFs. It is defined at the beginning of my module. Also, I am using Option Explicit at the beginning of my module as well.
Thank you!
Public Function fSum(ByVal Target As Range, bExtended As Boolean) As Single
'This function returns a sum, based on a range provided in the cell that holds the function.
'It checks to see if that line item has been marked as Non-Extended, based on the NE column
'that can be check marked. If that line item is marked NE, then only the NE sum columns can
'use that line item as part of their sum, and those values are removed from the E columns.
Dim sSum As Single
Dim i As Integer
Dim n As Integer
'This small section is used to determine complete references to the cell calling the function.
Dim sheetName As String
sheetName = Application.Caller.Parent.Name
'Loop through provided range, and sum up the contents based on whether they have been marked NE or not.
i = 1
n = Target.row
sSum = 0
If Sheets(sheetName).Visible = True Then
While i < Target.Rows.Count
If (bExtended = True) Then
If Sheets(sheetName).Range(neColumn.Address).Cells(n, 1) = vbNullString Then
sSum = sSum + Sheets(sheetName).Range(Target.Address).Cells(i, 1).Value
End If
Else
If Sheets(sheetName).Range(neColumn.Address).Cells(n, 1) <> vbNullString Then
sSum = sSum + Sheets(sheetName).Range(Target.Address).Cells(i, 1).Value
End If
End If
i = i + 1
n = n + 1
Wend
End If
fSum = sSum
End Function
Summarizing the comment thread in an answer for posterity:
I'm not sure why exactly you see this behavior.
There would be ways to better this UDF (including using Long instead of Integer, preferring a Do While...Loop to While...Wend, removing the .Visible check...
But in any case, it does feel like this is just replicating the functionality of SUMIFS so you might just consider going that route.
The reason is that your neColumn variable has become Nothing, because Excel is Volatile.
I assume that the start of your module looks something like this:
Option Explicit
Public neColumn As Range
Sub Auto_Open()
Set neColumn = Sheet1.Range("A1:B2")
End Sub
When you open the Workbook, you call the Auto_Open Sub to Set the neColumn variable. However - when certain actions occur, Excel rebuilds the VBA, which resets the Public Variables (such as neColumn) to their defaults (which, for an Object such as a Range, is Nothing). An easy way to trigger this is by deliberately throwing an error, such as attempting to run this:
Sub ThrowErr()
NotDefined = 1
End Sub
You can make it more visible to you by adding the following line to your fSum code:
If neColumn Is Nothing Then Stop
You either need a way to restore neColumn when it has been reset to Nothing, OR find a non-volatile way to store it.
I am assuming that this is not suitable to become a Const, because otherwise it already would be but you could turn it into a Named Range, or store the Address in a hidden worksheet / CustomDocumentProperty. These options would also allow you to store neColumn when the Workbook is saved for when you reopen it
I have created a pivot table using vba. I need help with the understanding of below-mentioned points.
1: I want to select(copy) values with certain filters (eg: Underlying price
for Instrument Type = OPTCUR, Symbol = GBPUSD). Basically a VBA alternative for formula
GETPIVOTDATA("Underlying_price",$C$4,"Instrument Type","OPTCUR","Symbol","GBPUSD")
2: I want to set "show detail=True" without knowing cell details but the criteria as mentioned above.
3: when we set "show detail=True" a new sheet opens. i want to asign this sheet to a variable of type worksheet.
below is the SS of my pivot table. and TableName:="My_Pivot"
You get the relevant cell with PivotTable.GetPivotData
The new worksheet with details of this cell is shown by Range.ShowDetail = True.
Directly after that, the new ActiveSheet is the wanted one.
Here is a function to get the wanted worksheet with the details for a specified data field:
Private Function GetDetailSheet(pt As PivotTable, Val1 As String, Val2 As String) As Worksheet
Dim myCell As Range
With pt
Set myCell = .GetPivotData(.DataFields(1).Name, _
.RowFields(1).Name, Val1, _
.RowFields(2).Name, Val2)
End With
myCell.ShowDetail = True
Set GetDetailSheet = ActiveSheet
End Function
It can be used like this:
Private Sub Test()
Dim ws as Worksheet
Set ws = GetDetailSheet(ActiveSheet.PivotTables("My_Pivot"), "OPTCUR", "GBPUSD")
ws.Name = "Details OPTCUR GBPUSD"
End Sub
If you don't want to use the (hideos) GETPIVOTDATA there's a solution for you!
It's called CUBEVALUE. It's a bit difficult to master but once it's done you reports & Pivot Table get to a whole new level.
See here: https://www.excelcampus.com/cubevalue-formulas/
Yes, it's a long article, but it's definitely worth the effort, as it would enable you to point to a specific data point, not to a specific cell.
Once mastered, adding a VBA code to a "changed" cell event and changing the visibility status of a certain sheet is just a matter of minutes.
I am looking to detect any changes in an autofilter on a specific table, for the purpose of forcing a UDF that does some simple arithmetic on the table entries that are currently visible to update its result. Making the UDF volatile has had no impact so far, but it doesn't target the table directly either.
This solution (https://stackoverflow.com/a/15906275/4604845) does not work for me, probably because I have only manual calculation.
My a-little-too-dirty workaround is Workbook_SelectionChange, and while the UDF doesn't kill resource consumption this way, I'd rather avoid it in case the data grows larger. Also I have some users that are complete novices in terms of using computers, and I have trouble being confident that I can get all of them to understand that they need to click something else after updating the autofilter for the result to be correct.
Any ideas? I have tried Workbook_Change and Workbook_Calculate but none of them are triggered (or I can't figure out how to trigger them) by the autofilter changes.
Even if you have no other formulas on the worksheet, if you include a Subtotal() formula somewhere on the sheet referencing the table, the Subtotal() will recalculate every time the autofilter is changed.
You can use this to trigger a Calculate() event macro.
EDIT#1:
Say we have an AutoFilter set on column A of a sheet named data. Sheet data also contains many other formulas. If we use the Calculate() event in the data worksheet, we will get fires any time any of these formulas re-calculate.
We create a new worksheet called trigger. This new worksheet is comletely empty except for a single cell that contains:
=SUBTOTAL(3,data!A1:A20)
It is in the trigger worksheet that we place the Calculate() event macro.
Now if we are using the data worksheet, we can make arbitrary changes and perform various recalculation and nothing fires, but if we change the AutoFilter, the event macro on trigger will see the change and fire!
How to trigger Worksheet Calculation following AutoFilter changes while in Manual Calculation
As we know changes to the AutoFilter selection cannot be automatically detected, as these changes do not trigger any Workbook Event or Worksheet Event. Therefore, only option available is to have the user triggering worksheet calculations with an action i.e. Cell Selection Change, Right Click, Double Click, etc; or just by pressing [F9]; which is the preferable action as there is nothing else involved and it's the way it's designed to work.
Nevertheless, at the user request, I'm providing this VBA code that although needs to be initiated by user's action, this action can be done immediately after the AutoFilter change is selected, just by Double Clicking.
The DoubleClick can be unrestricted (double clicking any cell in the worksheet) by using this code:
Private Sub Worksheet_BeforeDoubleClick(ByVal rTrg As Range, blCancel As Boolean)
rTrg.Worksheet.Calculate
End Sub
or setting up to three types of restrictions:
Any cell of the Table
The Header of the Table
The Body of the Table
Use this code to restrict the DoubleClick area:
Currently the code is set to restriction type 1, use variable bRType to change it to the preferred type. This code assumed the name of the Table is Table1 (change as required)
Private Sub Worksheet_BeforeDoubleClick(ByVal rTrg As Range, blCancel As Boolean)
Dim ObjLst As ListObject, rTbl As Range, bRType As Byte
Rem Set Restriction Type
Rem 1: DoubleCliking any cell of the Table - Default
Rem 2: DoubleCliking any cell of the Table Header
Rem 3: DoubleCliking any cell of the Table Body
bRType = 1
With rTrg
Set ObjLst = .Worksheet.ListObjects("Table1")
Select Case bRType
Case 2: Set rTbl = ObjLst.HeaderRowRange
Case 3: Set rTbl = ObjLst.DataBodyRange
Case Else: Set rTbl = ObjLst.Range
End Select
If Not (Intersect(.Cells, rTbl) Is Nothing) Then
.Worksheet.Calculate
blCancel = True
End If
End With
End Sub
Both procedures are Worksheet Events, therefore ensure that the one that you decide to implement goes into the module of the worksheet holding the Table. (do not change the name of the procedures)