Excel Macro Autofilter issue with variable - vba

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

Related

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.

In VBA the Rows property has a weird behavior

I am trying to figure out how to work on a specific row among a big range. However it appears that a range created with the rows property does not behave the same as a simple range. Check the following code, the first time the variable SpecificRow is defined, it is not possible to select a specific cell. However with a weird workaround that redefines the range, it works fine. Do you have an idea why and how could I define the range with a more elegant way?
'The following shows the weird behavior of Rows property
Dim SourceRng As Range
Dim SpecificRow As Range
Dim i As Long
i = 3
Set SourceRng = Range("A1:D20")
Set SpecificRow = SourceRng.Rows(i)
'This will show the address of the selected row ("A3:D3")
MsgBox SpecificRow.Address
'Unexplicable behavior of the range when trying to select a specific cell
'where it will instead consider the whole row (within the limits of SourceRng)
MsgBox SpecificRow(1).Address
MsgBox SpecificRow(2).Address
'This would send an error
'MsgBox SpecificRow(1, 1).Address
'Workaround
Set SpecificRow = Intersect(SpecificRow, SpecificRow)
'The following will select the same address than before
MsgBox SpecificRow.Address
'However, now it has a correct behavior when selecting a specific cell
MsgBox SpecificRow(1).Address
MsgBox SpecificRow(2).Address
You should expect weird behavior if you're passing indexed properties the incorrect parameters. As demonstrated by your code, the Range returned by SourceRng.Rows(i) is actually correct. It just isn't doing what you think it's doing. The Rows property of a Range just returns a pointer to the exact same Range object that it was called on. You can see that in its typelib definition:
HRESULT _stdcall Rows([out, retval] Range** RHS);
Note that it doesn't take any parameters. The returned Range object is what you're providing the indexing for, and you're indexing it based on it's default property of Item (technically it's _Default, but the 2 are interchangeable). The first parameter (which is the only one you're passing with Rows(i), is RowIndex. So Rows(i) is exactly the same thing as Rows.Item(RowIndex:=i). You can actually see this in the IntelliSense tooltip that pops up when you provide a Row index:
Excel handles the indexing differently on this call though, because providing any value parameter for the second parameter is a Run-time error '1004'. Note that a similar property call is going on when you call SpecificRow(1).Address. Again, the default property of Range is Range.Item(), so you're specifying a row again - not a column. SpecificRow(1).Address is exactly the same thing as SpecificRow.Item(RowIndex:=1).Address.
The oddity in Excel appears to be that the Range returned by Range.Rows "forgets" the fact that it was called within the context of a Rows call and doesn't suppress the column indexer anymore. Remember from the typelib definition above that the object returned is just a pointer back to the original Range object. That means SpecificRow(2) "leaks" out of the narrowed context.
All things considered, I'd say the Excel Rows implementation is somewhat of a hack. Application.Intersect(SpecificRow, SpecificRow) is apparently giving you back a new "hard" Range object, but the last 2 lines of code are not what you should consider "correct" behavior. Again, when you provide only the first parameter to Range.Items, it is declared as the RowIndex:
What appears to happen is that Excel determines that there is only one row in the Range at this point and just assumes that the single parameter passed is a ColumnIndex.
As pointed out by #CallumDA, you can avoid all of this squirrelly behavior by not relying on default properties at all and explicitly providing all of the indexes that you need, i.e.:
Debug.Print SpecificRow.Item(1, 1).Address
'...or...
Debug.Print SpecificRow.Cells(1, 1).Address
This is how I would work with rows and specific cells within those rows. The only real difference is the use of .Cells():
Sub WorkingWithRows()
Dim rng As Range, rngRow As Range
Set rng = Sheet1.Range("A1:C3")
For Each rngRow In rng.Rows
Debug.Print rngRow.Cells(1, 1).Address
Debug.Print rngRow.Cells(1, 2).Address
Debug.Print rngRow.Cells(1, 3).Address
Next rngRow
End Sub
which returns:
$A$1
$B$1
$C$1
$A$2
$B$2
$C$2
$A$3
$B$3
$C$3
As you would expect
I cannot find any proper documentation on this, but this observed behaviour actually appears to be very logical.
The Range class in Excel has two important properties:
A single instance of Range is enough to represent any possible range on a sheet
It is iterable (can be used in a For Each loop)
I believe that in order to achieve logically looking iterability and yet avoid creating unnecessary entities (i.e. separate classes like CellsCollection, RowsCollection and ColumnsCollection), the Excel developers came up with a design where each instance of Range holds a private property that tells it in which units it is going to count itself (so that one range could be "a collection of rows" and another range could be "a collection of cells").
This property is set to (say) "rows" when you create a range via the Rows property, to (say) "columns" when you create a range via the Columns property, and to (say) "cells" when you create a range in any other way.
This allows you to do this and not become unnecessarily surprised:
For Each r In SomeRange.Rows
' will iterate through rows
Next
For Each c In SomeRange.Columns
' will iterate through columns
Next
Both Rows and Columns here return the same type, Range, that refers to the exactly same sheet area, and yet the For Each loop iterates via rows in the first case and via columns in the second, as if Rows and Columns returned two different types (RowsCollection and ColumnsCollection).
It makes sense that it was designed this way, because the important property of a For Each loop is that it cannot provide multiple parameters to a Range object in order to fetch the next item (cell, row, or column). In fact, For Each cannot provide any parameters at all, it can only ask "Next one please."
To support that, the Range class had to be able to give the next "something" without parameters, even though a range is two-dimensional and needs two coordinates to fetch the "something." Which is why each instance of Range has to remember in what units it will be counting itself.
A side effect of that design is that it is perfectly fine to look up "somethings" in a Range providing only one coordinate. This is exactly what the For Each mechanism would do, we are just directly jumping to the ith item.
When iterating over (or indexing into) a range returned by Rows, we're going to get the ith row, from top to bottom; for a range returned by Columns we're getting the ith column, from left to right; and for a range returned by Cells or by any other method we're going to get the ith cell, counting from top left corner to the right and then to the bottom.
Another side effect of this design is that can "step out" of a range in a meaningful way. That is, if you have a range of three cells, and you ask for the 4th cell, you still get it, and it will be the cell dictated by the shape of the range and the units it's counting itself in:
Dim r As Range
Set r = Range("A1:C3") ' Contains 9 cells
Debug.Print r.Cells(12).Address ' $C$4 - goes outside of the range but maintains its shape
So your workaround of Set SpecificRow = Intersect(SpecificRow, SpecificRow) resets the internal counting mode of that specific Range instance from (say) "rows" to (say) "cells".
You could have achieved the same with
Set SpecificRow = SpecificRow.Cells
MsgBox SpecificRow(1).Address
But it's better to keep the Cells close to the point of usage rather than the point of range creation:
MsgBox SpecificRow.Cells(1).Address

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.

UDF's on different sheets calling eachother return error 2015

I have 2 sheets with 3 UDF's in the first and 2 in the second.
sheet 1 is a monthly matrix with 1 column for each day where people put in their hours on the rows beneath. On 3 specific rows there are UDF's that consolidate the data in the column above, referencing the row as an argument. I do the function call like below to avoid having to make the UDF volatile (which prolongs calculation time greatly if I do), so the UDF's result updates when anything changes in column R:
calculateOvertime(R:R)
On sheet 2, the days of the month are in rows (not columns) where one can put in details about their day IF they did overtime. This is detected by one of the UDF's in sheet 1, so the 2 UDF's here require data calculated by a UDF in sheet 1
I have some strange issues with this setup:
For some reason, when I switch tabs, all cells containing any of these functions show up as #VALUE!. I have to add "Application.CalculateFull" to an event handler that fires whenever the tab is activated:
Private Sub Worksheet_activate()
Application.CalculateFull
End Sub
The UDF's that reference a cell containing another UDF on the other tab, will always get '2015!' as a value, referring to error 2015 (a Value error, because the cell contains #VALUE! when the sheet is not active)
Obviously these 2 issues are connected because when I shift sheets, the UDF-calculated values in the non-active sheet are somehow lost.
My method of getting a value from a cell is as follows. I figure out on which row the label in column A is by using the Find() function
Dim compensationRowIndex As Integer
compensationRowIndex = CInt(othersheet.Range("A1:A250").Find("COMPENSATION").Row)
then I get the value and cast it to a Single
Dim compensation As Single
compensation = CSng(othersheet.Cells(compensationRowIndex , columnIndex).Value)
the variable 'compensation' holds the value 2015 always.
Is there any way around this? Also when I want to print the sheets, all cells containing UDF's are filled with #VALUE!. My guess is : If I can make issue 1 go away, so will issue 2.
ANY help on this is much appreciated. I've been troubleshooting this for almost a whole day now and haven't found a solution googling the symptoms.
Problem Solved!
"ActiveSheet" inside a UDF doesn't mean "The Sheet the UDF-containing cell is on" It literally means "the sheet that's active".
When referencing a UDF on anoter sheet, things go horribly wrong. It was all a matter of replacing ActiveSheet with a variable that's set in an If statement that decides from where the UDF is called.
In my case the second sheet always has the word "Info" in it. When on that sheet, you should go one sheet to the left:
Dim ws As Worksheet
If InStr(ActiveSheet.Name, "Info") = 0 Then
Set ws = ActiveSheet
Else
Set ws = Worksheets(ActiveSheet.Index - 1)
End If
Not a 100% waterproof solution (e.g. what if someone reorders the sheets), but for my purposes it's close enough.
The following code snippet (used within a UDF) should do what you want:
Dim ws As Worksheet
If TypeOf Application.Caller Is Range Then
Set ws = Application.Caller.Parent
End If
The Caller property of the Application object points to the Range covering the cell(s) where the UDF was called from. ws will point to the containing worksheet then.
The If TypeOf clause avoids errors in case the function has been called by something else than a UDF (for instance, another VBA procedure), where Caller might not point to a Range object. In such a case, ws remains unassigned (Nothing).

Why is my conditional format offset when added by 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.