Making Excel functions affect 'other' cells - excel

Let's say that I create a Sub (not a function) whose mission in life is to take the active cell (i.e. Selection) and set an adjacent cell to some value. This works fine.
When you try to convert that Sub to a Function and try to evaluate it from from spreadsheet (i.e. setting it's formula to "=MyFunction()") Excel will bark at the fact that you are trying to affect the value of the non-active cell, and simply force the function to return #VALUE without touching the adjacent cell.
Is it possible to turn off this protective behavior? If not, what's a good way to get around it? I am looking for something a competent developer could accomplish over a 1-2 week period, if possible.
Regards,
Alan.
Note: I am using 2002, so I would favor a solution that would work for that version. Having that said, if future versions make this significantly easier, I'd like to know about it too.

It can't be done, which makes sense because:
When a worksheet function is called, the cell containing the function is not necessarily the active cell. So you can't find the adjacent cell reliably.
When Excel is recalculating a worksheet, it needs to maintain dependencies between cells. So it can't allow worksheet functions to arbitrarily modify other cells.
The best you can do is one of:
Handle the SheetChange event. If a cell containing your function is changing, modify the adjacent cell.
Put a worksheet function in the adjacent cell to return the value you want.
Update
Regarding the comment: "I'd like this function to work on a 'blank' spreadsheet, so I can't really rely on the SelectionChange event of spreadsheets that may not yet exist, but will need to call this function":
Can you put your function in an XLA add-in? Then your XLA add-in can handle the Application SheetChange (*) event for all workbooks that are opened in that instance of Excel?
Regarding the comment: "Still, if you keep Excel at CalculationMode = xlManual and fill in just values, you should be just fine"
Even when CalculationMode is xlManual, Excel needs to maintain a dependency tree of references between cells so that it can calculate in the right order. And if one of the functions can update an arbitrary cell, this will mess up the order. Which is presumably why Excel imposes this restriction.
(*) I originally wrote SelectionChange above, corrected now - of course the correct event is SheetChange for the Workbook or Application objects, or Change for the Worksheet object.
Update 2
Some remarks on AlanR's post describing how to 'kinda' make it work using a timer:
It's not clear how the timer function ("Woohoo") will know which cells to update. You have no information indicating which cell contains the formula that triggered the timer.
If the formula exists in more than one cell (in the same or different workbooks), then the UDF will be called multiple times during a recalculation, overwriting the timerId. As a result, you will fail to destroy the timer reliably, and will leak Windows resources.

According to How to Create Custom User Defined Excel Functions:
Limitations of UDF's
Cannot place a value in a cell other than the cell (or range) containing
the formula. In other words, UDF's are
meant to be used as "formulas", not
necessarily "macros".
So, it looks like it cannot be done.

I'm using Excel 2007, and it does not work. Excel mentions it creates a circular reference. I don't think you can alter other cells from a function, just return a value.
It's kind of functional programming, no side effects. If you could just alter other cells inside a function (used from a worksheet), then there's no way for Excel to know the order and what to recalculate if a cell changes.
This article also contains a lot of information about how Excel does recalculation. But it never states that the other cells are frozen.
I don't know what you are trying to do, but, why don't you just place another function in the adjacent cell, that takes the first cell as a parameter?
Example:
Public Function Bar(r As Range) As Integer
If r.Value = 2 Then
Bar = 0
Else
Bar = 128
End If
End Function

Thank you all for responding. It is possible to do this! Kinda. I say 'kinda' because technically speaking the 'function' isn't affecting the cells around it. Practically speaking, however, no user could tell the difference.
The trick is to use a Win32 API to start a timer, and as soon as it goes off you do what you want to to whatever cell and turn off the timer.
Now I'm not an expert on how COM threading works (although I know VBA is Single Apartment Threaded), but be careful about your Timer running away with your Excel process and crashing it. This is really not something I would suggest as a solution to every other spreadsheet.
Just Make a Module with these contents:
Option Explicit
Declare Function SetTimer Lib "user32" (ByVal HWnd As Long, _
ByVal IDEvent As Long, ByVal mSec As Long, _
ByVal CallFunc As Long) As Long
Declare Function KillTimer Lib "user32" (ByVal HWnd As Long, _
ByVal timerId As Long) As Long
Private timerId As Long
Private wb As Workbook
Private rangeName As String
Private blnFinished As Boolean
Public Sub RunTimer()
timerId = SetTimer(0, 0, 10, AddressOf Woohoo)
End Sub
Public Sub Woohoo()
Dim i As Integer
' For i = 0 To ThisWorkbook.Names.Count - 1
' ThisWorkbook.Names(i).Delete
' Next
ThisWorkbook.Worksheets("Sheet1").Range("D8").Value = "Woohoo"
KillTimer 0, timerId
End Sub

While you can't do this in Excel, it's possible in Resolver One (although it's still a pretty odd thing to do).
It's a spreadsheet that allows you to define custom functions in Python that you can then call from a cell formula in the grid.
As an example of what you're asking, you might want to define a safeDivide function that (instead of raising a ZeroDivisionError) told you about the problem by colouring the denominator cell, and putting an error message beside it. You can define it like this:
def safeDivide(numerator, cellRange):
if not isinstance(cellRange, CellRange):
raise ValueError('denominator must be a cell range')
denominator = cellRange.Value
if denominator == 0:
cell = cellRange.TopLeft
cell.BackColor = Color.Red
cell.Offset(1, 0).Value = 'Tried to divide by zero'
return 0
return numerator / denominator
There's an extra wrinkle: functions that get passed cells just get passed the cell value, so to work around that we insist on being passed a one-cell cellrange for the denominator.
If you're trying to do unusual things with spreadsheets which don't quite fit into Excel, or you're interested in using the power of Python to work with your spreadsheet data, it's worth having a look at Resolver One.

Here's an easy VBA workaround that works. For this example, open a new Excel workbook and copy the following code into the code area for Sheet1 (not ThisWorkbook or a VBA Module). Then go into Sheet1 and put something into one of the upper-left cells of the worksheet. If you type a number and hit Enter, then the cell to the right will be updated with 4 times the number, and the cell background will become light-blue. Any other value causes the next cell to be cleared. Here's the code:
Dim busy As Boolean
Private Sub Worksheet_Change(ByVal Target As Range)
If busy Then Exit Sub
busy = True
If Target.Row <= 10 And Target.Column <= 10 Then
With Target.Offset(0, 1)
If IsNumeric(Target) Then
.Value = Target * 4
.Interior.Color = RGB(212, 212, 255)
Else
.Value = Empty
.Interior.ColorIndex = xlColorIndexNone
End If
End With
End If
busy = False
End Sub
The subroutine captures all cell change events in the sheet. If the row and column are both <= 10, then the cell to the right is set to 4 times the changed cell if the value is numeric; otherwise the cell to the right is cleared.

Related

VBA - Change cell background color

I've been building a team vacation calendar in Excel (Office 365 version) and I'm using VBA for the first time to automate some calculations and styling.
I've been stuck on the following:
I want to create a function that changes the background color of a cell.
I have four colors to switch between so I'd rather make four functions, one per color.
That function will then be called within different functions when needed.
I don't want to use ColorIndex, but rather a custom color (I can use RGB or the Long value), but I can't get the ColorIndex to work either.
My assumption is that the problem lies with the range but at this point, who knows :D.
The long values of each color are stored within a self-made Enum "OwnColorLong".
Here are some of my tries, every time the result in my Excel sheet (when running as a formula) is "#Value!".
'Function SetBackgroundToRed(RangeToChange As Range)
' Dim ColorIWant As Long
' ColorIWant = OwnColorLong.Red
' RangeToChange.Interior.Color = ColorIWant
'End Function
'Sub SetColorToRed(RangeToChange As Range)
' RangeToChange.Select
' With Selection.Interior
' .ColorIndex = 3
' End With
'End Sub
'
Function SetBackgroundToRed(RangeToChange As Range)
Dim MyRange As Range
Set MyRange = Worksheets("Vacation Calendar").Range("RangeToChange")
MyRange.Select
With Selection.Interior
.ColorIndex = 3
End With
End Function
I'm still a bit confused about when to use a sub or a function, or when to best use a class module. All code is now placed within one module, I'll be writing a Main sub linked to a button and putting all the code in there except for the functions themselves. If there are better practices, feel free to let me know.
This won't work as a UDF called from a worksheet cell. Except for some edge cases, e.g. this, UDFs called from a cell can't modify other cells on the worksheet.
Functions can perform a calculation that returns either a value or text to the cell that they are entered in. Any environmental changes should be made through the use of a Visual Basic subroutine.
Prefer Sub to Function since this does something and doesn't return anything.
"I have four colors to switch between so I'd rather make four functions, one per color" - better to make one function and pass a color parameter.
Private Sub SetColor(ByVal RangeToChange As Range, ByVal Color As Long)
RangeToChange.Interior.Color = Color
End Sub
Called like
SetColor yourRange, OwnColorLong.Red
Second, Sub or Function? If you need an answer from your method, then Function it is. In your case, you need no answer, so it is Sub.
Sub SetBackgroundToRed(RangeToChange As Range)
With RangeToChange.Interior
.ColorIndex = 3
End With
End Sub
I got this to work eventually!
Dim rng As String
rng = "A1"
Range(rng).Interior.Color = OwnColorLong.Red
The problem was not knowing how to pass a range as a variable, I had to use String apparently, not Range.
Thanks everyone for the help!

Combine new dynamic array features of excel with VBA

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.

Excel Macro event not working after saving data

My macro function is :
Function IsColouredCell(Range As Range, Colour As Range) As Boolean
If Range.Interior.Color = Colour.Interior.Color Then IsColouredCell = True
End Function
And event code is:
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
ActiveSheet.Calculate
End Sub
Use case: To check color match of target & reference cell.
Above code is active till the time i use it just after writing code in vba for n number of entries in excel. But as soon as i save the data on a already saved macro file. my event activation doesn't work and nothing happens on changing the color of a a cell.
Please provide the solution.
You cannot declare a variable name Range in Range As Range. That is very confusing and might mess up the code. Never use reserved words as variable name. All names in VBA should at best be unique.
If this is used as UDF (user defined function) like a formula you probably need to make it volatile, because it only re-calculates on ActiveSheet.Calculate if any dependant value changed but not if a cells color changed:
So it should look something like below:
Function IsColouredCell(ByVal MyRange As Range, ByVal Colour As Range) As Boolean
Application.Volatile
If MyRange.Interior.Color = Colour.Interior.Color Then IsColouredCell = True
End Function
If you use that like =IsColouredCell(A1,B1) and it is not volatile the formula only re-calculates if the value of A1 or B1 changed. But it will not re-calculate if just their color changed, even not on ActiveSheet.Calculate.
Note that making a function volatile means that it is calculated everytime Excel needs to calculate something. So using volatile functions a lot makes your worksheet slower the more you use that function.

Excel VBA UDF Returns #VALUE Error When Adding Or Deleting Unrelated Sheet

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

Macro added PrevSheet function returns #NAME error

I added a simple Previous Sheet macro to file and it worked normally, until it started to return #NAME error, and i can't figure out why.
I meant to use it with named single cell ranges which are consistent across the workbook, I'd used it successfully before naming the ranges and didn't think it would have any impact on the fuction. After naming the ranges though it no longer works, not even for regular non named ranges.
I have tested this by creating a new workbook, filling some sheets and trying it out, and it still returns a #NAME error. When i evaluate the function, the error appears at the very first step: recognizing the function. However, when i type into the formula bar, the programs offers me the formula normally.
I have also tried referring to the named cells by its cell, and even adding the worksheet name before the cell (eg "prevsheet(previoussheetname!a1), or prevsheet(thissheetname!a1)). I have even, in a last ditch effort, tried adding double quotes before the cell name.
For full disclosure, i have also another macro subroutine that uses references to previous and next sheets, but as it wouldnt recognize the function itself (which should have been an early sign), it makes use of relative referencing (ie activesheet(index - 1, activesheet(index + 1)). At the time i didn't think it would mess up the function, but as i grow ever more desperate and confused, maybe thats a possibility.
the PrevSheet() code i was using:
Function PrevSheet(RCell As Range)
Dim xIndex As Long
Application.Volatile
xIndex = RCell.Worksheet.Index
If xIndex > 1 Then _
PrevSheet = Worksheets(xIndex - 1).Range(RCell.Address)
End Function
And as it is now, as suggested by Chris Neilsen
Function PrevSheet(RCell As Range) As Variant
Application.Volatile
PrevSheet = RCell.Worksheet.Previous.Range(RCell.Address).Value
End Function
As suggested by Chris Neilsen i have edited the named ranges to look like this:
!(nothing)$column$row with its scope set to Workbook
The named range is not available at the range browser.
Only cell B1 is named. It is called "name"
PrevSheet() does not work with either range.
Macros are enabled
Anyone with a better understanding of vba, macros and excel can tell me why this is happening and how do i fix it so it returns the value of the specified cell in the first sheet to the left of sheet the function is typed in? (ie, in sheet4, =prevsheet(A1) will return the value of cell A1 in sheet3)
I hope my question is clearer now!
Your code appears to work if it is placed in a Standard Module:
Public Function PrevSheet(RCell As Range) As Variant
Dim xIndex As Long
Application.Volatile
xIndex = RCell.Worksheet.Index
MsgBox xIndex
If xIndex > 1 Then
PrevSheet = Worksheets(xIndex - 1).Range(RCell.Address)
End If
End Function
For example, in the worksheet:
I have assigned cell A7 the Name junk and the 666 is the value in the previous sheet's cell A7.
This will work, if you define your Named Ranges correctly. There are several ways this can be done, but here's one that is IMO simplest.
Since you say ...use it with named single cell ranges which are consistent across the workbook. you can create a single Named Range, Workbook scope, that will refer to a cell (or cells) on the sheet which references the name.
Lets say you want to refer to cell A1. In Name Manager, create a Name, lets say YourNamedRange workbook scope, Reference =!$A$1 (Note the ! without a sheet reference).
When you add a formula to a sheet (eg =YourNamedRange) it will refer to cell A1 on the sheet containing the formula.
Applying it to your UDF, just use =PrevSheet(YourNamedRange)
Your UDF works (mostly) as is, but will fail if a different Workbook is active. To fix that, use
Function PrevSheet(RCell As Range)
Dim xIndex As Long
Application.Volatile
xIndex = RCell.Worksheet.Index
If xIndex > 1 Then
With RCell.Worksheet.Parent 'The workbook containing RCell
PrevSheet = .Worksheets(xIndex - 1).Range(RCell.Address)
End With
End If
End Function
There is also a WorksheetProperty called Previous that does much the same thing, so you can refactor as
Function PrevSheet(RCell As Range) As Variant
Application.Volatile
PrevSheet = RCell.Worksheet.Previous.Range(RCell.Address).Value
End Function

Resources