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!
Related
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.
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
im trying to work out a little scheduler in excel to manage my life a little better. I use different cell colors for each event (yellow for work, red for university, etc.). I already have a macro CountColor which counts the occurence of a color in a certain range which works fine (i pretty much copy pasted it from an online solution). I now use the macro to calculate the used time into cells like this:
the cells contain =ColorCount(H5;B2:F15) whereas the interior color of the first argument determines the color to count and the seconds parameter is the range to count the color in. This macro works fine. The last number is just the sum of the above three.
I now however face the problem, that changing the interior color of a cell does not trigger the recalculation of formulars. I created a simple button (not the ActiveX one) and assigned a macro to it:
Public Sub CalcButton_onclick()
Worksheets(1).Range("I13:I16").Calculate
End Sub
but when i click the button (i also tried to recalculate the whole sheet by using Worksheets(1).Calculate) nothing happens. Only when i, for example, change the value int the cell my times get recalculated. My button's macro is definitely executed i tested that by adding Worksheets(1).Cells(20, 20).Value = "Test" after the Calculate call and it changed the value of the given cell properly.
For the purpose of completion, i also add the code of the CountColor macro:
'counts the occurence of the interior color of rColor in rRange
Public Function ColorCount(ByRef rColor As Range, ByRef rRange As Range) As Integer
Dim rCell As Range
Dim lCol As Long
Dim vResult As Integer
vResult = 0
lCol = rColor.Interior.ColorIndex
For Each rCell In rRange
If rCell.Interior.ColorIndex = lCol Then
vResult = 1 + vResult
End If
Next rCell
ColorCount = vResult
End Function
Im not really sure what the problem is but i believe i may have misinterpreted the Calculate method. I only created the 2 macros above. I appriciate any help!
By the way something meta: is this a proper use of a picture in a question? I could not think of a better way to show what i want my output to look like.
Making your colorcount UDF volatile would help (add Application.Volatile) but as you have discovered changing the color or formatting of a cell does not trigger a recalculation so even a volatile UDF will not recalc just on a color change.
If you make your UDF volatile then Range.Calculate should trigger a recalc in Automatic calc mode.
I found the problem myself.
At first i want to thank everyone for their hints and tips. I would propably have needed them after fixing my initial problem and so i had them fixed in advance :)
My problem actually was apparently, that i wanted to declare my spare time as white color. But actually i had several cells which had "no fill color" after coloring every free cell explicitely white it now works with the button. The solution with Worksheet_Change() method in the sheet code did not work unfortunately because a color change is not evaluated as a change in the sheet. Worksheet_SelectionChange() however did the trick with updating when you click on another cell so i do not need the button anymore.
Background:
I need help adjusting a code I wrote by copy-pasting earlier. The goal of the function is to add a new row in a specified format whenever a target cell is clicked.
This was achieved via this code [excerpt only]:
Private Sub worksheet_selectionchange(ByVal target As Range)
If Not Intersect(target, Range("G6")) Is Nothing Then
Rows("7:7").Select
Selection.Insert Shift:=xlDown, CopyOrigin:=xlFormatFromLeftOrAbove
Range("B7:F7").Select
With Selection.Font
.ColorIndex = xlAutomatic
.TintAndShade = 0
End With
It also works fine so far. However now I want to add this functionality to the same tab again for another cell range. The problem I have now is, that since the relevant rows are stacked vertically, whenever I add a row via the original function, the ranges defined in the second routine now no longer work.
My Question:
Can I define ranges for the Intersect method dynamically? My idea would be something clunky like having the second method refer to a variable instead of a fixed cell (MyRange) which is changed automatically by the first routine (e.g. MyRange = MyRange + 1). How would I do that? Or are there any other ways to achieve what I want to do?
Your idea is on the right track - defining variables beforehand and not hardcoding values in your code is one of the building blocks of maintainable code. There are many ways to do this, including having Excel search for keywords, etc.
My suggestion for your case is that you look into the Name Manager within Excel, and define a new name which refers to all cells that you would like to be watched by your sub. If you do this, Excel will track those cells the same as it would if you were just typing formulas within cells. ie: if you put in cell C5: "=A5+B5", and you insert a new column to the left of column B, C5 will automatically now read "=A5+C5". In this way, your VBA code won't change, but the values within your defined Name will change.
Without knowing exactly how your sheet is setup, here's an example of how you could actually do this:
Private Sub worksheet_selectionchange(ByVal Target As Range)
Dim MyRange As Range
Dim CurrentRow As Integer
Dim FormattedArea As Range 'This will hold the area of your row which you want formatted
Const LeftColumn = 2 'this holds the left-most column of the area you want formatted...
Const RightColumn = 6 'these hardcoded numbers will need to be changed if you want to format a different number of columns; if you have a dynamic way of determining them that would be best
Set MyRange = Range("Possible_Areas") 'This makes MyRange = your defined, Excel-tracked name; you will need to go to the name manager in Excel and create it, listing all target cells you want included within it
CurrentRow = Target.Row 'This will be used to find where to insert the new row, based on the current selection
If Not Intersect(Target, MyRange) Is Nothing Then
Target.EntireRow.Insert 'Notice that I have removed your "selection" command - you can search this site for reasons on why ".Select" is problematic
Set FormattedArea = Range(Cells(CurrentRow, LeftColumn), Cells(CurrentRow, RightColumn))
With FormattedArea.Font
.ColorIndex = xlAutomatic
.TintAndShade = 0
End With
End If
End Sub
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.