PowerPoint VBA - Passing Shapes by value seems to be happening by reference - vba

For part of a VBA script I'm putting together I want to iterate through all the shapes on the current slide and insert another shape on top of each.
I have a first subroutine, GetShapes(), that gets all the shapes on the current slide and then passes them by value to a second subroutine, LabelShapes(), which adds the new shapes on top.
However, the new shapes seem to show up in the Shapes object that was passed. It seems like this should not be the case as it was passed by reference.
WARNING, the below will quickly lockup PowerPoint
Sub GetShapes()
Dim ss As Shapes
Set ss = Application.ActiveWindow.View.Slide.Shapes
Call LabelShapes(ss)
End Sub
Sub LabelShapes(ByVal ss As Shapes)
Dim s As Shape
For Each s In ss
Debug.Print s.Name
Application.ActiveWindow.View.Slide.Shapes.AddShape _
Type:=msoShapeRectangle, Left:=50, Top:=50, Width:=15, Height:=15
Next
End Sub
I imagine I can get around this by using a special naming convention for my new shapes and then filtering them out. Maybe there is a better way? But, really I would just like to understand why this isn't behaving the way I expect.

Not sure exactly what you're trying to do, but it's a common misunderstanding that passing object references ByVal would magically create a copy of the object.
Passing an object reference ByVal means you're passing a copy of the object pointer, as opposed to a reference to that very same object pointer.
In both cases, you're passing an object pointer that's pointing to the exact same object, so when you .AddShape, you're altering the very same shape collection you're in the middle of iterating.
Passing an object reference ByVal does NOT pass a copy of the object. If you want to pass a copy, you need to make a copy.
This might help clarify:
Public Sub DoSomething()
Dim obj As Object
Set obj = New Collection
TestByVal obj 'pass a copy of the object reference
Debug.Assert Not obj Is Nothing
TestByRef (obj) 'force a copy of the object reference (despite ByRef)
Debug.Assert Not obj Is Nothing
TestByRef obj 'pass a reference to the object pointer
Debug.Assert Not obj Is Nothing ' << assert will fail here
End Sub
Private Sub TestByVal(ByVal obj As Object)
Set obj = Nothing ' only affects the local copy
End Sub
Private Sub TestByRef(ByRef obj As Object)
Set obj = Nothing ' DANGER! call site will see this
End Sub

The solution is to use the ShapeRange object, which "represents a shape range, which is a set of shapes on a document."
Note from the Shapes documentation:
If you want to work with a subset of the shapes on a document — for example, to do something to only the AutoShapes on the document or to only the selected shapes — you must construct a ShapeRange collection that contains the shapes you want to work with.
Sub GetShapes()
Dim ss As ShapeRange
Set ss = Application.ActiveWindow.View.Slide.Shapes.Range
LabelShapes ss
End Sub
Sub LabelShapes(ByVal ss As ShapeRange)
Dim s As Shape
For Each s In ss
Debug.Print s.Name
Application.ActiveWindow.View.Slide.Shapes.AddShape _
Type:=msoShapeRectangle, Left:=50, Top:=50, Width:=15, Height:=15
Next
End Sub

Related

Word VBA Userform, Getting Control Object By Name fails sometimes

I have a userform “myUserForm” with dozens of controls whose TypeName() is “CheckBox”. I just hate having dozens of _Click() routines named like “Private Sub Chk1_Click()”, so in order to manage the quantity of _Click() routines, I simplified and made them nearly identical:
Private Sub Chk1_Click()
ProcessClickFor ("Chk1")
End Sub
Private Sub Chk2_A_Click()
ProcessClickFor ("Chk2_A")
End Sub
Private Sub Chk3_Z_Click()
ProcessClickFor ("Chk3_Z")
End Sub
ProcessClickFor() does most of the work.
Sub ProcessClickFor(anyCheckBox As String)
Dim cbControl As Object
Set cbControl = ControlByName(anyCheckBox)
If cbControl.Value Then
cbControl.Value = True
End If
End Sub
Later, when I want to work with any control, I can get the Control object by name, like:
Dim aControl As Object
Set aControl = ControlByName(“Chk3”)
MsgBox “The control named “ & cbControl.Name & “ is “ & cbControl.Visible
Function ControlByName(sName) As Object
Dim objectified As Object
For Each objectified In myUserForm.Controls
If objectified.Name = sName Then
Set ControlByName = objectified
Exit Function
End If
Next objectified
End Function
This works fine, almost, but it FAILS on the same four controls on myUserForm every time.
The failure “mode” is that ControlByName() seems to return successfully, but the first use of the returned control (such as my MsgBox) gives the error:
"Run-time error '91': Object variable or With block variable not set".
I verified that the spelling of the defined control names matches the names in my _Click() routines. Dozens of similarly designed CheckBox controls work perfectly. Could it have to do with the length of the CheckBox names or the number of “_” characters in the CheckBox names? Could there be a corrupt character in a CheckBox name? Can you think of other things for me to try?

PPT VBA Send trigger from ppt shape to Chart data

have formatted the code as below. Error comes as data member not found at ".Sheets(1).Range("A7").Value"
Sub DATA()
temp = 0
ActivePresentation.Slides(4).Shapes("Temp").TextFrame.TextRa‌​nge.Text = ActivePresentation.Slides(4).Shapes("Temp").TextFrame.TextRa‌​nge.Text +1 'counter to add +1
With ActivePresentation.Slides(4).Shapes("Bar1").Chart.ChartData
.Activate
.Sheets(1).Range("A7").Value = ActivePresentation.Slides(4).Shapes("Temp").TextFrame.TextRa‌​nge.Text
.Workbook.Close
End With
End Sub
The ChartData object refers to the entire Workbook, so you're probably getting an error like 438: Object does not support this property or method, because Cell is not a child of Workbook object.
NB also, Cell is not a child of Worksheet object, either, it's Cells or Range that you need, and you'll need to qualify that to a worksheet, like Sheets(1) (modify as needed).
So, try:
Dim val$
val = ActivePresentation.Slides(4).Shapes("temp").textrange.text
With ActivePresentation.Slides(4).Shapes("Bar1").Chart.ChartData
'.Activate
' something like this:
.Sheets(1).Range("A1").value = val
.Workbook.Close
End With
In theory you're supposed to be able to use a With block to manage the ChartData object. In practice, I have always had to actually Activate it and subsequently close it. YMMV.

Why is Object type instead of Sheet type used for variable declaration in the following VBA code?

The following code gets user back to the old sheet if a Chart is activated, and it shows how many data points are included in the Chart before getting back. And I wonder why the variable Sh is defined as Object rather than Sheet in the two event-handler procedures. Same for the variable OldSheet.
Dim OldSheet As Object
Private Sub Workbook_SheetDeactivate(ByVal Sh As Object)
Set OldSheet = Sh
End Sub
Private Sub Workbook_SheetActivate(ByVal Sh As Object)
Dim Msg As String
If TypeName(Sh) = "Chart" Then
Msg = "This chart contains "
Msg = Msg & ActiveChart.SeriesCollection(1).Points.Count
Msg = Msg & " data points." & vbNewLine
Msg = Msg & "Click OK to return to " & OldSheet.Name
MsgBox Msg
OldSheet.Activate
End If
End Sub
Because there is no such thing as a 'Sheet' in Excel.
Notice the event is SheetActivate, not WorksheetActivate - the concept of a "sheet" encompasses several types that have nothing in common, other than the ability to be "activated". There is no Sheet type in the Excel object model - the Workbook.Sheets collection contains various types of objects, including Chart and Worksheet objects.
The Sh parameter in the SheetActivate event has to be an Object, because there is no common interface between a Chart and a Worksheet.
So you need do what you did: verify the type of the object instead of assuming you're dealing with a Chart or a Worksheet object.
Instead of using the TypeName function and thus stringly-typed type checks, you should use the TypeOf operator instead:
Private Sub Workbook_SheetActivate(ByVal Sh As Object)
If TypeOf Sh Is Excel.Worksheet Then
Debug.Print "Worksheet!"
ElseIf TypeOf Sh Is Excel.Chart Then
Debug.Print "Chart!"
Else
Debug.Print "Something else!"
End If
End Sub
The parameter being Object allows future versions to activate a "sheet" of a type that couldn't be dreamed of at the time the event declaration was written in the IWorkbookEvents hidden interface that every Workbook object implements.
The short version of the answer is that it has to be declared as Object. The events are being "fired" through a COM source sink, and that returns an IDispatch pointer (known in VBA as Object) to anything that has a subscribed callback function. The ByVal Sh As Object parameter is passed to the callback function so that the event handler can determine which object was responsible for raising the event. It's declared in the Excel type library on dispinterface WorkbookEvents like this:
[id(0x00000619), helpcontext(0x0007ad30)]
void SheetActivate([in] IDispatch* Sh);
Even without considering the COM plumbing of its implementation, it has to be declared as Object because the Sheets collection holds both Worksheet and Chart objects, and the event will fire if either type of tab is activated. The two types don't share a common interface, but they do both source the same event. That means in order to pass the source object to the event handler, it has to be passed as late bound (IDispatch). The assumption is that the handler will determine what type of object it was passed and take the appropriate action based on the type of the sender.

VBA Looping through a Collection

I have a collection of files that I selected in the SelectManyFiles function and I want to run multiple private subs on each Drawing in the collection function. Here's my code:
Sub Main()
Dim Drawing As Object
Dim Drawings As Collection
Set Drawings = SelectManyFiles()
For Each Drawing In Drawings
'Call multiple private subs to run on each drawing
Next Drawing
End Sub
I think there's something wrong with the loop but not sure exactly! Any help is appreciated.
The collection that's returned by SelectManyFiles is not returning a collection of objects. It's probably returning a collection of Strings, but that's just a guess. Change your sub to this
Sub Main()
Dim Drawing As Variant
Dim Drawings As Collection
Set Drawings = SelectManyFiles()
For Each Drawing In Drawings
Debug.Print TypeName(Drawing)
Next Drawing
End Sub
And see what the Debug.Print gives you. If it's any scalar (string, long, double, Boolean, etc), then you need to declare Drawing as Variant. Only if all of the collection items are objects can you use Object.
TRY
FOR X = 1 TO DRAWING.COUNT
'STUFF HAPPENS
NEXT X

Forcing the unloading forms from memory

I am writing a solution in Excel that uses a number of linked data entry forms. To move between he sequence of forms, the user can click a "Previous" or "Next button. The current form is unloaded and the new one loaded and opened.
Sub NextForm(curForm As MSForms.UserForm, strFormName As String)
Dim intCurPos As Integer
Dim strNewForm As String
Dim newForm As Object
intCurPos = WorksheetFunction.Match(strFormName, Range("SYS.formlist"), 0)
If intCurPos = WorksheetFunction.CountA(Range("SYS.formlist")) Then
Debug.Print "No"
Else
Unload curForm
strNewForm = WorksheetFunction.Index(Range("SYS.formlist"), intCurPos + 1)
Set newForm = VBA.UserForms.Add(strNewForm)
newForm.Show
End Sub
The code as is allows new forms to be added into the sequence at any time through the editing of the range "SYS.formlist".
One problem I have noticed is that even after the current form is unloaded, it still remains in the VBA.Userforms collection. I would presume this is because this code has been called from that userform.
Is there a way to force the removal of that form from the VBA.Userforms collection? What is occuring is that if the user moves forward and then back, two copies of the form appear in memory and and excel throws exceptions about two modal forms being open.
Cheers,
Nick
The answer was (sadly) quite simple and inspired by bugtussle's answer.
The subroutine was passing the curForm variable as an MSForms.Userform object, but the form is held in memory as its own object type. (As an example, you can access a form through Set form = new formName)
So by changing the curForm paramater type to Variant, it will pass the actual object through rather than a copy of the object. Unload was only unloading the copy, not the actual object.
Thanks bugtussle!
So, corrected code is:
Sub NextForm(curForm As Variant, strFormName As String)
Dim intCurPos As Integer
Dim strNewForm As String
Dim newForm As Object
intCurPos = WorksheetFunction.Match(strFormName, Range("SYS.formlist"), 0)
If intCurPos = WorksheetFunction.CountA(Range("SYS.formlist")) Then
Debug.Print "No"
Else
Unload curForm
strNewForm = WorksheetFunction.Index(Range("SYS.formlist"), intCurPos + 1)
Set newForm = VBA.UserForms.Add(strNewForm)
newForm.Show
End Sub
I'm thinking that unloading from the collection object instead of the variable will really get rid of it. Try something like this:
For i = VBA.UserForms.Count - 1 To 0 Step -1
if VBA.UserForms(i).Name = curForm.name
Unload VBA.UserForms(i)
end if
Next i