How to create and call a function in VBA? - excel

I am trying to create my first function or procedure in VBA. Basic types which I use in the code:
Private Type T_DATA_COLUMN_INFO
count As Integer
positiveColumnsColors(2) As Long ' decimal values from Hex
negativeColumnsColors(1) As Long
excludeColumnsColors(1) As Long
zeroTop As Integer ' position of zero, the Top property of zero rectangle
dataWidth As Integer
negativeDataHeight As Integer
positiveDataFound As Boolean
negativeDataFound As Boolean
End Type
' All is on horizontal axis except negativeValueY
Private Type T_COLUMN_RANGES
Xmin As Integer ' (Left) actually
Xmid As Integer ' middle position
Xmax As Integer ' Left + Column width
Xgap As Integer ' Gap between column rectangles
Xpitch As Integer ' Gap between colRanges()(1).mid and colRanges()(2).mid
negativeValueY As Integer ' Top+Height
Q1Y As Integer
Q2Y As Integer ' position of median
Q3Y As Integer
initiated As ENUM_INITIATED
End Type
What I have currently is not a function but procedure:
Sub SetColumnRanges(Sh As Shape, i As Integer)
colRanges(0).Width = 0
End Sub
But best would be if it would return variable 'passed boolean'
My code starts like this (shorted version):
Sub LookForAxis()
Dim colRanges() As T_COLUMN_RANGES
Dim i As Integer
Dim Sh As Shape
Dim passed As Boolean
ReDim colRanges(1 To 1) As T_COLUMN_RANGES
colRanges(1).initiated = 0
...
With ActiveWindow.Selection
If (.Type = ppSelectionShapes) And (.ShapeRange.Type = msoGroup) Then
For Each Sh In .ShapeRange.GroupItems
If (Sh.Name Like "Rec*") Then
For i = 1 To dataInfo.count
If Not passed Then ' If the array ...
' code skipped
' HERE I TRY TO CALL THE PROCEDURE OR FUNCTION... best if passed is returned for function:
SetColumnRanges Sh, i
Now the code to be placed in the function:
If Sh.Fill.ForeColor.RGB = dataInfo.positiveColumnsColors(1) Then
colRanges(i).initiated = colRanges(i).initiated Or columns_initiated_positive
If colRanges(i).Q1Y = 0 Then
colRanges(i).Q3Y = 0
colRanges(i).Q2Y = Sh.Top
colRanges(i).Q1Y = (Sh.Top + Sh.Height) * -1
ElseIf colRanges(i).Q1Y < 0 Then
If colRanges(i).Q1Y * -1 < Sh.Top + Sh.Height Then
tempInt = colRanges(i).Q2Y * -1
colRanges(i).Q3Y = colRanges(i).Q2Y ' Make the old value positive
colRanges(i).Q2Y = tempInt ' Make the old value positive
colRanges(i).Q1Y = Sh.Top + Sh.Height
Else
' Sh.Name = "Q3"
colRanges(i).Q3Y = Sh.Top + Sh.Height
colRanges(i).Q1Y = colRanges(i).Q1Y * -1 ' Make the old value positive
End If
End If
ElseIf Sh.Fill.ForeColor.RGB = dataInfo.negativeColumnsColors(1) Then
' Sh.Name = "Negative"
colRanges(i).initiated = colRanges(i).initiated Or columns_initiated_negative
End If
So colRanges and SH should be used in the function.
Error I get is:
Byref argument type mismatch
What am I doing wrong and how to fix it correctly?

you question format is a bit messed up which makes it complicated to read so if you can update it I could be more precise but calling a function works like this:
Sub test()
MsgBox test2("hi")
Dim var As String
var = test2("hi")
MsgBox var
End Sub
Function test2(var As String) As Boolean
test2 = True
End Function
you must make sure the type of the vars you are passing to your function are of the same type as the ones declared in your function (e.g. passing "hi" to "string" is ok but this would not work if var would be of type long in the function.
at the end of your function you send the result back by using the function name => "Test2 = output of your function you want to send back".

Related

Find textbox in a group of shapes (VBA 6.3)

I am using VBA 6.3. In Excel I have a chart - left y axis on it and numbers. When I copy the Chart to powerpoint and degroup it, I leave the y axis only with the TextBoxes and the axis alone. Now I would like to obtain the minimum and maximum numbers beside the axis. First I tried to detect the TextBoxes it should be msoTextBox value 17. But when I checked the type number is 378*... I could not find out what is it (using google search). I need to obtain the two numbers and length of the axis line (so I can calculate the ratio y_length/(max-min) ).
This is the code sofar.
Sub GetMinMax()
Dim YAxisMinMax() As Integer
With ActiveWindow.Selection
If (.Type = ppSelectionShapes) And (.ShapeRange.Type = msoTextBox) Then
Else
MsgBox "Failed"
Exit Sub
End If
End With
End Sub
Rhe result - The Message is Failed. It did not found the TextGroup with number.
*Edit: I have found, that the type is different for every "text box". However the name of the shape is "rectangle", not a text box". It looks like text box, because it has text inside it.
So far, this what I have done:
Option Explicit
Private Type T_HORIZONTAL_LINE
ShortLineLeft As Integer
ShortLineTop As Integer
Length As Integer
isFound As Boolean
End Type
Private Type T_VERTICAL_LINE
ShortLineLeft As Integer
ShortLineTop As Integer
Length As Integer
isFound As Boolean
End Type
Sub LookForAxis()
Dim Horizontal As T_HORIZONTAL_LINE
Dim Axis As T_VERTICAL_LINE
Dim YAxisMinMax(2) As Integer
Dim OldMinMax(2) As Integer
Dim Value As Integer
Dim Ratio As Single
Dim Text As String
Dim Sh As Shape
With ActiveWindow.Selection
If (.Type = ppSelectionShapes) And (.ShapeRange.Type = msoGroup) Then
For Each Sh In .ShapeRange.GroupItems
If Sh.Type = msoLine Then
If (Axis.isFound) And (Horizontal.isFound) Then
ElseIf (Sh.Width < Sh.Height) And (Not Axis.isFound) Then
Axis.Length = Sh.Height
Axis.ShortLineLeft = Sh.Left
Axis.ShortLineTop = Sh.Top
Axis.isFound = True
ElseIf (Sh.Width > Sh.Height) And (Not Horizontal.isFound) Then
Horizontal.Length = Sh.Width
Horizontal.ShortLineLeft = Sh.Left
Horizontal.ShortLineTop = Sh.Top
Horizontal.isFound = True
End If
ElseIf (Sh.Type = msoAutoShape) And (Sh.HasTextFrame = msoTrue) Then
Text = Sh.TextFrame.TextRange.Text
Value = CInt(Text) ' Possibly: CLng()
If Value < OldMinMax(1) Then
OldMinMax(1) = Value
ElseIf Value > OldMinMax(2) Then
OldMinMax(2) = Value
End If
End If
Next Sh
Ratio = Axis.Length / (OldMinMax(2) - OldMinMax(1)) ' Axis length div. axis range
Else
MsgBox "You have not selected an OBJECT in PowerPoint to dimension."
Exit Sub
End If
End With
End Sub
Notice: I have first placed all the axis lines and rectangles with TextFrame to one group to make it easy.

Excel Vba recursive Sub not changing Variant ByRef parameter

I have the following Sub getting a column of numbers into a Variant array than calling a Bubble sorting function on it to sort the numbers still in the Loo variable. I expect Loo to be changed i.e. numbers in it appearing in sorted order after the sorting function has received it Byref and has done its job. Interestingly the sorting happens, as a Watch shows on Inarray in the sorting function. But Loo - despite given ByRef to a Function - is never changed.
Are variant arrays not suitable for ByRef parameter passing? Am I doing something wrong in the sorter function? The Recursion is what "kills" Inarray before it can be written back to Loo?
Option Explicit
Option Base 1
Public Loo As Variant
Public Destfile As Workbook
Private i As Integer
Sub sorter()
Set Destfile = Workbooks("SomeWB")
With Destfile.Worksheets("Somesheet").ListObjects("sometable").ListColumns("Numbers")
Loo = Application.Transpose(.DataBodyRange)
End With
BubbleSortArray (Loo)
End Sub
The Bubble sorting Function is the following:
Option Explicit
Option Base 1
Private NumberOfChanges, p As Integer
Private Store As Variant
Public Sub BubbleSortArray(ByRef Inarray As Variant)
NumberOfChanges = 0
For p = 1 To UBound(Inarray, 1) - 1
If Inarray(p) > Inarray(p + 1) Then
Store = Inarray(p + 1)
Inarray(p + 1) = Inarray(p)
Inarray(p) = Store
NumberOfChanges = NumberOfChanges + 1
End If
Next p
If NumberOfChanges <> 0 Then BubbleSortArray (Inarray)
End Sub
The problem you are facing is caused by the parentheses around Loo in your call. VBA is different from other languages in that if a call to a routine does not have an assignment, parenthesis are not needed and in fact cause different behaviour. So:
BubbleSortArray (Loo)
more or less tells VBA to evaluate Loo before passing it to the sorting routine.
If you would call it like this, all would be well:
BubbleSortArray Loo
See for example this little demo:
Sub foo()
Dim x As Integer
x = 1
bar (x)
MsgBox x
bar x
MsgBox x
End Sub
Sub bar(ByRef x As Integer)
x = 10
End Sub
I believe you are going to need to explicitly make Loo and Inarray arrays and not variants (you can make them arrays of variants...or Long, String, whatever). Also not a great idea to have global or module-level variables.
Something like this will work:
Option Explicit
Option Base 1
Sub sorter()
Dim Loo() As Variant
Dim Destfile As Workbook
Set Destfile = ThisWorkbook
With Destfile.Worksheets("QueryResult").ListObjects("Table1").ListColumns("COLUMN_NAME")
Loo = Application.Transpose(.DataBodyRange)
End With
Call BubbleSortArray(Inarray:=Loo)
Stop
End Sub
Public Sub BubbleSortArray(ByRef Inarray() As Variant)
Dim NumberOfChanges As Integer
Dim p As Integer
Dim Store As Variant
NumberOfChanges = 0
For p = 1 To UBound(Inarray, 1) - 1
If Inarray(p) > Inarray(p + 1) Then
Store = Inarray(p + 1)
Inarray(p + 1) = Inarray(p)
Inarray(p) = Store
NumberOfChanges = NumberOfChanges + 1
End If
Next p
If NumberOfChanges <> 0 Then BubbleSortArray Inarray:=Inarray
End Sub

VBA Check whether array returned is 2D or 1D? [duplicate]

This question already has answers here:
How to find the number of dimensions that an array has?
(3 answers)
Closed 2 years ago.
Does anyone know how to return the number of dimensions of a (Variant) variable passed to it in VBA?
Function getDimension(var As Variant) As Long
On Error GoTo Err
Dim i As Long
Dim tmp As Long
i = 0
Do While True
i = i + 1
tmp = UBound(var, i)
Loop
Err:
getDimension = i - 1
End Function
That's the only way I could come up with. Not pretty….
Looking at MSDN, they basically did the same.
To return the number of dimensions without swallowing errors:
#If VBA7 Then
Private Type Pointer: Value As LongPtr: End Type
Private Declare PtrSafe Sub RtlMoveMemory Lib "kernel32" (ByRef dest As Any, ByRef src As Any, ByVal Size As LongPtr)
#Else
Private Type Pointer: Value As Long: End Type
Private Declare Sub RtlMoveMemory Lib "kernel32.dll" (ByRef dest As Any, ByRef src As Any, ByVal Size As Long)
#End If
Private Type TtagVARIANT
vt As Integer
r1 As Integer
r2 As Integer
r3 As Integer
sa As Pointer
End Type
Public Function GetDims(source As Variant) As Integer
Dim va As TtagVARIANT
RtlMoveMemory va, source, LenB(va) ' read tagVARIANT '
If va.vt And &H2000 Then Else Exit Function ' exit if not an array '
If va.vt And &H4000 Then RtlMoveMemory va.sa, ByVal va.sa.Value, LenB(va.sa) ' read by reference '
If va.sa.Value Then RtlMoveMemory GetDims, ByVal va.sa.Value, 2 ' read cDims from tagSAFEARRAY '
End Function
Usage:
Sub Examples()
Dim list1
Debug.Print GetDims(list1) ' >> 0 '
list1 = Array(1, 2, 3, 4)
Debug.Print GetDims(list1) ' >> 1 '
Dim list2()
Debug.Print GetDims(list2) ' >> 0 '
ReDim list2(2)
Debug.Print GetDims(list2) ' >> 1 '
ReDim list2(2, 2)
Debug.Print GetDims(list2) ' >> 2 '
Dim list3(0 To 0, 0 To 0, 0 To 0)
Debug.Print GetDims(list3) ' >> 3 '
End Sub
#cularis and #Issun have perfectly adequate answers for the exact question asked. I'm going to question your question, though. Do you really have a bunch of arrays of unknown dimension count floating around? If you're working in Excel, the only situation where this should occur is a UDF where you might get passed either a 1-D array or a 2-D array (or a non-array), but nothing else.
You should almost never have a routine that expects something arbitrary though. And thus you probably shouldn't have a general "find # of array dimensions" routine either.
So, with that in mind, here is the routines I use:
Global Const ERR_VBA_NONE& = 0
Global Const ERR_VBA_SUBSCRIPT_OUT_OF_RANGE& = 9
'Tests an array to see if it extends to a given dimension
Public Function arrHasDim(arr, dimNum As Long) As Boolean
Debug.Assert IsArray(arr)
Debug.Assert dimNum > 0
'Note that it is possible for a VBA array to have no dimensions (i.e.
''LBound' raises an error even on the first dimension). This happens
'with "unallocated" (borrowing Chip Pearson's terminology; see
'http://www.cpearson.com/excel/VBAArrays.htm) dynamic arrays -
'essentially arrays that have been declared with 'Dim arr()' but never
'sized with 'ReDim', or arrays that have been deallocated with 'Erase'.
On Error Resume Next
Dim lb As Long
lb = LBound(arr, dimNum)
'No error (0) - array has given dimension
'Subscript out of range (9) - array doesn't have given dimension
arrHasDim = (Err.Number = ERR_VBA_NONE)
Debug.Assert (Err.Number = ERR_VBA_NONE Or Err.Number = ERR_VBA_SUBSCRIPT_OUT_OF_RANGE)
On Error GoTo 0
End Function
'"vect" = array of one and only one dimension
Public Function isVect(arg) As Boolean
If IsObject(arg) Then
Exit Function
End If
If Not IsArray(arg) Then
Exit Function
End If
If arrHasDim(arg, 1) Then
isVect = Not arrHasDim(arg, 2)
End If
End Function
'"mat" = array of two and only two dimensions
Public Function isMat(arg) As Boolean
If IsObject(arg) Then
Exit Function
End If
If Not IsArray(arg) Then
Exit Function
End If
If arrHasDim(arg, 2) Then
isMat = Not arrHasDim(arg, 3)
End If
End Function
Note the link to Chip Pearson's excellent web site: http://www.cpearson.com/excel/VBAArrays.htm
Also see: How do I determine if an array is initialized in VB6?. I personally don't like the undocumented behavior it relies on, and performance is rarely that important in the Excel VBA code I'm writing, but it's interesting nonetheless.
For arrays, MS has a nice method that involves looping through until an error occurs.
"This routine tests the array named Xarray by testing the LBound of each dimension. Using a For...Next loop, the routine cycles through the number of possible array dimensions, up to 60000, until an error is generated. Then the error handler takes the counter step that the loop failed on, subtracts one (because the previous one was the last one without an error), and displays the result in a message box...."
http://support.microsoft.com/kb/152288
Cleaned-up version of code (decided to write as a function, not sub):
Function NumberOfDimensions(ByVal vArray As Variant) As Long
Dim dimnum As Long
On Error GoTo FinalDimension
For dimnum = 1 To 60000
ErrorCheck = LBound(vArray, dimnum)
Next
FinalDimension:
NumberOfDimensions = dimnum - 1
End Function
Microsoft has documented the structure of VARIANT and SAFEARRAY, and using those you can parse the binary data to get the dimensions.
Create a normal code module. I call mine "mdlDims". You would use it by calling the simple function 'GetDims' and passing it an array.
Option Compare Database
Option Explicit
Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByVal Destination As Long, ByVal Source As Long, ByVal Length As Integer)
Private Declare Function VarPtrArray Lib "msvbvm60.dll" Alias "VarPtr" (var() As Any) As Long
'http://msdn.microsoft.com/en-us/library/windows/desktop/ms221482(v=vs.85).aspx
Private Type SAFEARRAY
cDims As Integer
fFeatures As Integer
cbElements As Long
cLocks As Long
pvData As Long
End Type
'Variants are all 16 bytes, but they are split up differently based on the contained type
'VBA doesn't have the ability to Union, so a Type is limited to representing one layout
'http://msdn.microsoft.com/en-us/library/windows/desktop/ms221627(v=vs.85).aspx
Private Type ARRAY_VARIANT
vt As Integer
wReserved1 As Integer
wReserved2 As Integer
wReserved3 As Integer
lpSAFEARRAY As Long
data(4) As Byte
End Type
'http://msdn.microsoft.com/en-us/library/windows/desktop/ms221170(v=vs.85).aspx
Private Enum VARENUM
VT_EMPTY = &H0
VT_NULL
VT_I2
VT_I4
VT_R4
VT_R8
VT_CY
VT_DATE
VT_BSTR
VT_DISPATCH
VT_ERROR
VT_BOOL
VT_VARIANT
VT_UNKNOWN
VT_DECIMAL
VT_I1 = &H10
VT_UI1
VT_UI2
VT_I8
VT_UI8
VT_INT
VT_VOID
VT_HRESULT
VT_PTR
VT_SAFEARRAY
VT_CARRAY
VT_USERDEFINED
VT_LPSTR
VT_LPWSTR
VT_RECORD = &H24
VT_INT_PTR
VT_UINT_PTR
VT_ARRAY = &H2000
VT_BYREF = &H4000
End Enum
Public Function GetDims(VarSafeArray As Variant) As Integer
Dim varArray As ARRAY_VARIANT
Dim lpSAFEARRAY As Long
Dim sArr As SAFEARRAY
'Inspect the Variant
CopyMemory VarPtr(varArray.vt), VarPtr(VarSafeArray), 16&
'If the Variant is pointing to an array...
If varArray.vt And (VARENUM.VT_ARRAY Or VARENUM.VT_BYREF) Then
'Get the pointer to the SAFEARRAY from the Variant
CopyMemory VarPtr(lpSAFEARRAY), varArray.lpSAFEARRAY, 4&
'If the pointer is not Null
If Not lpSAFEARRAY = 0 Then
'Read the array dimensions from the SAFEARRAY
CopyMemory VarPtr(sArr), lpSAFEARRAY, LenB(sArr)
'and return them
GetDims = sArr.cDims
Else
'The array is uninitialized
GetDims = 0
End If
Else
'Not an array, you could choose to raise an error here
GetDims = 0
End If
End Function
I presume you mean without using On Error Resume Next which most programmers dislike and which also means that during debugging you can't use 'Break On All Errors' to get the code to stop dead (Tools->Options->General->Error Trapping->Break on All Errors).
For me one solution is to bury any On Error Resume Next into a compiled DLL, in the old days this would have been VB6. Today you could use VB.NET but I choose to use C#.
If Visual Studio is available to you then here is some source. It will return a dictionary, the Dicitionary.Count will return the number of dimensions. The items will also contain the LBound and UBound as a concatenated string. I'm always querying an array not just for its dimensions but also for LBound and UBound of those dimensions so I put these together and return a whole bundle of info in a Scripting Dictionary
Here is C# source, start a Class Library calling it BuryVBAErrorsCS, set ComVisible(true) add a reference to COM library 'Microsoft Scripting Runtime', Register for Interop.
using Microsoft.VisualBasic;
using System;
using System.Runtime.InteropServices;
namespace BuryVBAErrorsCS
{
// Requires adding a reference to COM library Microsoft Scripting Runtime
// In AssemblyInfo.cs set ComVisible(true);
// In Build tab check 'Register for Interop'
public interface IDimensionsAndBounds
{
Scripting.Dictionary DimsAndBounds(Object v);
}
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IDimensionsAndBounds))]
public class CDimensionsAndBounds : IDimensionsAndBounds
{
public Scripting.Dictionary DimsAndBounds(Object v)
{
Scripting.Dictionary dicDimsAndBounds;
dicDimsAndBounds = new Scripting.Dictionary();
try
{
for (Int32 lDimensionLoop = 1; lDimensionLoop < 30; lDimensionLoop++)
{
long vLBound = Information.LBound((Array)v, lDimensionLoop);
long vUBound = Information.UBound((Array)v, lDimensionLoop);
string concat = (string)vLBound.ToString() + " " + (string)vUBound.ToString();
dicDimsAndBounds.Add(lDimensionLoop, concat);
}
}
catch (Exception)
{
}
return dicDimsAndBounds;
}
}
}
For Excel client VBA code here is some source
Sub TestCDimensionsAndBounds()
'* requires Tools->References->BuryVBAErrorsCS.tlb
Dim rng As Excel.Range
Set rng = ThisWorkbook.Worksheets.Item(1).Range("B4:c7")
Dim v As Variant
v = rng.Value2
Dim o As BuryVBAErrorsCS.CDimensionsAndBounds
Set o = New BuryVBAErrorsCS.CDimensionsAndBounds
Dim dic As Scripting.Dictionary
Set dic = o.DimsAndBounds(v)
Debug.Assert dic.Items()(0) = "1 4"
Debug.Assert dic.Items()(1) = "1 2"
Dim s(1 To 2, 2 To 3, 3 To 4, 4 To 5, 5 To 6)
Set dic = o.DimsAndBounds(s)
Debug.Assert dic.Items()(0) = "1 2"
Debug.Assert dic.Items()(1) = "2 3"
Debug.Assert dic.Items()(2) = "3 4"
Debug.Assert dic.Items()(3) = "4 5"
Debug.Assert dic.Items()(4) = "5 6"
Stop
End Sub
NOTE WELL: This answer handles grid variants pulled off a worksheet with Range.Value as well as arrays created in code using Dim s(1) etc.! Some of the other answers do not do this.
I like to use the fact that with an error, the new variable-value is not charged.
To get the dimension (A_Dim) of an Array (vArray) you can use following code:
On Error Resume Next
A_Dim = -1
Do Until A = "X"
A_Dim = A_Dim + 1
A = "X"
A = UBound(vArray, A_Dim + 1)
Loop
On Error GoTo 0
Function ArrayDimension(ByRef ArrayX As Variant) As Byte
Dim i As Integer, a As String, arDim As Byte
On Error Resume Next
i = 0
Do
a = CStr(ArrayX(0, i))
If Err.Number > 0 Then
arDim = i
On Error GoTo 0
Exit Do
Else
i = i + 1
End If
Loop
If arDim = 0 Then arDim = 1
ArrayDimension = arDim
End Function
I found a pretty simple way to check, probably laden with a bunch of coding faux pas, incorrect lingo, and ill advised techniques but never the less:
Dim i as Long
Dim VarCount as Long
Dim Var as Variant
'generate your variant here
i = 0
VarCount = 0
recheck1:
If IsEmpty(Var(i)) = True Then GoTo VarCalc
i = i + 1
GoTo recheck1
VarCalc:
VarCount= i - 1
Note: VarCount will obviously return a negative number if Var(0) doesn't exist. VarCount is the max reference number for use with Var(i), i is the number of variants you have.
What about just using ubound(var) + 1? That should give you the last element of most of variables (unless it's a custom range, but in that case you should know that info already). The range of a conventional variable (for instance, when using the split function) starts with 0; ubound gives you the last item of the variable. So if you have a variable with 8 elements, for instance, it will go from 0 (lbound) to 7 (ubound), and you can know the quantity of elements just adding ubound(var) + 1. For example:
Public Sub PrintQntElements()
Dim str As String
Dim var As Variant
Dim i As Integer
str = "Element1!Element2!Element3!Element4!Element5!Element6!Element7!Element8"
var = Split(str, "!")
i = UBound(var) + 1
Debug.Print "First element: " & LBound(var)
Debug.Print "Last element: " & UBound(var)
Debug.Print "Quantity of elements: " & i
End Sub
It will print this output to the Inmediate window:
First element: 0
Last element: 7
Quantity of elements: 8
Also, if you are not sure that the first element (lbound) is 0, you can just use:
i = UBound(var) - LBound(var) + 1

How to return the number of dimensions of a (Variant) variable passed to it in VBA [duplicate]

This question already has answers here:
How to find the number of dimensions that an array has?
(3 answers)
Closed 2 years ago.
Does anyone know how to return the number of dimensions of a (Variant) variable passed to it in VBA?
Function getDimension(var As Variant) As Long
On Error GoTo Err
Dim i As Long
Dim tmp As Long
i = 0
Do While True
i = i + 1
tmp = UBound(var, i)
Loop
Err:
getDimension = i - 1
End Function
That's the only way I could come up with. Not pretty….
Looking at MSDN, they basically did the same.
To return the number of dimensions without swallowing errors:
#If VBA7 Then
Private Type Pointer: Value As LongPtr: End Type
Private Declare PtrSafe Sub RtlMoveMemory Lib "kernel32" (ByRef dest As Any, ByRef src As Any, ByVal Size As LongPtr)
#Else
Private Type Pointer: Value As Long: End Type
Private Declare Sub RtlMoveMemory Lib "kernel32.dll" (ByRef dest As Any, ByRef src As Any, ByVal Size As Long)
#End If
Private Type TtagVARIANT
vt As Integer
r1 As Integer
r2 As Integer
r3 As Integer
sa As Pointer
End Type
Public Function GetDims(source As Variant) As Integer
Dim va As TtagVARIANT
RtlMoveMemory va, source, LenB(va) ' read tagVARIANT '
If va.vt And &H2000 Then Else Exit Function ' exit if not an array '
If va.vt And &H4000 Then RtlMoveMemory va.sa, ByVal va.sa.Value, LenB(va.sa) ' read by reference '
If va.sa.Value Then RtlMoveMemory GetDims, ByVal va.sa.Value, 2 ' read cDims from tagSAFEARRAY '
End Function
Usage:
Sub Examples()
Dim list1
Debug.Print GetDims(list1) ' >> 0 '
list1 = Array(1, 2, 3, 4)
Debug.Print GetDims(list1) ' >> 1 '
Dim list2()
Debug.Print GetDims(list2) ' >> 0 '
ReDim list2(2)
Debug.Print GetDims(list2) ' >> 1 '
ReDim list2(2, 2)
Debug.Print GetDims(list2) ' >> 2 '
Dim list3(0 To 0, 0 To 0, 0 To 0)
Debug.Print GetDims(list3) ' >> 3 '
End Sub
#cularis and #Issun have perfectly adequate answers for the exact question asked. I'm going to question your question, though. Do you really have a bunch of arrays of unknown dimension count floating around? If you're working in Excel, the only situation where this should occur is a UDF where you might get passed either a 1-D array or a 2-D array (or a non-array), but nothing else.
You should almost never have a routine that expects something arbitrary though. And thus you probably shouldn't have a general "find # of array dimensions" routine either.
So, with that in mind, here is the routines I use:
Global Const ERR_VBA_NONE& = 0
Global Const ERR_VBA_SUBSCRIPT_OUT_OF_RANGE& = 9
'Tests an array to see if it extends to a given dimension
Public Function arrHasDim(arr, dimNum As Long) As Boolean
Debug.Assert IsArray(arr)
Debug.Assert dimNum > 0
'Note that it is possible for a VBA array to have no dimensions (i.e.
''LBound' raises an error even on the first dimension). This happens
'with "unallocated" (borrowing Chip Pearson's terminology; see
'http://www.cpearson.com/excel/VBAArrays.htm) dynamic arrays -
'essentially arrays that have been declared with 'Dim arr()' but never
'sized with 'ReDim', or arrays that have been deallocated with 'Erase'.
On Error Resume Next
Dim lb As Long
lb = LBound(arr, dimNum)
'No error (0) - array has given dimension
'Subscript out of range (9) - array doesn't have given dimension
arrHasDim = (Err.Number = ERR_VBA_NONE)
Debug.Assert (Err.Number = ERR_VBA_NONE Or Err.Number = ERR_VBA_SUBSCRIPT_OUT_OF_RANGE)
On Error GoTo 0
End Function
'"vect" = array of one and only one dimension
Public Function isVect(arg) As Boolean
If IsObject(arg) Then
Exit Function
End If
If Not IsArray(arg) Then
Exit Function
End If
If arrHasDim(arg, 1) Then
isVect = Not arrHasDim(arg, 2)
End If
End Function
'"mat" = array of two and only two dimensions
Public Function isMat(arg) As Boolean
If IsObject(arg) Then
Exit Function
End If
If Not IsArray(arg) Then
Exit Function
End If
If arrHasDim(arg, 2) Then
isMat = Not arrHasDim(arg, 3)
End If
End Function
Note the link to Chip Pearson's excellent web site: http://www.cpearson.com/excel/VBAArrays.htm
Also see: How do I determine if an array is initialized in VB6?. I personally don't like the undocumented behavior it relies on, and performance is rarely that important in the Excel VBA code I'm writing, but it's interesting nonetheless.
For arrays, MS has a nice method that involves looping through until an error occurs.
"This routine tests the array named Xarray by testing the LBound of each dimension. Using a For...Next loop, the routine cycles through the number of possible array dimensions, up to 60000, until an error is generated. Then the error handler takes the counter step that the loop failed on, subtracts one (because the previous one was the last one without an error), and displays the result in a message box...."
http://support.microsoft.com/kb/152288
Cleaned-up version of code (decided to write as a function, not sub):
Function NumberOfDimensions(ByVal vArray As Variant) As Long
Dim dimnum As Long
On Error GoTo FinalDimension
For dimnum = 1 To 60000
ErrorCheck = LBound(vArray, dimnum)
Next
FinalDimension:
NumberOfDimensions = dimnum - 1
End Function
Microsoft has documented the structure of VARIANT and SAFEARRAY, and using those you can parse the binary data to get the dimensions.
Create a normal code module. I call mine "mdlDims". You would use it by calling the simple function 'GetDims' and passing it an array.
Option Compare Database
Option Explicit
Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByVal Destination As Long, ByVal Source As Long, ByVal Length As Integer)
Private Declare Function VarPtrArray Lib "msvbvm60.dll" Alias "VarPtr" (var() As Any) As Long
'http://msdn.microsoft.com/en-us/library/windows/desktop/ms221482(v=vs.85).aspx
Private Type SAFEARRAY
cDims As Integer
fFeatures As Integer
cbElements As Long
cLocks As Long
pvData As Long
End Type
'Variants are all 16 bytes, but they are split up differently based on the contained type
'VBA doesn't have the ability to Union, so a Type is limited to representing one layout
'http://msdn.microsoft.com/en-us/library/windows/desktop/ms221627(v=vs.85).aspx
Private Type ARRAY_VARIANT
vt As Integer
wReserved1 As Integer
wReserved2 As Integer
wReserved3 As Integer
lpSAFEARRAY As Long
data(4) As Byte
End Type
'http://msdn.microsoft.com/en-us/library/windows/desktop/ms221170(v=vs.85).aspx
Private Enum VARENUM
VT_EMPTY = &H0
VT_NULL
VT_I2
VT_I4
VT_R4
VT_R8
VT_CY
VT_DATE
VT_BSTR
VT_DISPATCH
VT_ERROR
VT_BOOL
VT_VARIANT
VT_UNKNOWN
VT_DECIMAL
VT_I1 = &H10
VT_UI1
VT_UI2
VT_I8
VT_UI8
VT_INT
VT_VOID
VT_HRESULT
VT_PTR
VT_SAFEARRAY
VT_CARRAY
VT_USERDEFINED
VT_LPSTR
VT_LPWSTR
VT_RECORD = &H24
VT_INT_PTR
VT_UINT_PTR
VT_ARRAY = &H2000
VT_BYREF = &H4000
End Enum
Public Function GetDims(VarSafeArray As Variant) As Integer
Dim varArray As ARRAY_VARIANT
Dim lpSAFEARRAY As Long
Dim sArr As SAFEARRAY
'Inspect the Variant
CopyMemory VarPtr(varArray.vt), VarPtr(VarSafeArray), 16&
'If the Variant is pointing to an array...
If varArray.vt And (VARENUM.VT_ARRAY Or VARENUM.VT_BYREF) Then
'Get the pointer to the SAFEARRAY from the Variant
CopyMemory VarPtr(lpSAFEARRAY), varArray.lpSAFEARRAY, 4&
'If the pointer is not Null
If Not lpSAFEARRAY = 0 Then
'Read the array dimensions from the SAFEARRAY
CopyMemory VarPtr(sArr), lpSAFEARRAY, LenB(sArr)
'and return them
GetDims = sArr.cDims
Else
'The array is uninitialized
GetDims = 0
End If
Else
'Not an array, you could choose to raise an error here
GetDims = 0
End If
End Function
I presume you mean without using On Error Resume Next which most programmers dislike and which also means that during debugging you can't use 'Break On All Errors' to get the code to stop dead (Tools->Options->General->Error Trapping->Break on All Errors).
For me one solution is to bury any On Error Resume Next into a compiled DLL, in the old days this would have been VB6. Today you could use VB.NET but I choose to use C#.
If Visual Studio is available to you then here is some source. It will return a dictionary, the Dicitionary.Count will return the number of dimensions. The items will also contain the LBound and UBound as a concatenated string. I'm always querying an array not just for its dimensions but also for LBound and UBound of those dimensions so I put these together and return a whole bundle of info in a Scripting Dictionary
Here is C# source, start a Class Library calling it BuryVBAErrorsCS, set ComVisible(true) add a reference to COM library 'Microsoft Scripting Runtime', Register for Interop.
using Microsoft.VisualBasic;
using System;
using System.Runtime.InteropServices;
namespace BuryVBAErrorsCS
{
// Requires adding a reference to COM library Microsoft Scripting Runtime
// In AssemblyInfo.cs set ComVisible(true);
// In Build tab check 'Register for Interop'
public interface IDimensionsAndBounds
{
Scripting.Dictionary DimsAndBounds(Object v);
}
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IDimensionsAndBounds))]
public class CDimensionsAndBounds : IDimensionsAndBounds
{
public Scripting.Dictionary DimsAndBounds(Object v)
{
Scripting.Dictionary dicDimsAndBounds;
dicDimsAndBounds = new Scripting.Dictionary();
try
{
for (Int32 lDimensionLoop = 1; lDimensionLoop < 30; lDimensionLoop++)
{
long vLBound = Information.LBound((Array)v, lDimensionLoop);
long vUBound = Information.UBound((Array)v, lDimensionLoop);
string concat = (string)vLBound.ToString() + " " + (string)vUBound.ToString();
dicDimsAndBounds.Add(lDimensionLoop, concat);
}
}
catch (Exception)
{
}
return dicDimsAndBounds;
}
}
}
For Excel client VBA code here is some source
Sub TestCDimensionsAndBounds()
'* requires Tools->References->BuryVBAErrorsCS.tlb
Dim rng As Excel.Range
Set rng = ThisWorkbook.Worksheets.Item(1).Range("B4:c7")
Dim v As Variant
v = rng.Value2
Dim o As BuryVBAErrorsCS.CDimensionsAndBounds
Set o = New BuryVBAErrorsCS.CDimensionsAndBounds
Dim dic As Scripting.Dictionary
Set dic = o.DimsAndBounds(v)
Debug.Assert dic.Items()(0) = "1 4"
Debug.Assert dic.Items()(1) = "1 2"
Dim s(1 To 2, 2 To 3, 3 To 4, 4 To 5, 5 To 6)
Set dic = o.DimsAndBounds(s)
Debug.Assert dic.Items()(0) = "1 2"
Debug.Assert dic.Items()(1) = "2 3"
Debug.Assert dic.Items()(2) = "3 4"
Debug.Assert dic.Items()(3) = "4 5"
Debug.Assert dic.Items()(4) = "5 6"
Stop
End Sub
NOTE WELL: This answer handles grid variants pulled off a worksheet with Range.Value as well as arrays created in code using Dim s(1) etc.! Some of the other answers do not do this.
I like to use the fact that with an error, the new variable-value is not charged.
To get the dimension (A_Dim) of an Array (vArray) you can use following code:
On Error Resume Next
A_Dim = -1
Do Until A = "X"
A_Dim = A_Dim + 1
A = "X"
A = UBound(vArray, A_Dim + 1)
Loop
On Error GoTo 0
Function ArrayDimension(ByRef ArrayX As Variant) As Byte
Dim i As Integer, a As String, arDim As Byte
On Error Resume Next
i = 0
Do
a = CStr(ArrayX(0, i))
If Err.Number > 0 Then
arDim = i
On Error GoTo 0
Exit Do
Else
i = i + 1
End If
Loop
If arDim = 0 Then arDim = 1
ArrayDimension = arDim
End Function
I found a pretty simple way to check, probably laden with a bunch of coding faux pas, incorrect lingo, and ill advised techniques but never the less:
Dim i as Long
Dim VarCount as Long
Dim Var as Variant
'generate your variant here
i = 0
VarCount = 0
recheck1:
If IsEmpty(Var(i)) = True Then GoTo VarCalc
i = i + 1
GoTo recheck1
VarCalc:
VarCount= i - 1
Note: VarCount will obviously return a negative number if Var(0) doesn't exist. VarCount is the max reference number for use with Var(i), i is the number of variants you have.
What about just using ubound(var) + 1? That should give you the last element of most of variables (unless it's a custom range, but in that case you should know that info already). The range of a conventional variable (for instance, when using the split function) starts with 0; ubound gives you the last item of the variable. So if you have a variable with 8 elements, for instance, it will go from 0 (lbound) to 7 (ubound), and you can know the quantity of elements just adding ubound(var) + 1. For example:
Public Sub PrintQntElements()
Dim str As String
Dim var As Variant
Dim i As Integer
str = "Element1!Element2!Element3!Element4!Element5!Element6!Element7!Element8"
var = Split(str, "!")
i = UBound(var) + 1
Debug.Print "First element: " & LBound(var)
Debug.Print "Last element: " & UBound(var)
Debug.Print "Quantity of elements: " & i
End Sub
It will print this output to the Inmediate window:
First element: 0
Last element: 7
Quantity of elements: 8
Also, if you are not sure that the first element (lbound) is 0, you can just use:
i = UBound(var) - LBound(var) + 1

Excel Slider Control: How could I limit the sum of all sliders to, say, 100?

See image for clarity.
I have 5 variables (A, B, C, D and E), each of which can range from 0-100. I need the sum of all these variables to be 100 at all times, not more, not less. However, the way it is set up currently, if I change variable A from 21 to, say, 51, the total becomes 130.
How could I set this up such that if I change one variable, the others could automatically compensate for that increase or decrease, such that the total is always 100?
Use the Slider Change events, so that when one slider changes value the others are scaled so values sum to 100
Example code, using 3 sliders - you can scale it to allow for as many sliders as you want
Private UpdateSlider As Boolean
Private Sub ScaleSliders(slA As Double, ByRef slB As Double, ByRef slC As Double)
Dim ScaleFactor As Double
If (slB + slC) = 0 Then
ScaleFactor = (100# - slA)
slB = ScaleFactor / 2
slC = ScaleFactor / 2
Else
ScaleFactor = (100# - slA) / (slB + slC)
slB = slB * ScaleFactor
slC = slC * ScaleFactor
End If
End Sub
Private Sub ScrollBar1_Change()
Dim slB As Double, slC As Double
' UpdateSlider = False
If Not UpdateSlider Then
slB = ScrollBar2.Value
slC = ScrollBar3.Value
ScaleSliders ScrollBar1.Value, slB, slC
UpdateSlider = True
ScrollBar2.Value = slB
ScrollBar3.Value = slC
UpdateSlider = False
End If
End Sub
Private Sub ScrollBar2_Change()
Dim slB As Double, slC As Double
If Not UpdateSlider Then
slB = ScrollBar1.Value
slC = ScrollBar3.Value
ScaleSliders ScrollBar2.Value, slB, slC
UpdateSlider = True
ScrollBar1.Value = slB
ScrollBar3.Value = slC
UpdateSlider = False
End If
End Sub
Private Sub ScrollBar3_Change()
Dim slB As Double, slC As Double
If Not UpdateSlider Then
slB = ScrollBar1.Value
slC = ScrollBar2.Value
ScaleSliders ScrollBar1.Value, slB, slC
UpdateSlider = True
ScrollBar1.Value = slB
ScrollBar2.Value = slC
UpdateSlider = False
End If
End Sub
Note that sliders data type in integer, so you may need to allow for rounding not summing to exactly 100
Thx Chris for posting your solution. To scale it to six, I've made this. I'm no VBA expert, this code is not yet really clean or great. but it might help someone.
Private UpdateSlider As Boolean
Private Sub ScaleSliders_arr(slider_value As Double, ByRef other_sliders() As Double)
Dim scale_factor As Double
Dim total_other_sliders As Double
Dim element As Variant
Dim i As Integer
Dim other_sliders_arr_length As Long
For Each element In other_sliders
total_other_sliders = total_other_sliders + element
Debug.Print total_other_sliders
Next element
' when all other values are 0
If total_other_sliders = 0 Then
ScaleFactor = (100# - slider_value)
other_sliders_arr_length = ArrayLength(other_sliders)
i = 0
For Each element In other_sliders
other_sliders(i) = ScaleFactor / other_sliders_arr_length
i = i + 1
Next element
Debug.Print other_sliders_arr_length
' When other sliders have >0 as a total sum
Else
ScaleFactor = (100# - slider_value) / total_other_sliders
' Adjust other sliders according to current value
i = 0
For Each element In other_sliders
other_sliders(i) = other_sliders(i) * ScaleFactor
i = i + 1
Next element
End If
End Sub
Private Sub AdjustSliderByMagic(this_slider As Variant)
Dim slider_value As Double
Dim other_sliders() As Double
Dim cell_locations() As Variant
Dim other_sliders_arr_size As Integer
Dim value As Variant
Dim i As Integer
Dim k As Integer
' which cells contain the values - this also determines number of rows
cell_locations = Array("HiddenTable!B2", "HiddenTable!B3", "HiddenTable!B4", "HiddenTable!B5", "HiddenTable!B6", "HiddenTable!B7")
' size of the others is minus 2 because A) counting starts at 0 B) one slider is the current one which is not the other
other_sliders_arr_size = ArrayLength(cell_locations) - 2
' need to size the other sliders array
ReDim other_sliders(other_sliders_arr_size)
' start loops with 0's
i = 0
k = 0
' Determine the value of this slider and of the other sliders
For Each value In cell_locations
If this_slider = cell_locations(i) Then
slider_value = Range(cell_locations(i)).value
Else
other_sliders(k) = Range(cell_locations(i)).value
k = k + 1
End If
i = i + 1
Next value
' use function to determine slider values
ScaleSliders_arr slider_value, other_sliders
UpdateSlider = True
' start loops with 0's
i = 0
k = 0
' change the values of the other sliders
For Each value In cell_locations
If this_slider = cell_locations(i) Then
'do nothing
Else
Range(cell_locations(i)).value = other_sliders(k)
k = k + 1
End If
i = i + 1
Next value
End Sub
Private Sub ScrollBar1_Change()
Dim this_slider As Variant
' what is the connected field of this slider
this_slider = "HiddenTable!B2"
AdjustSliderByMagic (this_slider)
End Sub
Private Sub ScrollBar2_Change()
Dim this_slider As Variant
' what is the connected field of this slider
this_slider = "HiddenTable!B3"
AdjustSliderByMagic (this_slider)
End Sub
Private Sub ScrollBar3_Change()
Dim this_slider As Variant
' what is the connected field of this slider
this_slider = "HiddenTable!B4"
AdjustSliderByMagic (this_slider)
End Sub
Private Sub ScrollBar4_Change()
Dim this_slider As Variant
' what is the connected field of this slider
this_slider = "HiddenTable!B5"
AdjustSliderByMagic (this_slider)
End Sub
Private Sub ScrollBar5_Change()
Dim this_slider As Variant
' what is the connected field of this slider
this_slider = "HiddenTable!B6"
AdjustSliderByMagic (this_slider)
End Sub
Private Sub ScrollBar6_Change()
Dim this_slider As Variant
' what is the connected field of this slider
this_slider = "HiddenTable!B7"
AdjustSliderByMagic (this_slider)
End Sub
Function ArrayLength(arr As Variant) As Long
On Error GoTo eh
' Loop is used for multidimensional arrays. The Loop will terminate when a
' "Subscript out of Range" error occurs i.e. there are no more dimensions.
Dim i As Long, length As Long
length = 1
' Loop until no more dimensions
Do While True
i = i + 1
' If the array has no items then this line will throw an error
length = length * (UBound(arr, i) - LBound(arr, i) + 1)
' Set ArrayLength here to avoid returing 1 for an empty array
ArrayLength = length
Loop
Done:
Exit Function
eh:
If Err.Number = 13 Then ' Type Mismatch Error
Err.Raise vbObjectError, "ArrayLength" _
, "The argument passed to the ArrayLength function is not an array."
End If
End Function

Resources