I would like to get all the values of a column based on a matching value through the filter function.
So far I have a form, please just pay attention to label4(Name: LBL_CODIGO_SH), label5(Name: LBL_CODIGO_SH) and combobox(Name: ComboBox3).
My code is executed when clicking on the "Buscar" button:
Private Sub CommandButton4_Click()
Dim ws As Worksheet
Dim tabla As ListObject
Dim codigo As String
Set ws = Worksheets("Relacion")
Set tabla = ws.ListObjects("Tabla3")
codigo = Me.LBL_CODIGO_SH.Caption & "_" & Me.LBL_CODIGO_TH.Caption
Me.ComboBox3.List = WorksheetFunction.Filter( _
tabla.ListColumns(1).DataBodyRange, _
Evaluate(tabla.ListColumns(1).DataBodyRange.Value & "=" & codigo) _
)
End Sub
I run my program and it gives me error 13.
I think I'm skipping code in the "Filter" function of the spreadsheet.
I would like to get the results in a combobox and also in an array variable.
Try using the Address property of the DataBodyRange object, instead of the Value property . . .
Me.ComboBox3.List = WorksheetFunction.Filter( _
tabla.ListColumns(1).DataBodyRange, _
ws.Evaluate(tabla.ListColumns(1).DataBodyRange.Address & "=" & codigo) _
)
However, if you're filtering for a string, you'll need to wrap your criteria within quotes . . .
Me.ComboBox3.List = WorksheetFunction.Filter( _
tabla.ListColumns(1).DataBodyRange, _
ws.Evaluate(tabla.ListColumns(1).DataBodyRange.Address & "=""" & codigo & """") _
)
Note that the Address property returns the range reference as a string, which is used to build another string to form an expression. This concatenated string is passed to the Evaluate method, which evaluates the expression, and returns the array of booleans needed for the second argument of WorksheetFunction.Filter.
For additional information regarding the Address property, have a look here.
Related
I am trying to use "conditional" Large function in my code e.g. to use Large function for values where in the other column there is "Y".
I am using "Evaluate" function as I need only the results in the other part of the code.
However, this is not working - I understand that I need to work with Formula2 because otherwise excel will add '#' to the function and it wont work. But still I dont know how to 'repair' evaluate function.
I am using R1C1 formula because later I want to use columns in the loop.
Sub Makro()
'not working - there is '#' included
Range("G3") = "=Large(if(c[-4]:c[-4]=""Y"", c[-3]),2)"
'working
Range("G4").Formula2 = "=Large(if(c[-4]:c[-4]=""Y"", c[-3]),2)"
'not working
Range("G5") = Evaluate("=Large(if(c[-4]:c[-4]=""Y"", c[-3]),2)")
End Sub
Using Evaluate in a Function
Sub EvaluateStuffTEST()
Debug.Print EvaluateStuff("D", Sheet1) ' code name
Debug.Print EvaluateStuff("E", ThisWorkbook.Worksheets("Sheet1")) ' tab name
Debug.Print EvaluateStuff("F") ' ActiveSheet
End Sub
Function EvaluateStuff( _
ByVal ColumnString As String, _
Optional ByVal SourceWorksheet As Worksheet = Nothing) _
As Variant
If IsMissing(SourceWorksheet) Then Set SourceWorksheet = ActiveSheet
EvaluateStuff = SourceWorksheet.Evaluate( _
"=Large(if(C:C=""Y""," & ColumnString & ":" & ColumnString & "),2)")
End Function
Is there any reason one would write VBA.Replace(), VBA.Instr() instead of just Replace(), Instr() when using these standard functions in VBA?
The only reason I can think of is when you declared functions with the same name in a custom module and you want to differentiate between them.
Edit: This example is answered, thanks to the link provided below (in short Len() is a keyword, not just a function). The example was bad since Len() is an exception.
Debug.Print VBA.Len(10) returns "2" while
Debug.Print Len(10) throws an error.
The original question remains, is there any caveat of not using the VBA. every time?
You basically answered your own question: If you create a public method with the same name as a VBA method, it takes precedence over the VBA one. E.g. put this one in a module:
'------------------------------------------------------------------------------
'Purpose : Override the VBA.CreateObject function in order to register what object
' is being created in any error message that's generated.
'
' Author: Darin Higgins
' Source: http://www.darinhiggins.com/the-vb6-createobject-function/
'------------------------------------------------------------------------------
Public Function CreateObject(ByVal sClass As String, _
Optional ByVal sServerName As String = vbNullString _
) As Object
Dim sSource As String, sDescr As String, lErrNum As Long
On Error Resume Next
If Len(sServerName) Then
Set CreateObject = VBA.CreateObject(sClass, sServerName)
Else
Set CreateObject = VBA.CreateObject(sClass)
End If
If VBA.Err Then
sSource = VBA.Err.Source
sDescr = VBA.Err.Description
lErrNum = VBA.Err
sDescr = sDescr & " (ProgID: " & sClass
If Len(sServerName) Then
sDescr = sDescr & ". Instantiated on Server '" & sServerName & "'"
End If
sDescr = sDescr & ")"
On Error GoTo 0
VBA.Err.Raise lErrNum, sSource, sDescr
End If
On Error GoTo 0
End Function
Steping through the code Set x = CreateObject("Object.Class") will step into this function instead of VBA.CreateObject.
How can I concatenate the word "Sheet" with a number (say, 2) to form a string that can be used as the code name of a sheet.
I've tried the following piece of code but it doesn't seem to work.
Sh = "Sheet" & 2
Range("A1") = Sh.index
If you want to refer the sheet just based on index you could try something like this as well ... hope it works for you
Sub trial()
i = 2
Sheets(i).Select
End Sub
I assume you want to check if a given â–ºstring argument (CodeNameString) refers to a valid Code(Name) in the VBA project. *)
If so, the following function returns the worksheet to be set to memory; otherwise the second argument IsAvailable passed by reference will change to False and can be used for error checks (c.f. ExampleCall below).
Function SheetByCodename(ByVal CodeNameString As String, ByRef IsAvailable As Boolean) As Object
'check for same CodeName in Sheets collection
Dim ws As Object
For Each ws In ThisWorkbook.Sheets
If ws.CodeName = CodeNameString Then ' check for string identity
Set SheetByCodename = ws ' set sheet object to memory
IsAvailable = True ' assign true to 2nd argument passed ByRef
Exit For
End If
Next
End Function
Example call
Sub ExampleCall()
dim cnt As Long: cnt = 2 ' << change example counter
Dim okay As Boolean ' << needed variable passed as 2nd function argument
With SheetByCodename("Sheet" & cnt, okay)
If okay Then
Debug.Print _
"a) Worksheet Name: " & .Name & vbNewLine & _
"b) Sheet's Code(Name) in Project: " & .CodeName
Else
Debug.Print "Given string refers to no valid Code(Name)."
'do other stuff to handle the wrong input
End If
End With
End Sub
*) Take note of #RonRosenfeld 's important remarks in comment:
"Codename is assigned when the worksheet is created. It can be changed in the properties window. In order to change it programmatically, you need to enable Trust Access to the VBA object model. Otherwise, it's a read-only property. "
This is the case:
I need to have two values back from a function: this function needs an input parameter to works.
strTitleName: input parameter
sName: output paramter
sScope: output paramter
Function getScenarioName(strTitleName As String, sName As String, sScope As String)
activateSheet ("Test Scenarios")
Dim rng1 As Range
Dim strSearch As String
strSearch = strTitleName & "*"
Set rng1 = Range("B:B").Find(strSearch, , xlValues, xlWhole)
If Not rng1 Is Nothing Then
'getScenarioName = rng1.Offset(0, 0)
sName = rng1.Address
sScope = rng1.Offset(1, 1).Address
Debug.Print "sName=" & sName
Debug.Print "sScope=" & sScope
End If
How can I getout in a subroutine the values of sName and sScope?
The concept at play here is ByRef vsByVal parameters.
In VBA parameters are passed by reference unless specified otherwise, which is... an unfortunate default: in most other languages, parameters are passed by value.
99% of the time, you don't need to pass anything ByRef, so ByVal is perfect, and should be specified explicitly... 99% of the time.
Passing parameters ByRef is useful for cases like this, when you need to return two or more values and returning an instance of a class encapsulating the return values would be overkill.
Keep in mind that a Function procedure always returns a value, even if you don't declare a return type. If you fail to declare a return type, and never assign a return value, the function will return Variant/Empty, which makes for a rather confusing and non-idiomatic API.
Here are a couple options:
ByRef return parameters
Say your signature looked like this:
Public Function GetScenarioName(ByVal title As String, ByRef outName As String, ByRef outScope As String) As Boolean
Now you can return True when the function succeeds, False when it doesn't (say, if rng1 happens to be Nothing), and then assign the outName and outScope parameters.
Because they're passed by reference, the calling code gets to see the new values - so the caller would look like this:
Dim scenarioTitle As String
scenarioTitle = "title"
Dim scenarioName As String, scenarioScope As String
If GetScenarioName(scenarioTitle, scenarioName, scenarioScope) Then
Debug.Print scenarioName, scenarioScope
Else
Debug.Print "No scenario was found for title '" & scenarioTitle & "'."
End If
What happens is that the function receives a copy of the scenarioTitle variable - that copy is essentially a variable that's local to the function: if you re-assign it in the body of the function, the caller doesn't get to see the updated value, the original argument remains unaffected (and this is why ByVal is the safest way to pass parameters).
But the function also receives a reference to the scenarioName and scenarioScope variables - and when it assigns to its outName and outScope parameters, the value held by that reference is updated accordingly - and the caller gets to see the updated values.
User-Defined Type
Still leveraging ByRef return values, it can sometimes be a good idea to encapsulate members in a cohesive unit: VBA lets you create user-defined types, for the simple cases where you just need to toss a bunch of values around:
Public Type TScenario
Title As String
Name As String
Scope As String
'...
End Type
Public Function GetScenarioInfo(ByRef info As TScenario) As Boolean
Now this function would work similarly, except now you no longer need to change its signature whenever you want to add a parameter: simply add the new member to TScenario and you're good to go!
The calling code would be doing this:
Dim result As TScenario
result.Tite = "title"
If GetScenarioInfo(result) Then
Debug.Print result.Name, result.Scope
Else
Debug.Print "No scenario was found for title '" & result.Title & "'."
End If
Alternatively, you could have a full-fledged class module to encapsulate the ScenarioInfo - in which case...
Full-Blown OOP
Encapsulating everything you need in its own class module gives you the most flexibility: now your function can return an object reference!
Public Function GetScenarioName(ByVal title As String) As ScenrioInfo
Now the function can return Nothing if no scenario is found, and there's no need for any parameters other than the input one:
Dim scenarioTitle As String
scenarioTitle = "title"
Dim result As ScenarioInfo
Set result = GetScenarioInfo(scenarioTitle)
If Not result Is Nothing Then
Debug.Print result.Name, result.Scope
Else
Debug.Print "No scenario was found for title '" & scenarioTitle & "'."
End If
This is IMO the cleanest approach, but does require a bit of boilerplate - namely, the ScenarioInfo class module. The simplest possible implementation would simply expose read/write public fields:
Option Explicit
Public Name As String
Public Scope As String
More elaborate implementations could involve an IScenarioInfo interface that only exposes Property Get members, the ScenarioInfo class that Implements IScenarioInfo, a VB_PredeclaredId attribute (that's... hidden... and much easier to handle with the Rubberduck VBIDE add-in) with a public factory method that lets you parameterize the object's creation - turning the function into something like this:
If Not rng1 Is Nothing Then
Set GetScenarioInfo = ScenarioInfo.Create(rng1.Address, rng1.Offset(1,1).Address)
End If
If that's an approach you find interesting, you can read up about it on the Rubberduck News blog, which I maintain.
You can create an array inside the function to store both values. Then, return the array.
For example:
'If strTitleName is your only argument, then:
Function getScenarioName(strTitleName As String) As Variant
Dim rng1 As Range
Dim strSearch As String
Dim result(1) As String
activateSheet ("Test Scenarios")
Set rng1 = Range("B:B").Find(strSearch, , xlValues, xlWhole)
strSearch = strTitleName & "*"
result(0) = ""
result(1) = ""
If Not rng1 Is Nothing Then
sName = rng1.Address
sScope = rng1.Offset(1, 1).Address
Debug.Print "sName=" & sName
Debug.Print "sScope=" & sScope
result(0) = "sName=" & sName
result(1) = "sScope=" & sScope
End If
getScenarioName = result
End Function
Using #Freeflow's suggestion of the collection, here's your updated code:
Function getScenarioName(strTitleName As String, sName As String, sScope As String) as Collection
activateSheet ("Test Scenarios")
Dim rng1 As Range
Dim strSearch As String
strSearch = strTitleName & "*"
Set rng1 = Range("B:B").Find(strSearch, , xlValues, xlWhole)
If Not rng1 Is Nothing Then
'getScenarioName = rng1.Offset(0, 0)
sName = rng1.Address
sScope = rng1.Offset(1, 1).Address
dim colToReturn as Collection
set colToReturn = New Collection
colToReturn.Add sName
colToReturn.Add sScope
Set getScenarioName = colToReturn
End If
End Function
I have the following code
Dim S As Range
Set S = Range(Sheets(1).Cells(4, 2), Sheets(1).Cells(18, 9))
Sheets(4).PivotTables("PivotTable1").ChangePivotCache ThisWorkbook. _
PivotCaches.Create(SourceType:=xlDatabase, SourceData:=S, Version:=xlpivotversion14)
This gives me an "Invalid procedure or argument" error!
however when I use the range instead of "S" like the following:
Sheets(4).PivotTables("PivotTable1").ChangePivotCache ThisWorkbook. _
PivotCaches.Create(SourceType:=xlDatabase, SourceData:= _
"C:\Users\Matt\Desktop\[FY19_Proposed_Sheet_V2.xlsm]Proposed Projects!R5C1:R29C18" _
, Version:=xlPivotTableVersion14)
I get no errors!
I want to use "S" so that I can actively change and update it. the specified fixed range in the code is hypothetical.
Thanks,
Matt!
It's because you have to provide a string referencing the Range you wish to set as SourceData. The following will work:
Sub Test()
Dim S As Range
Dim shName as string
Dim rngAddress as string
Dim sourceDataStr as string 'Has to hold both sheetname and address.
Set S = Range(Sheets(1).Cells(4, 2), Sheets(1).Cells(18, 9))
shName = Sheets(1).Name 'Capture sheet name of where the range is in your case
rngAddress = S.Address 'Get the address for reference.
sourceDataStr = shName & "!" & rngAddress 'Get the correct sourceData string
Sheets(4).PivotTables("PivotTable1").ChangePivotCache ThisWorkbook. _
PivotCaches.Create(SourceType:=xlDatabase, SourceData:=sourceDataStr , Version:=xlpivotversion14) 'Tadaaaaa...
End Sub
Not the shortest possible / most optimezed code, but it's a sample after all.