I'm trying to create a Player object that holds a Long property Wealth that must be assigned to a cell in Excel, but I get the Overflow runtime error only when the last line references newPlayer.Wealth and not when the value is set to a fixed number greater than the max integer. Here is the code:
For i = 68 To (67 + numPlayers)
Dim newPlayer As New Player
newPlayer.Wealth = CLng(40000)
newPlayer.Number = i - 67
Range(Chr(i) & "2").Value = "Player " & CStr(newPlayer.Number)
Range(Chr(i) & "3").Value = newPlayer.Wealth
Next
This works, however:
Range(Chr(i) & "3").Value = CLng(40000)
I tried to clear formatting from the cell that I'm specifying but I still have problems. What is the reason for this? Here is also my class properties:
Private pNumber As Integer
Private pProperties As New Collection
Private pWealth As Long
Property Get Number() As Integer
Number = pNumber
End Property
Property Let Number(pNum As Integer)
pNumber = pNum
End Property
Property Get Properties() As Collection
Properties = pProperties
End Property
Property Get Wealth() As Long
Number = pWealth
End Property
Property Let Wealth(w As Long)
pWealth = w
End Property
Public Sub addProperty(property As PropertyUnit)
pProperties.Add (property)
End Sub
Modify your class definition.
This appears wrong. Beyond the copy/paste from Get Number() error, shoving a 40K long into a signed integer is going to overflow.
Private pNumber As Integer
Private pWealth As Long
...
Property Get Wealth() As Long
Number = pWealth
End Property
Change to something like,
...
Private pWealth As Long
Property Get Wealth() As Long
Wealth= pWealth
End Property
Property Let Wealth(Value As Long)
pWealth = Value
End Property
As Jeeped pointed out Property Get Wealth() needs to be modified like this:
Property Get Wealth() As Long
Wealth = pWealth
End Property
A couple of other points.
There is no reason to cast the number to string for the address concatenations. The VBA will do that automatically. Range(Chr(i) & "2").Value and Range(Chr(i) & 2).Value are effectively the same.
There is no reason to cast a number into long. CLng(40000) or 40000 will work.
Dim newPlayer As New Player inside a loop will only create a single instance of the Player class. Declare newPlayer outside of the loop and use Set newPlayer = New Player inside the loop to create more instances of newPlayer.
Range(Chr(i) & "2") will work as long as you don't have more then more than 22 players. I prefer Cells(2, i + 4).Value.
Dim newPlayer As New Player
numPlayers = 10
For i = 1 To numPlayers
Set newPlayer = New Player
newPlayer.Wealth = CLng(40000)
newPlayer.Number = i
Cells(2, i + 4).Value = "Player " & CStr(newPlayer.Number)
Cells(2, i + 5).Value = newPlayer.Wealth
Next
Related
I'm new to using VBA classes, and I think what I want to do -- using them with ListObjects -- is more of an "intermediate" than "beginner" technique.
Let's say I have two tables related to cars.
tblCarDesc
ID MAKE MODEL DOORS ENGINE
1 Chevrolet Corvette 2 V8
2 Ford Escort 4 V6
3 Rolls-Royce SilverCloud 4 V8
tblCarProd
ID COUNTRY TYPE
1 US Sport
2 US Economy
3 UK Luxury
(The same cars are in both tables, and shown by the ID numbers.)
I want to have a class called objCars that includes the fields (columns) from both tables. That way, when referring to Car #3, objCars.Make would be "Rolls-Royce" and objCars.Type would be "Luxury".
1) Is there a way to import both tables into objCars?
Perhaps I would create an array big enough to hold all the columns, then load both tables into it. The tutorial I've been reading says that I would then create a Collection, loop through each row of the array, make a new instance of objCars, and assign objCars.Make, objCars.Model, etc., for each row. Then each item of the Collection would contain a car. (Or something like that. I don't really know much about Collections either.) If that's right, is it the best way?
2) How exactly does one refer to a specific car? The examples I've read like to loop through Collections and work on each item therein, but what if I want to extract a particular item? I know the Ford is Car #2; how do I get the objCars.Make and objCars.Model for that particular ID number?
I would have two classes. A class clsCar for one car and a class clsCars for a collection of cars.
Each of this classes may have setter and getter methods and also may have custom methods if needed. Especially the clsCars should have a set of getBy...-methods to get a car or a collection of cars from the collection by criterion.
Example:
clsCar:
Private pID As Long
Private pMAKE As String
Private pMODEL As String
Private pDOORS As Integer
Private pENGINE As String
Private pCOUNTRY As String
Private pTYPE As String
Public Property Get ID() As Long
ID = pID
End Property
Public Property Let ID(Value As Long)
pID = Value
End Property
Public Property Get MAKE() As String
MAKE = pMAKE
End Property
Public Property Let MAKE(Value As String)
pMAKE = Value
End Property
Public Property Get MODEL() As String
MODEL = pMODEL
End Property
Public Property Let MODEL(Value As String)
pMODEL = Value
End Property
Public Property Get DOORS() As Integer
DOORS = pDOORS
End Property
Public Property Let DOORS(Value As Integer)
pDOORS = Value
End Property
Public Property Get ENGINE() As String
ENGINE = pENGINE
End Property
Public Property Let ENGINE(Value As String)
pENGINE = Value
End Property
Public Property Get COUNTRY() As String
COUNTRY = pCOUNTRY
End Property
Public Property Let COUNTRY(Value As String)
pCOUNTRY = Value
End Property
Public Property Get CarTYPE() As String
CarTYPE = pTYPE
End Property
Public Property Let CarTYPE(Value As String)
pTYPE = Value
End Property
Public Function toString() As String
toString = pID & "; " & _
pMAKE & "; " & _
pMODEL & "; " & _
pDOORS & "; " & _
pENGINE & "; " & _
pCOUNTRY & "; " & _
pTYPE
End Function
clsCars:
Private pCars As collection
Private Sub Class_Initialize()
Set pCars = New collection
End Sub
Public Sub add(oCar As clsCar)
pCars.add oCar
End Sub
Public Function getByIndex(lIndex As Long) As clsCar
Set getByIndex = pCars.Item(lIndex)
End Function
Public Function getByID(lID As Long) As clsCar
Dim oCar As clsCar
For Each oCar In pCars
If oCar.ID = lID Then
Set getByID = oCar
End If
Next
End Function
Public Function getByEngine(sEngine As String) As collection
Dim oCar As clsCar
Set getByEngine = New collection
For Each oCar In pCars
If oCar.ENGINE = sEngine Then
getByEngine.add oCar
End If
Next
End Function
default Module:
Public oCars As clsCars
Sub initialize()
Dim oCar As clsCar
Dim oListObject As ListObject
Dim oListRow As ListRow
Dim oCells As Range
Set oCars = New clsCars
Set oListObject = Worksheets("Sheet1").ListObjects("tblCarDesc")
For Each oListRow In oListObject.ListRows
Set oCells = oListRow.Range.Cells
Set oCar = New clsCar
oCar.ID = oCells(, 1).Value
oCar.MAKE = oCells(, 2).Value
oCar.MODEL = oCells(, 3).Value
oCar.DOORS = oCells(, 4).Value
oCar.ENGINE = oCells(, 5).Value
oCars.add oCar
Next
Set oListObject = Worksheets("Sheet1").ListObjects("tblCarProd")
Dim lID As Long
For Each oListRow In oListObject.ListRows
Set oCells = oListRow.Range.Cells
lID = oCells(, 1).Value
Set oCar = oCars.getByID(lID)
If Not oCar Is Nothing Then
oCar.COUNTRY = oCells(, 2).Value
oCar.CarTYPE = oCells(, 3).Value
End If
Next
MsgBox oCars.getByIndex(2).toString
For Each oCar In oCars.getByEngine("V8")
MsgBox oCar.toString
Next
End Sub
I would use a class for each, and an array of each also, so arrCars holds clsCars, and arrProd holds clsProduction. I would then use the index of each for each array when populating, so arrCars(1)=Corvette and arrProd(1)=US Sport then from each you can refer to the others, so if x=3, cars(x) and prod(x) will be correct. Or use a vlookup in excel first, and make one larger table, with the need for only 1 ID then, if that is the way they are related, but can a Bentley also be 3. Its not quite clear what you mean by the 2nd table, is there an entry for each car, or is it like catgeorising the car further, just using a certain selection. Another idea would be to have an extra property in the car class, of ProductionID and then use a static production "table" of classes to refer to.
I'm having an issue in VBA where every item in the array is being replaced every time i add something to that array.
I am attempting to go through the rows in a given range and cast every row of that into a custom class (named 'CustomRow' in below example). there is also a manager class (called 'CustomRow_Manager' below) which contains an array of rows and has a function to add new rows.
When the first row is added it works fine:
https://drive.google.com/file/d/0B6b_N7sDgjmvTmx4NDN3cmtYeGs/view?usp=sharing
however when it loops around to the second row it replaces the contents of the first row as well as add a second entry:
https://drive.google.com/file/d/0B6b_N7sDgjmvNXNLM3FCNUR0VHc/view?usp=sharing
Any ideas on how this can be solved?
I've created a bit of code which shows the issue, watch the 'rowArray' variable in the 'CustomRow_Manager' class
Macro file
https://drive.google.com/file/d/0B6b_N7sDgjmvUXYwNG5YdkoySHc/view?usp=sharing
otherwise code is below:
Data
A B C
1 X1 X2 X3
2 xx11 xx12 xx13
3 xx21 xx22 xx23
4 xx31 xx32 xx33
Module "Module1"
Public Sub Start()
Dim cusRng As Range, row As Range
Set cusRng = Range("A1:C4")
Dim manager As New CustomRow_Manager
Dim index As Integer
index = 0
For Each row In cusRng.Rows
Dim cusR As New CustomRow
Call cusR.SetData(row, index)
Call manager.AddRow(cusR)
index = index + 1
Next row
End Sub
Class module "CustomRow"
Dim iOne As String
Dim itwo As String
Dim ithree As String
Dim irowNum As Integer
Public Property Get One() As String
One = iOne
End Property
Public Property Let One(Value As String)
iOne = Value
End Property
Public Property Get Two() As String
Two = itwo
End Property
Public Property Let Two(Value As String)
itwo = Value
End Property
Public Property Get Three() As String
Three = ithree
End Property
Public Property Let Three(Value As String)
ithree = Value
End Property
Public Property Get RowNum() As Integer
RowNum = irowNum
End Property
Public Property Let RowNum(Value As Integer)
irowNum = Value
End Property
Public Function SetData(row As Range, i As Integer)
One = row.Cells(1, 1).Text
Two = row.Cells(1, 2).Text
Three = row.Cells(1, 3).Text
RowNum = i
End Function
Class module "CustomRow_Manager"
Dim rowArray(4) As New CustomRow
Dim totalRow As Integer
Public Function AddRow(r As CustomRow)
Set rowArray(totalRow) = r
If totalRow > 1 Then
MsgBox rowArray(totalRow).One & rowArray(totalRow - 1).One
End If
totalRow = totalRow + 1
End Function
Your issue is using
Dim cusR As New CustomRow
inside the For loop. This line is actually only executed once (note that when you single F8 step through the code it does not stop on that line)
Each itteration of the For loop uses the same instance of cusR. Therefore all instances of manager added to the class point to the same cusR
Replace this
For Each row In cusRng.Rows
Dim cusR As New CustomRow
with this
Dim cusR As CustomRow
For Each row In cusRng.Rows
Set cusR = New CustomRow
This explicitly instantiates a new instance of the class
We have a series of collections of objects that all have two fields for sure, an integer "key" and a string "name". We have methods that return a particular instance based on the name or key...
Public ReadOnly Property Inflations(ByVal K as String) As InflationRow
' look for K in the names
End Property
Public ReadOnly Property Inflations(ByVal K as Integer) As InflationRow
' look for K in the keys
End Property
COM interop has the interesting side effect that only the first method with a given name is exported. So we added this...
Public ReadOnly Property Inflations(ByVal K as Object) As InflationRow
Return Inflations(K)
End Property
This leads to some confusion when reading the code, and multiple lines doing the same thing. So what if I replace all of this with...
Public ReadOnly Property Inflations(ByVal K as Object) As InflationRow
If TypeOf K Is String then
'do a string lookup on name
else
'try it on the key
end if
End Property
This does the same thing in the end, but seems much easier to read and keeps all the code in the same place. But...
Most of the calls into this code doesn't come from COM, but our own code. Will many calls to TypeOf in our .net code be significantly slower than allowing the runtime to make this decision through polymorphism? I really don't know enough about the runtime to even guess.
Test it and see! :-)
Option Strict On
Module Module1
Sub Main()
Dim irc As New InflationRowCollection
For i As Integer = 0 To 4999
irc.InflationList.Add(New InflationRow With {.IntProperty = i, .StrProperty = i.ToString})
Next i
Dim t1 As Date = Now
For i As Integer = 0 To 4999
Dim ir1 As InflationRow = irc.Inflations(i)
Dim ir2 As InflationRow = irc.Inflations(i.ToString)
Next i
Dim t2 As Date = Now
For i As Integer = 0 To 4999
Dim ir1 As InflationRow = irc.InflationsObj(i)
Dim ir2 As InflationRow = irc.InflationsObj(i.ToString)
Next i
Dim t3 As Date = Now
Console.WriteLine("Typed property: " & (t2 - t1).TotalSeconds & " sec" & vbCrLf & "Object property: " & (t3 - t2).TotalSeconds & " sec")
Console.ReadKey()
End Sub
End Module
Class InflationRow
Property IntProperty As Integer
Property StrProperty As String
End Class
Class InflationRowCollection
Property InflationList As New List(Of InflationRow)
ReadOnly Property InflationsObj(o As Object) As InflationRow 'use different name for testing, so we can compare
Get
If TypeOf o Is String Then
Return Inflations(DirectCast(o, String))
ElseIf TypeOf o Is Integer Then
Return Inflations(DirectCast(o, Integer))
Else
Throw New ArgumentException
End If
End Get
End Property
ReadOnly Property Inflations(k As String) As InflationRow
Get
For Each ir As InflationRow In InflationList
If ir.StrProperty = k Then Return ir
Next
Return Nothing
End Get
End Property
ReadOnly Property Inflations(k As Integer) As InflationRow
Get
For Each ir As InflationRow In InflationList
If ir.IntProperty = k Then Return ir
Next
Return Nothing
End Get
End Property
End Class
I am try to code this "Public Property Flow(NumCommodities - 1) As Double" under a class, but apparently it need additional coding.
So I need this property because I need to keep track of values based on this code:
For t As Integer = 0 To MyProblem.Arcs.Count-1
For k = 0 To MyProblem.NumCommodities-1
Dim i As Integer = MyProblem.Arcs(t).Tail
Dim j As Integer = MyProblem.Arcs(t).Head
Dim akey As String = "x(" & i & "," & j & "," & k & ")“
Dim aid As Integer = solver.GetIndexFromKey(akey)
MyProblem.Arcs(t).Flow(k) = solver.GetValue(aid).ToDouble
Next
Next
OBS: I have already declared this under my MyProblem Class
Public Class MyProblem
Public NumCommodities
You cannot declare and array with a variable size. Instead you must declare and then initialize it.
Public Property Flow As Double() = New Double(NumCommodities - 1) {}
Note that this will fail if NumCommodities - 1 is negative or if NumCommodities has not yet been instantiated.
Since your question does not provide a lot of insight to the issue, you may also be trying to declare a property that accesses an item of an array. Here is how you would implement such a thing:
Private _Flow As Double() = New Double() {}
Public Property Flow(ByVal NumCommodities As Int32) As Double
Get
Return _Flow(NumCommodities - 1)
End Get
Set(value As Double)
_Flow(NumCommodities - 1) = value
End Set
End Property
Ahoy hoy,
I'm trying to do stuff to a custom object in a custom collection by referencing it's name property in VBA Excel. I swear it worked before (or at least didn't throw an error) and now its kaput. I'm getting an invalid call or argument error when I try to Get something by a string. Thanks in advance for even reading this too, any help is appreciated. <\edit>
Here's the collection:
Option Explicit
Private DRAFields As New Collection
Sub Add(Name As String, Optional colNbr As Long, Optional Exists As Boolean)
Dim fld As New DRAFld
fld.colNbr = colNbr
fld.Name = Name
fld.Exists = Exists
DRAFields.Add fld
End Sub
Property Get Item(NameOrNumber As Variant)
Set Item = DRAFields(NameOrNumber) '<------- Error here
End Property
The collections has items added by passing an array of names in to a function and the collection is returned without issue. I can iterate over by using the key. But the error happens if get as such: Debug.Print myFlds.Item("Customer").colNbr
And the object class just in case:
Option Explicit
Private clmNbrPvt As Long
Private namePvt As String
Private existsPvt As Boolean
Public Property Get colNbr() As Long
colNbr = clmNbrPvt
End Property
Public Property Let colNbr(lngParam As Long)
clmNbrPvt = lngParam
End Property
Public Property Get Name() As String
Name = namePvt
End Property
Public Property Let Name(strParam As String)
namePvt = strParam
End Property
Public Property Get Exists() As Boolean
Exists = existsPvt
End Property
Public Property Let Exists(booParam As Boolean)
existsPvt = booParam
End Property
And why not that function too:
Function validateAndBuildDRAFields(ByRef arrReqFields() As String, _
inputSheet As Worksheet, _
Optional VBAModule As String) As clsDRAFields
Dim lEndCol As Long: lEndCol = Standard.zGetLastColumn(inputSheet, 1)
Dim i As Long
Dim x As Long
Dim intExit As Long
Dim myDRAFields As New clsDRAFields
Set validateAndBuildDRAFields = myDRAFields
'Builds myDRAFields items from arrReqFields
For i = LBound(arrReqFields) To UBound(arrReqFields)
myDRAFields.Add arrReqFields(i)
Next i
'checks if required fields exist on input sheet
'if found then sets column number and exists = true
For i = 1 To myDRAFields.Count
For x = 1 To lEndCol
If inputSheet.Cells(1, x) = myDRAFields.Item(i).Name Then
myDRAFields.Item(i).colNbr = x
myDRAFields.Item(i).Exists = True
intExit = intExit + 1
Exit For
End If
Next x
If intExit = UBound(arrReqFields) + 1 Then Exit For
Next i
' tells user if there are any missing fields and ends if true
If (Not intExit = UBound(arrReqFields) + 1) Or _
intExit = 0 Then
For i = 1 To myDRAFields.Count
If myDRAFields.Item(i).Exists = False Then
Call Standard.TheEndWithError("I couldn't find the " & myDRAFields.Item(i).Name & _
" column in your file. Please add " & myDRAFields.Item(i).Name & _
" to your DRA Layout.", False, VBAModule)
End If
Next i
Set myDRAFields = Nothing
Standard.TheEnd
End If
End Function
To access a collection item by its key, you have to supply a key when you add the item to the collection. The key is optional. When you access a collection item with a string, the Item method assumes you want to match the key. When you use an integer, it assumes you want the positional index.
So, change the line in your Add method to
DRAFields.Add fld, fld.Name
and you'll be able to access items by their Name property.