VBA How to return from function - vba

I created a class module Rect:
Public top As Single
Public left As Single
Public bottom As Single
Public right As Single
Public Sub Class_Initialize()
Me.top = 0
Me.bottom = 0
Me.left = 0
Me.right = 0
End Sub
And a factory function to create Rect objects:
Private Function n() As Rect
Dim r As New Rect
n = r
End Function
When I call function n() I got runtime error '91'. I don't know what is wrong...

For object variables, you need to use Set to assign them:
Set n = r
See e.g. What does the keyword Set actually do in VBA?

Related

Assign custom class instance to variable with get-Function

I have two classes:
clsDataElement
Public dataA As String
Public dataB As String
Public dataC As String
clsInterface
Private mDataElements() As clsDataElement
Private mCountOfDataElements As Integer
Private Sub Class_Initialize()
mCountOfDataElements = 0
End Sub
Public Sub addDataElement(dataElement As clsDataElement)
ReDim Preserve mDataElements(mCountOfDataElements)
Set mDataElements(mCountOfDataElements) = New clsDataElement
mDataElements(mCountOfDataElements).dataA = dataElement.dataA
mDataElements(mCountOfDataElements).dataB = dataElement.dataB
mDataElements(mCountOfDataElements).dataC = dataElement.dataC
mCountOfDataElements = mCountOfDataElements + 1
End Sub
Public Function getDataElement(Optional index As Integer) As clsDataElement
If Not index >= mCountOfDataElements Then
getDataElement = mDataElements(index)
End If
End Function
So as you can see, the class clsDataElement is just a containe for some data.
The class clsInterface can contain multiple Elements of clsDataElement
Now if I want to read one Data Element from the interface class, it just does not work. However, I feel like this is related to the fact, that I cannot just assign custom classes:
Public Sub TestMyClass(myInterface As clsInterface)
Dim tmpDataElement As New clsDataElement
Set tmpDataElement = myInterface.getDataElement(0)
End Sub
So when running this code I get the error
Object variable or with block variable not set
I made sure that there are multiple data elements stored in myInterface.
Do you have any ideas what I am doing wrong?
Aren't you missing the 'Set' keyword when getting the element:
Public Function getDataElement(Optional index As Integer) As clsDataElement
If Not index >= mCountOfDataElements Then
Set getDataElement = mDataElements(index)
End If
End Function

How To Code So That Intellisense Follows The Complete Chain

While this code works and I can assign and retrieve values across all levels, intellisense only displays the methods or properties 1 level deep. How would I go about coding this so that I can follow my "Path" all the way down using intellisense and not necessarily have to just remember the methods or properties?
for instance if I type Wip. I get
but when I type Wip.Parts("Test"). , the SequenceNumbers member and its Methods/Properties are not displayed
I have the following code
clsSeq:
Option Explicit
Private iSeq As String
Private iQty As Double
Public Property Get Qty() As Double
Qty = iQty
End Property
Public Property Let Qty(lQty As Double)
iQty = lQty
End Property
Public Property Get Sequence() As String
Sequence = iSeq
End Property
Public Property Let Sequence(lSeq As String)
iSeq = lSeq
End Property
clsPart:
Option Explicit
Private iPart As String
Public SequenceNumbers As Collection
Public Property Get PartNumber() As String
PartNumber = iPart
End Property
Public Property Let PartNumber(lPart As String)
iPart = lPart
End Property
Public Sub AddSequence(aSeq As String, aQty As Double)
Dim iSeq As clsSeq
If SeqExists(aSeq) Then
Set iSeq = SequenceNumbers.Item(aSeq)
iSeq.Qty = iSeq.Qty + aQty
Else
Set iSeq = New clsSeq
With iSeq
.Sequence = aSeq
.Qty = aQty
End With
SequenceNumbers.Add iSeq, iSeq.Sequence
End If
Set iSeq = Nothing
End Sub
Private Sub Class_Initialize()
Set SequenceNumbers = New Collection
End Sub
Private Function SeqExists(iSeq As String) As Boolean
Dim v As Variant
On Error Resume Next
v = IsObject(SequenceNumbers.Item(iSeq))
SeqExists = Not IsEmpty(v)
End Function
clsParts:
Option Explicit
Public Parts As Collection
Public Sub AddPart(iPart As String)
Dim iPrt As clsPart
If Not PartExists(iPart) Then
Set iPrt = New clsPart
With iPrt
.PartNumber = iPart
End With
Parts.Add iPrt, iPrt.PartNumber
End If
End Sub
Private Function PartExists(iPT As String) As Boolean
Dim v As Variant
On Error Resume Next
v = IsObject(Parts.Item(iPT))
PartExists = Not IsEmpty(v)
End Function
Private Sub Class_Initialize()
Set Parts = New Collection
End Sub
modTest:
Sub TestWipCls()
Dim Wip As clsParts
Dim Part As clsPart
Set Wip = New clsParts
Wip.AddPart ("Test")
Set Part = Wip.Parts("Test")
Part.AddSequence "Proc7", 1505
Debug.Print Wip.Parts("Test").SequenceNumbers("Proc7").Qty
Part.AddSequence "Proc7", 100
Debug.Print Wip.Parts("Test").SequenceNumbers("Proc7").Qty
End Sub
That is because Parts is a Collection and its Default Member Call (or .Item) will return a value/object depending what was stored. While editing your code VBA does not know what kind of value/object is stored in the collection (as this is only established during run-time, eg. late-bound), so it can not give you any Intellisense-suggestions.
To circumvent this, you need a method (property/function) that returns a defined type of value/object (early-bound).
btw. (myCollection.("Foo") is the same as myCollection.Item("Foo"))
The solution is to create a custom collection that returns a value of a defined type.
The following example also explains how to implement a custom Collection so you can use the default member call instead of using .Item.
How to use the Implements in Excel VBA
While we're at it, please never use public variables in classes, make them accessible via Property Let/Set/Get methods!
More on this here: https://rubberduckvba.wordpress.com/2019/07/08/about-class-modules/
Edit:
Example for a custom Collection for classes that implement ICustomElement (Interfaces are explained in the link above)
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "CustomCollectionTemplate"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
'#Folder("Classes")
Option Explicit
Private Type TCustomCollection
CustomCollection as Collection
End Type
Dim this as TCustomCollection
Private Sub Class_Initialize()
Set this.CustomCollection = New Collection
End Sub
Private Sub Class_Terminate()
Set this.CustomCollection = Nothing
End Sub
Public Property Get NewEnum() As IUnknown
Attribute NewEnum.VB_UserMemId = -4
Attribute NewEnum.VB_MemberFlags = "40"
Set NewEnum = this.CustomCollection.[_NewEnum]
End Property
Public Sub Add(ByVal newCustomElement As ICustomElement)
this.CustomCollection.Add newCustomElement
End Sub
Public Sub Remove(ByVal Index As Long)
this.CustomCollection.Remove Index
End Sub
Public Function Item(ByVal Index As Long) As ICustomElement
Set Item = this.CustomCollection.Item(Index)
End Function
Public Function Count() As Long
Count = this.CustomCollection.Count
End Function
Thanks to M.Doerner & Mathieu Guindeon for the edits/comments

Is it possible to reach the property of a collection object that is not part of an interface

I have two classes of shapes and an interface. After I instantiate objects from each class and set their properties I add them to a collection. That's pretty straightforward. Then I declare a variable of type MyInterface and loop through the collection to add each shape. But there is an additional property for each type of shape that I want to set but which are not part of the interface. Is there a way to do this? I think other languages call this type casting but I am not sure. Does VBA support this? Any help is appreciated. My code is below:
Interface (iShape)
Option Explicit
Public Property Let Top(value As Long)
End Property
Public Property Get Top() As Long
End Property
Public Property Let Left(value As Long)
End Property
Public Property Get Left() As Long
End Property
Public Property Let Width(value As Long)
End Property
Public Property Get Width() As Long
End Property
Public Property Let Height(value As Long)
End Property
Public Property Get Height() As Long
End Property
Public Function Draw(obj As Worksheet) As Excel.Shape
End Function
Class (cDiamond)
Option Explicit
Private pTop As Long
Private pLeft As Long
Private pWidth As Long
Private pHeight As Long
Private pColor As Long
Implements iShape
'====================Properties====================
Public Property Let Top(value As Long)
pTop = value
End Property
Public Property Get Top() As Long
Top = pTop
End Property
Public Property Let Left(value As Long)
pLeft = value
End Property
Public Property Get Left() As Long
Left = pLeft
End Property
Public Property Let Width(value As Long)
pWidth = value
End Property
Public Property Get Width() As Long
Width = pWidth
End Property
Public Property Let Height(value As Long)
pHeight = value
End Property
Public Property Get Height() As Long
Height = pHeight
End Property
Public Property Let Color(value As Long)
pColor = value
End Property
Public Property Get Color() As Long
Color = pColor
End Property
'====================Methods====================
Public Function Draw(obj As Worksheet) As Excel.Shape
Set Draw = obj.Shapes.AddShape(msoShapeFlowchartOffpageConnector, Me.Left, Me.Top, Me.Width, Me.Height)
End Function
'====================Interface====================
Private Property Get iShape_Height() As Long
iShape_Height = Height
End Property
Private Property Let iShape_Height(RHS As Long)
Height = RHS
End Property
Private Property Get iShape_Left() As Long
iShape_Left = Left
End Property
Private Property Let iShape_Left(RHS As Long)
Left = RHS
End Property
Private Property Get iShape_Top() As Long
iShape_Top = Top
End Property
Private Property Let iShape_Top(RHS As Long)
Top = RHS
End Property
Private Property Get iShape_Width() As Long
iShape_Width = Width
End Property
Private Property Let iShape_Width(RHS As Long)
Width = RHS
End Property
Private Function iShape_Draw(obj As Worksheet) As Shape
Set iShape_Draw = Draw(obj)
End Function
Class (cTextbox)
For the sake of brevity, this class is identical to cDiamond except it has a Caption property instead of a Color property.
Module (mTest)
Option Explicit
Private Sub Test()
Dim wks As Excel.Worksheet
Set wks = ActiveSheet
Dim c As Collection
Set c = New Collection
Dim d1 As cDiamond
Set d1 = New cDiamond
d1.Top = 10
d1.Left = 10
d1.Height = 25
d1.Width = 25
d1.Color = RGB(255, 0, 0)
c.Add d1
Dim d2 As cDiamond
Set d2 = New cDiamond
d2.Top = 50
d2.Left = 10
d2.Height = 25
d2.Width = 25
d2.Color = RGB(0, 255, 0)
c.Add d2
Dim t1 As cTextbox
Set t1 = New cTextbox
t1.Top = 90
t1.Left = 10
t1.Height = 25
t1.Width = 25
t1.Caption = "Textbox"
c.Add t1
Dim shp As iShape
For Each shp In c
shp.Draw wks
' I would like to set the color or caption properties depending on the type of shape in the collection.
Next shp
Set c = Nothing
End Sub
If I understand you correctly, (and I don't fully understand interfaces either), you should be able to do what you want by declaring shp as of type variant.
Dim shp
For Each shp in C ...
shp will then take on the type of either cDiamond or cTextbox depending on which is retrieved from the Collection. Then you will be able to retrieve or modify shp.color or shp.caption. You may also want to change Caption to datatype String in cTextBox
Declaring shp as object you will lose intellisense. Another technique would be to have Interface return a reference to the instance of object. Similarly to how OLEObject.Object returns the instance of the object that it is wrapping. In my example I use This to return the instance of the class.
In this way you will have intellisense for all the common properties and methods. We can also access the properties and methods that are unique to the classes that implement your interface using a With statement.
You'll need to first test check the type of the object. Then use a With statement to temporarily instantiate a new Object of that type and write your code inside the With statement .
If TypeOf shp Is cTextbox Then
With New cTextbox
Msgbox .Caption
End With
End If
Finally, you simply replace the New instance with the actual instance of the Object.
If TypeOf shp Is cTextbox Then
With shp.This
Msgbox .Caption
End With
End If
Interface (iShape)
Public Function This() As Object
End Function
Classes cTextbox & cDiamond
Public Function This() As Object
Set This = Me
End Function
Public Function iShape_This() As Object
Set iShape_This = This
End Function
mTest.Test
Dim shp As iShape
For Each shp In c
shp.Draw wks
If TypeOf shp Is cTextbox Then
With shp.This
MsgBox .Caption
End With
End If
' I would like to set the color or caption properties depending on the type of shape in the collection.
Next shp

Create VBA class in VB.net

Is it possible (using COM and regasm.exe) to have a vba function call a vb.net function - which creates the class in vb.net and then passes the class back to vba, where it is recognised as a vba class?
In VBA, I can work with classes by using Insert>Class Module. I have set up a function that creates a class.
Private length As Double
Private height As Double
Public Sub init(ByRef hgt As Double)
height = hgt
length = dbl_height()
End Sub
Public Function dbl_height()
dbl_height = height * 2
End Function
I can initialize it accordingly using this function:
Public Function CreateClassFunction(foo As Integer)
Dim my_rect As Rectangle
Set my_rect = New Rectangle
my_rect.init (foo)
Set CreateClassFunction = my_rect
End Function
I can also do the same thing in vb.net with virtually identical code.
Public Class Rectangle
Private length As Double
Private height As Double
Public Sub init(ByRef hgt As Double)
height = hgt
length = dbl_height()
End Sub
Public Function dbl_height()
dbl_height = height * 2
End Function
End Class
where this vb.net function creates the class:
Public Function CreateClassFunction(foo As Integer) As Rectangle
Dim my_rect As Rectangle
my_rect = New Rectangle
my_rect.init(foo)
CreateClassFunction = my_rect
End Function
I can pull in a Variant/Object/Rectangle into vba using:
Function MyCreateClass(a As Double)
Dim classLib As New MyAnalytics.Class1
Set MyCreateClass = classLib.CreateClassFunction(a)
End Function
However this object does not have the height or length variables. (It says "no variables" on the watch window)
Edit:
Amended code as per Mat's Mug answer:
Public Class Rectangle
Private plength As Double
Private pheight As Double
Public Property length() As Double
Get
Return plength
End Get
Set(ByVal value As Double)
plength = value
End Set
End Property
Public Property height() As Double
Get
Return pheight
End Get
Set(ByVal value As Double)
pheight = value
End Set
End Property
Public Sub init(ByRef hgt As Double)
height = hgt
length = dbl_height()
End Sub
Public Function dbl_height()
dbl_height = height * 2
End Function
End Class
and testing in VBA:
Function MyCreateClass(a As Double)
Dim classLib As New MyAnalytics.Class1
Set MyCreateClass = classLib.CreateClassFunction(a)
Debug.Print MyCreateClass.Height()
Debug.Print MyCreateClass.length()
MyCreateClass.Height = 30
MyCreateClass.length = 20
Debug.Print MyCreateClass.Height()
Debug.Print MyCreateClass.length()
MyCreateClass.init (100)
Debug.Print MyCreateClass.Height()
Debug.Print MyCreateClass.length()
End Function
It won't be recognized as a VBA class - it's not a VBA class, but a COM object.
Your Rectangle class has private fields. Private fields are, well, Private. This is, roughly, what VBA sees:
Public Class Rectangle
Sub init(ByRef hgt As Double)
Function dbl_height()
End Class
Where are the fields?
Private length As Double
Private height As Double
You haven't exposed them - as far as VBA goes, they don't exist.
Now, you could make them Public - but then you would be breaking encapsulation by exposing fields; don't do that!
Expose property getters instead, and setters if you want VBA code to be able to change the Length and Height properties of a Rectangle instance.

workaround to VB.Net 2003 System.Collections.Generic?

I'm working in an old web application (vb.net 2003) and I'm trying to use a generic list of a custom class.
I realized that System.Collections.Generic was introduced in .Net 2 according to link
Is there any alternative to the list? For instance an array of class?
Let's say I have the following class definition:
Public Class Box
Public x As Integer
Public y As Integer
End Class
And an array of Class Box:
Dim BoxList() As Box
BoxList(0).x = 1
BoxList(0).y = 1
BoxList(1).x = 2
BoxList(2).y = 2
But I'm getting an error when BoxList(0).x = 1 error: Object reference not set to an instance of an object
I'm just guessing here.
Use ArrayList, like this:
Dim BoxList As New ArrayList
Dim box = New Box()
box.x = 1
box.y = 2
BoxList.Add(box)
Note: It is recommended that you add a constructor to the Box class that will accept the x and y values, like this:
Public Class Box
Public x As Integer
Public y As Integer
Public Sub New(ByVal _x As Integer, ByVal _y As Integer)
x = _x
y = _y
End Sub
End Class
Now you can shorten your ArrayList code to this:
Dim BoxList As New ArrayList
BoxList.Add(New Box(1, 2))
To use the values in the ArrayList you will need to un-box (pun not intended) the value out of the ArrayList, like this:
For Each box In BoxList
' Use x value, like this
CType(box, Box).x
Next
OR (as Meta-Knight suggested)
For Each box As Box In BoxList
' Now box is typed as Box and not object, so just use it
box.x
Next
You can create your own custom collection class - this is what we had to do back before generics. This article from MSDN gives you the specifics:
''' Code copied directly from article
Public Class WidgetCollection
Inherits System.Collections.CollectionBase
Public Sub Add(ByVal awidget As Widget)
List.Add(aWidget)
End Sub
Public Sub Remove(ByVal index as Integer)
If index > Count - 1 Or index < 0 Then
System.Windows.Forms.MessageBox.Show("Index not valid!")
Else
List.RemoveAt(index)
End If
End Sub
Public ReadOnly Property Item(ByVal index as Integer) As Widget
Get
Return CType(List.Item(index), Widget)
End Get
End Property
End Class