Why is my conditional format offset when added by VBA? - vba

I was trying to add conditional formats like this:
If expression =($G5<>"") then make set interior green, use this for $A$5:$H$25.
Tried this, worked fine, as expected, then tried to adapt this as VBA-Code with following code, which is working, but not as expected:
With ActiveSheet.UsedRange.Offset(1)
.FormatConditions.Delete
'set used row range to green interior color, if "Erledigt Datum" is not empty
With .FormatConditions.Add(Type:=xlExpression, _
Formula1:="=($" & cstrDefaultProgressColumn & _
.row & "<>"""")")
.Interior.ColorIndex = 4
End With
End With
The Problem is, .row is providing the right row while in debug, however my added conditional-formula seems to be one or more rows off - depending on my solution for setting the row. So I am ending up with a conditional formatting, which has an offset to the row, which should have been formatted.
In the dialog it is then =($G6<>"") or G3 or G100310 or something like this. But not my desired G5.
Setting the row has to be dynamicall, because this is used to setup conditional formats on different worksheets, which can have their data starting at different rows.
I was suspecting my With arrangement, but it did not fix this problem.
edit: To be more specific, this is NOT a UsedRange problem, having the same trouble with this:
Dim rngData As Range
Set rngData = ActiveSheet.Range("A:H") 'ActiveSheet.UsedRange.Offset(1)
rngData.FormatConditions.Delete
With rngData.FormatConditions.Add(Type:=xlExpression, _
Formula1:="=($" & cstrDefaultProgressColumn & _
1 & "<>"""")")
.Interior.ColorIndex = 4
End With
My Data looks like this:
1 -> empty cells
2 -> empty cells
3 -> empty cells
4 -> TitleCols -> A;B;C;...;H
5 -> Data to TitleCols
. .
. .
. .
25
When I execute this edited code on Excel 2007 and lookup the formula in the conditional dialog it is =($G1048571<>"") - it should be =($G1<>""), then everything works fine.
Whats even more strange - this is an edited version of a fine working code, which used to add conditional formats for each row. But then I realized, that it's possible to write an expression, which formats a whole row or parts of it - thought this would be adapted in a minute, and now this ^^
edit: Additional task informations
I use conditional formatting here, because this functions shall setup a table to react on user input. So, if properly setup and a user edits some cell in my conditionalized column of this tabel, the corresponding row will turn green for the used range of rows.
Now, because there might be rows before the main header-row and there might be a various number of data-columns, and also the targeted column may change, I do of course use some specific informations.
To keep them minimal, I do use NamedRanges to determine the correct offset and to determine the correct DefaultProgessColumn.
GetTitleRow is used to determine the header-row by NamedRange or header-contents.
With ActiveSheet.UsedRange.Offset(GetTitleRow(ActiveSheet.UsedRange) - _
ActiveSheet.UsedRange.Rows(1).row + 1)
Corrected my Formula1, because I found the construct before not well formed.
Formula1:="=(" & Cells(.row, _
Range(strMatchCol1).Column).Address(RowAbsolute:=False) & _
"<>"""")"
strMatchCol1 - is the name of a range.

Got it, lol. Set the ActiveCell before doing the grunt work...
ActiveSheet.Range("A1").Activate
Excel is pulling its automagic range adjusting which is throwing off the formula when the FromatCondition is added.

The reason that Conditional Formatting and Data Validation exhibit this strange behavior is because the formulas they use are outside the normal calculation chain. They have to be so that you can refer to the active cell in the formula. If you're in G1, you can't type =G1="" because you'll create a circular reference. But in CF or DV, you can type that formula. Those formulas are disassociated with the current cell unlike real formulas.
When you enter a CF formula, it's always relative to the active cell. If, in CF, you make a formula
=ISBLANK($G2)
and you're in A5, Excel converts it to
=ISBLANK(R[-3]C7)
and when that gets put into the CF, it ends up being relative to the cell it's applied to. So in row 2, the formula comes out to
=ISBLANK($G655536)
(for Excel 2003). It offsets -3 rows and that wraps to the bottom of the spreadsheet.
You can use Application.ConvertFormula to make the formula relative to some other cell. If I'm in row 5 and the start of my range is in row 2, I make the formula relative to row 8. That way the R[-3] will put the formula in A5 as $G5 (three rows up from A8).
Sub test()
Dim cstrDefaultProgressColumn As String
Dim sFormula As String
cstrDefaultProgressColumn = "$G"
With ActiveSheet.UsedRange.Offset(1)
.FormatConditions.Delete
'set used row range to green interior color, if "Erledigt Datum" is not empty
'Build formula
sFormula = "=ISBLANK(" & cstrDefaultProgressColumn & .Row & ")"
'convert to r1c1
sFormula = Application.ConvertFormula(sFormula, xlA1, xlR1C1)
'convert to a1 and make relative
sFormula = Application.ConvertFormula(sFormula, xlR1C1, xlA1, , ActiveCell.Offset(ActiveCell.Row - .Cells(1).Row))
With .FormatConditions.Add(Type:=xlExpression, _
Formula1:=sFormula)
.Interior.ColorIndex = 4
End With
End With
End Sub
I only offset .Cells(1) row-wise because the column is absolute in this example. If both row and column are relative in your CF formula, you need more offsetting. Also, this only works if the active cell is below the first cell in your range. To make it more general purpose, you would have to determine where the activecell is relative to the range and offset appropriately. If the offset put you above row 1, you would need to code it so that it referred to a cell nearer the bottom of the total number of rows for your version of Excel.
If you thought selecting was a bit of a kludge, I'm sure you'll agree that this is worse. Even though I abhor unnecessary Selecting and Activating, Conditional Formatting and Data Validation are two places where it's a necessary evil.

A brief example:
Sub Format_Range()
Dim oRange As Range
Dim iRange_Rows As Integer
Dim iCnt As Integer
'First, create a named range manually in Excel (eg. "FORMAT_RANGE")
'In your case that would be range "$A$5:$H$25".
'You only need to do this once,
'through VBA you can afterwards dynamically adapt size + location at any time.
'If you don't feel comfortable with that, you can create headers
'and look for the headers dynamically in the sheet to retrieve
'their position dynamically too.
'Setting this range makes it independent
'from which sheet in the workbook is active
'No unnecessary .Activate is needed and certainly no hard coded "A1" cell.
'(which makes it more potentially subject to bugs later on)
Set oRange = ThisWorkbook.Names("FORMAT_RANGE").RefersToRange
iRange_Rows = oRange.Rows.Count
For iCnt = 1 To iRange_Rows
If oRange(iCnt, 1) <> oRange(iCnt, 2) Then
oRange(iCnt, 2).Interior.ColorIndex = 4
End If
Next iCnt
End Sub
Regarding my comments given on the other reply:
If you have to do this for many rows, it is definitely faster to load the the entire range into memory (an array) and check the conditions within the array, after which you do the writing on those cells that need to be written (formatted).
I could agree that this technique is not "necessary" in this case - however it is good practise because it is flexible for many (any type of) customizations afterwards and easier to debug (using the immediate / locals / watches window).
I'm not a fan of Offset although I don't state it doesn't work as it should and in some limited scenarios I could say that the chance for problems "could" be small: I experienced that some business users tend to use it constantly (here offset +3, there offset -3, then again -2, etc...); although it is easy to write, I can tell you it is hell to revise. It is also very often subject to bugs when changes are made by end users.
I am very much "for" the use of headers (although I'm also a fan of reducing database capabilities for Excel, because for many it results in avoiding Access), because it will allow you very much flexibility. Even when I used columns 1 and 2; better is it to retrieve the column nr dynamically based on the location of the named range of the header. If then another column is inserted, no bugs will appear.
Last but not least, it may sound exaggerated, but the last time, I used a class module with properties and functions to perform all retrievals of potential data within each sheet dynamically, perform checks on all bugs I could think of and some additional functions to execute specific tasks.
So if you need many types of data from a specific sheet, you can instantiate that class and have all the data at your disposal, accessible through defined functions. I haven't noticed anyone doing it so far, but it gives you few trouble despite a little bit more work (you can use the same principles again over and over).
Now I don't think that this is what you need; but there may come a day that you need to make large tools for end users who don't know how it works but will complain a lot about things because of something they might have done themselves (even when it's not your "fault"); it's good to keep this in mind.

Related

Copying columns including blanks without skipping rows..leave "blanks" blank VBA

Aplication Defined error Copying a specified column and range including blanks with an embedded button running multiple Macros. I know that all rows will be filled in column A so if I could reference the rest of the Macros to A.end
I've looked Google youtube and here although there is a lot of info on copying and pasting, I cannot find one that works for this running multiple Macros.
Macros 5 & 6 is where I start having problems because these columns have multiple blanks throughout.
Raw data to Copy:
Destination:
Private Sub CommandButton1_Click()
Worksheets("Sheet1").Range("a2", Range("a2").End(xlDown)).Copy _
Worksheets("Sheet2").Range("a2") 'macro1
Worksheets("Sheet1").Range("d2", Range("d2").End(xlDown)).Copy _
Worksheets("Sheet2").Range("b2") 'Macro2
Worksheets("Sheet1").Range("c2", Range("c2").End(xlDown)).Copy _
Worksheets("Sheet2").Range("c2") 'macro3
Worksheets("Sheet1").Range("g2", Range("g2").End(xlDown)).Copy _
Worksheets("Sheet2").Range("d2") 'macro4
If Worksheets("Sheet1").Range("e2", Range("e2").End(xlDown)).Value = "<0" Then
Worksheets("Sheet2").Range("i2").Copy 'macro5
If Worksheets("Sheet1").Range("e2", Range("e2").End(xlDown)).Value = ">0" Then
Worksheets("Sheet2").Range("j2").Copy 'macro6
Worksheets("Sheet2").Activate 'macro7
Range.end(xldown) only gets you a contiguous range (effectively it will stop at the first blank cell).
Since you want to include blanks, you might want to instead work from the last row of your worksheet back up to the first non-blank cell encountered in that column (which is a way of getting the last row).
This would mean something like:
' If you are new to With statements (below), any objects within the With block that begin with a . relate to "Sheet1". Saves us typing Sheet1 repeatedly, and makes sense to use it since we access a lot of Sheet1's members like range/cells/rows
With Worksheets("Sheet1")
.Range("a2", .cells(.rows.count, "A").End(xlup)).Copy Worksheets("Sheet2").Range("a2") 'macro1
End with
Untested, written on mobile -- but hope it works or gets you closer to a solution. You would need to copy-paste the above and change the A to B, C, D, E, etc. I wasn't too sure what you're trying to achieve with the "<0" condition in macro 5 and 6.
(It would better if you turned the code into a parameterised Sub and just provide the column letter/number as an argument to the sub, but just depends how new you are to VBA and programming in general -- and for the time being whatever is easier for you to understand/maintain.)
Edit regarding macro 5 and 6
With Worksheets("Sheet1")
Dim cell as range
For each cell in .Range("E2", .Cells(.Rows.Count, "E").End(xlUp))
If cell.Value <= 0 Then 'Get rid of the equal sign if you don't want it in your logic/condition'
Cell.Copy Worksheets("Sheet2").cells(cell.row, "I") 'Macro5
ElseIf cell.value > 0 Then
Cell.Copy Worksheets("Sheet2").cells(cell.row, "J") 'Macro6
End If
Next cell
End With
Worksheets("Sheet2").Activate 'macro7

VBA creating formulas referencing a range

After several hours of research, I still can't solve what seems to be a pretty simple issue. I'm new to VBA, so I will be as specific as possible in my question.
I'm working with a DDE link to get stock quotes. I have managed to work out most of the table, but I need a VBA to create a finished formula (i.e., without cell referencing) in order to the DDE link to work properly.
My first code is as follows:
Sub Create_Formulas()
Range("J1").Formula = "=Trade|Strike!" & Range("A1").Value
End Sub
Where J2 is the blank cell and A2 contains the stock ticker. It works fine, but when I try to fill out the rows 2 and bellow, it still uses A1 as a static value.
Sub Create_Formulas()
Dim test As Variant
ticker = Range("A1").Value
'Test to make variable change with each row
'Range("J1:J35").Formula = "=Trade|Strike!" & Range("A1:A35").Value
'not working
Range("J1:J35").Formula = "=Trade|Strike!" & ticker
'not working
End Sub
I couldn't find a way to solve that, and now I'm out of search queries to use, so I'm only opening a new topic after running out of ways to sort it by myself. Sorry if it is too simple.
You are referencing absolute cell adresses here. Like you would do when using $A$1 in a normal excel formula.
What you want to do is:
Dim row as Integer
For row = 1 to 35
Cells(row,10).Formula = "=Trade|Strike!" & Cells(row,1).Value
Next row
This will fill the range J1 to J35 with the formula. Since (row,10) indicates the intersection of row and column 10 (J)
Firstly, in your second set of code, you define a variable "test", but never give it a value.
You assign a value to the variable "ticker", and then never reference it.
Secondly, the value you have assigned to ticker is a static value, and will not change when it is entered in a different row.
Thirdly, I think your issue could be solved with a formula in Excel rather than VBA.
The "INDIRECT" function can be quite useful in situations like this.
Try inserting the formula
=INDIRECT("'Trade|Strike'!"&A1)
into cell A1, then copy down.
Note the ' ' marks around "Trade|Strike". This is Excels syntax for referencing other sheets.

Referring Range with Known Columns and Unknown Rows Excel VBA

How do you refer to a range where the number of columns is known but you don't know which row? What's the correct way of rendering Range("A&i:J&i")?
For i = 8 To WSData.Range("A8").End(xlDown).Row
If Cells(i, 1) = "Overall Totals:" Then
WSData.Range("A&i:J&i").Interior.Color = RGB(217, 217, 217)
End If
Next
Scott's answer is off course quite correct. However there are several other ways of referring to a variable range which you might find useful.
1) You could also use WSData.Range("A10", "J10"), i.e. you specify the top left and bottom right cells as two separate parameters. (The order of the paraneters doesn't actually matter!)
In your example, you would use: WSData.Range("A" & i , "J" & i)
2) I find using numbers, rather than letters for columns is useful, especially if your columns will be unknown in advance. The basic structure is as follows.
WSData.Range(Cells(1,10), Cells(10,10) 'A10 to J10)
or in your example
WSData.Range(Cells(1,i), Cells(10,i))
However one has to be careful! The default worksheet for the Cells range is the Active Worksheet. If this is not the same as the WSData, it will lead to a run time error. However, this can easily by avoided by specifying the worksheet to which the "Cells" belong:
WSData.Range(WSData.Cells(1,i), WSData.Cells(10,i))
This may look rather long-winded but it gives you complete flexibility in specifying your range as you can use variables for each of the cell parameters.

How to code Excel VBA equivalent of INDIRECT function?

I have many uses of the INDIRECT function in my workbook, and it is causing performance issues. I need to replace them with something that will give me the same results. All the INDIRECTS recalculate anytime anything is changed, causing the workbook to lag.
I was wondering if there is a way to code INDIRECT in VBA without actually using the INDIRECT function, and take away the volatility of the function in the code.
=INDIRECT("'" & $AC$9 & "'!" & AC26)
This is an example. I need to remove INDIRECT but get the same results for this cell. Is there a way to accomplish this in VBA?
You can try this.
Place the following routines in a standard code module:
Public Function INDIRECTVBA(ref_text As String)
INDIRECTVBA = Range(ref_text)
End Function
Public Sub FullCalc()
Application.CalculateFull
End Sub
Replace the INDIRECT functions in your formulas with INDIRECTVBA.
These will be static. If the slowness of your workbook is because your INDIRECTs are constantly evaluating, then this will put an end to that.
IMPORTANT: all cells that contain a formula using INDIRECTVBA will be static. Each formula will calculate when you confirm it, but it will not recalculate when precedents change.
You will then need a way to force them to recalculate at a convenient time. You can do that from the Ribbon. Or, you can run FullCalc.
Was going to add this as a comment, but my thought process got too long.
What is the context of the problem you are trying to solve?
I am guessing you are using some kind of data validation drop-down menu in $AC$9 to select a sheet name and then all your INDIRECT formulas are providing a mirror image of a particular section of the user-specified worksheet.
If that is the case then you might consider using INDEX as an alternative. It is written as =INDEX(Range, RowNum, ColNum) E.g. if you put this in H20: =INDEX(Sheet1!A:Z,ROW()+10,COLUMN()-5) then it would reflect whatever is in sheet 1, cell C30 (H - 5 columns, 20 + 10 rows). Of course, you don't have to offset anything if you don't want to, I just wanted to demonstrate that as an option.
Now, the trickier part would still remain - assigning/updating the SheetName variable. This could be done with a UserForm instead of typing in a value in a particular input cell. For example, you could have VBA provide an input box/dropdown menu for the user to select one of the available sheet names, then take that input and use it in a quick find and replace instruction - searching for "=INDEX(*!" and replacing with "=INDEX(" & InputVariable & "!"
I've made a few assumptions about your dataset and what you're trying to achieve, so it might not be the ideal solution, but perhaps something to think about.
The solution to volatility with the Indirect function (typical in multi-version cross platform use and partitioning to run Windows on Mac) can be absorbed by splitting its various functions with a pseudonym for Indirect I have named "Implied":
Public Function Implied(Varient)
' CREDIT: Stephen L. Rush
On Error Resume Next
If IsError(Range(Varient)) Then
If IsError(Find(Varient, "&")) Then
'Not Range, is Indirect. "A" & Match() Style (where Match() = row).
Implied = WorksheetFunction.Indirect(Varient)
Else
'Not a Range, not Indirect. "A" & B99 Reference (where B99 = row).
Implied = Range(Left(Varient, Find(Varient, "&") - 1) & Range(Right(Varient, Len(Varient) - Find(Varient, "&"))))
End If
Else
'Is pure Range
Implied = Range(Varient)
End If
'[On Error GoTo 0] Conflicts with use as formula
End Function

Excel Macro Autofilter issue with variable

I have a table of data with the top row being filters, I have a loop that changes which filter needs to be used inside the loop is the variable filterColumn that is being assigned a new value every time the loop runs through.
when i try to use filterColumn to determine which filter will be 'switched on' i get an error
Autofilter method of Range Class Failed
ActiveSheet.Range("$U$83:$CV$1217").AutoFilter Field:=filterColumn, Criteria1:="<>"
What is the correct syntax in order to use a variable to determine which field the filter is in?
Problem Solved I found the solution. I was referencing the filters columns position in terms of the whole worksheet when in fact I should have been referencing what number it was in the group of filters. For example the filter I wanted to change was in 'CF' which is the 84th column but my the filter I wanted to change is the 64th in the group.
Dim filterColumn As Integer
filterColumn = 2
ActiveSheet.Range("$U$83:$CV$1217").AutoFilter Field:=filterColumn, _
Criteria1:="<>"
EDIT: I tried #HeadofCatering's solution and initially it failed. However I filled in values in the referenced columns and it worked (my solution also failed under reverse conditions - make the column headers blank and it fails).
However this doesn't quite mesh with what I've (and probably you've) seen - you can definitely add filters to columns with blank headers. However one thing was consistent in the failures I saw - the filterColumn referenced a column that was outside of Application.UsedRange. You may want to try verifying that the column you are referencing is actually within Application.UsedRange (easy way: run Application.UsedRange.Select in the Immediate Window and see if your column is selected). Since you are referencing a decent amount of columns, it is possible that there are no values past a certain point (including column headers), and when you specify the column to filter, you are actually specifying something outside of your UsedRange.
An interesting (this is new to me as well) thing to test is taking a blank sheet, filling in values in cells A1 and B1, selecting columns A:G and manually adding AutoFilters - this will only add filters to columns A and B (a related situation can be found if you try to add filters to a completely blank sheet).
Sorry for the babble - chances are this isn't even your problem :)
Old solution (doesn't work when conditions described above are used)
I may be overkilling it, but try setting the sheet values as well (note I used a sample range here):
Sub SOTest()
Dim ws As Worksheet
Dim filterColumn As Integer
' Set the sheet object and declare your variable
Set ws = ActiveSheet
filterColumn = 2
' Now try the filter
ws.Range("$A$1:$E$10").AutoFilter Field:=filterColumn, Criteria1:="<>"
End Sub