Application.Caller for Shapes with duplicate names - excel

I am using Application.Caller in a subroutine that I programmatically tied to the OnAction property of all the shapes I find on a worksheet. Application.Caller returns the name of the shape which initiated the call so that I can then obtain the appropriate shape object to process.
All of this is fine unless there is more than one shape on the sheet with the same name making it impossible to determine which is the caller. Excel manages the naming when inserting, copying and pasting shapes manually in a worksheet but these worksheets are populated through external apps which can cause this naming redundancy.
I am currently managing this by first scanning and renaming the redundant shapes so that I can identify them with the Application.Caller function. However, I do not want to rename them.
Code I've tried:
Set objShape = Application.Caller - unfortunately does not work
iShapeID = Application.Caller.ID - unfortunately does not work
iShapeID = ActiveSheet.Shapes(Application.Caller).ID - works but does not identify the correct caller when there are shapes with the same name
So, my question is: How can I obtain the proper Application.Caller shape object when there are redundantly named shapes on the worksheet?.
Put another way: Is there a way to cast the Application.Caller to a shape object without using the name of the shape returned by Application.Caller ideally using the ID property of the shape?

I don't think there is a an alternative for Application.Caller to return the ID property of the Shape or some other 'trick' to achieve what you want.
The work-around is to ensure that all your Shapes have unique names. If you have a sheet of names with duplicates you can quickly make them unique by re-naming them to preserve the original duplicate but add a suffix e.g. _1 to make them unique.
The sub could work like this (using a Dictionary to track the suffix value):
Sub MakeShapeNamesUnique(ws As Worksheet)
Dim shp As Shape
Dim dic As Object
Dim lng As Long
Set dic = CreateObject("Scripting.Dictionary")
'iterate shapes
For Each shp In ws.Shapes
' does shape name exist ?
If Not dic.Exists(shp.Name) Then
' add name to dictionary if not exists with counter of 0
dic.Add shp.Name, 0
Else
' found a duplicate
' increment counter
dic(shp.Name) = dic(shp.Name) + 1
' rename shape with suffix indicating dupe index
shp.Name = shp.Name & "_" & dic(shp.Name)
End If
Next shp
' job done - clean up the dictionary
Set dic = Nothing
End Sub
Here's the full test code that creates your issue and uses MakeShapeNamesUnique to work-around the problem. If you want to try it out, put it in a blank workbook because it will delete shapes out of the sheet before it starts:
Option Explicit
Sub Test1()
Dim ws As Worksheet
Dim shp As Shape
' reset shapes
Set ws = ThisWorkbook.Worksheets("Sheet1")
For Each shp In ws.Shapes
shp.Delete
Next shp
' add shape
With ws.Shapes.AddShape(msoShapeRectangle, 10, 10, 100, 100)
.Name = "Foo1"
.OnAction = "ShapeAction"
End With
' add another shape
With ws.Shapes.AddShape(msoShapeRectangle, 160, 10, 100, 100)
.Name = "Foo2"
.OnAction = "ShapeAction"
End With
' add another shape with duplicate name
With ws.Shapes.AddShape(msoShapeRectangle, 310, 10, 100, 100)
.Name = "Foo1"
.OnAction = "ShapeAction"
End With
' add another shape with duplicate name
With ws.Shapes.AddShape(msoShapeRectangle, 10, 160, 100, 100)
.Name = "Foo2"
.OnAction = "ShapeAction"
End With
' add another shape with duplicate name
With ws.Shapes.AddShape(msoShapeRectangle, 160, 160, 100, 100)
.Name = "Foo1"
.OnAction = "ShapeAction"
End With
' add another shape
With ws.Shapes.AddShape(msoShapeRectangle, 310, 160, 100, 100)
.Name = "Foo3"
.OnAction = "ShapeAction"
End With
' uniqueify shape names - comment out to replicate OP problem
MakeShapeNamesUnique ws
End Sub
Sub ShapeAction()
Dim shp As Shape
Set shp = Sheet1.Shapes(Application.Caller)
MsgBox " My name is: " & shp.Name & " and my ID is: " & shp.ID
End Sub
Sub MakeShapeNamesUnique(ws As Worksheet)
Dim shp As Shape
Dim dic As Object
Dim lng As Long
Set dic = CreateObject("Scripting.Dictionary")
'iterate shapes
For Each shp In ws.Shapes
' does shape name exist ?
If Not dic.Exists(shp.Name) Then
' add name to dictionary if not exists with counter of 0
dic.Add shp.Name, 0
Else
' found a duplicate
' increment counter
dic(shp.Name) = dic(shp.Name) + 1
' rename shape with suffix indicating dupe index
shp.Name = shp.Name & "_" & dic(shp.Name)
End If
Next shp
' job done - clean up the dictionary
Set dic = Nothing
End Sub

Counter must be unique, also when adding shapes between.
Sub MakeShapeNamesUnique(ws As Worksheet)
Dim shp As Shape
Dim dic As Object
Dim lng As Long
Set dic = CreateObject("Scripting.Dictionary")
'iterate shapes
For Each shp In ws.Shapes
' does shape name exist ?
If Not dic.Exists(shp.Name) Then
' add name to dictionary if not exists with counter of 0
dic.Add shp.Name, 0
Else
' found a duplicate
' increment counter (must be unique)
Do
dic(shp.Name) = dic(shp.Name) + 1
Loop Until Not dic.Exists(shp.Name & "_" & dic(shp.Name))
' rename shape with suffix indicating dupe index
shp.Name = shp.Name & "_" & dic(shp.Name)
End If
Next shp
' job done - clean up the dictionary
Set dic = Nothing
End Sub

Related

Vba, Programatically assign a macro to a "Shape" inside shapegroup

Thanks in advance, not sure why this wouldn't work.
I want to assign a macro to each button inside a shape group on load.
Inside Module:
Private Const SideNavName As String = "SideNav"
Public Sub SetSideNavigationOnAllSheets()
Dim ws As Worksheet
Dim oShape As Shape
For Each ws In ActiveWorkbook.Sheets
'check to see if sidenav shape/group exists in sheet
If Common.ShapeExists(ws, SideNavName) Then
' get side nav
For Each oShape In ws.Shapes(SideNavName).GroupItems
' only need the nav buttons not container
If Left(oShape.Name, 3) = "Nav" Then
Debug.Print ws.Name, oShape.Name
oShape.TextFrame.Characters.Text = "btn 1" ' pull from DB
oShape.OnAction = "'" & ActiveWorkbook.Name & "'!FolderSelectorButton" ' ERRORS OUT HERE
End If
'
Next
End If
Next
End Sub
Public Sub FolderSelectorButton()
Debug.Print 1
End Sub
Seems VBA doesn't like setting the OnAction property for Shapes that have been grouped. Solution is to store details of the group, ungroup it, update the OnAction property then re-create the group.
Replace your two lines setting the TextFrame and OnAction of the oShape object with the following:
' save then ungroup the Shapes
Dim oShpGrp As Shape, sShapeNames() As String, i As Long
Set oShpGrp = ws.Shapes(SideNavName)
ReDim sShapeNames(1 To oShpGrp.GroupItems.Count)
For i = 1 To oShpGrp.GroupItems.Count
sShapeNames(i) = oShpGrp.GroupItems.Item(i).Name
Next i
oShpGrp.Ungroup
' update Shape
oShape.TextFrame.Characters.Text = "btn 1" ' pull from DB
oShape.OnAction = "'" & ActiveWorkbook.Name & "'!FolderSelectorButton" ' ERRORS OUT HERE
' re-group the Shapes
Set oShpGrp = oShpGrp.Parent.Shapes.Range(sShapeNames).Group
oShpGrp.Name = SideNavName
This assumes that the group is a single-level group (ie it is not a group embedded within another group)

Group Shapes by Shape Object in VBA Excel

I'm having problems in grouping shapes by name with VBA in Excel.
This happens because I have multiple shapes that can have the same name.
The following code can recreate my problem.
You can uncomment line OriginalShape.Name = "MyShape" to see the error.
Sub test()
' Create Original Shape
Dim OriginalShape As Shape
Set OriginalShape = Sheet1.Shapes.AddShape(msoShapeRectangle, 5, 20, 50, 50)
' Rename Shape to simulate my project
' OriginalShape.Name = "MyShape" ' Uncomment line to recreate problem
' Copy and Paste Shape (I believe there is no other way to do this)
OriginalShape.Copy
Sheet1.Paste Sheet1.Range("C2")
' Get Object of Last Pasted Shape
Dim CloneShape As Shape
Set CloneShape = Sheet1.Shapes(Sheet1.Shapes.Count)
' Group Shapes
Dim ShapeGroup As Shape
Set ShapeGroup = Sheet1.Shapes.Range(Array(OriginalShape.Name, CloneShape.Name)).Group
End Sub
I know I also have the possibility to use Shape indexes, like Sheet1.Shapes.Range(Array(1, 2)).Group, but this is doesn't seem a good way either, as I would need to store one more variable for each shape (the shape index) apart from the shape Object.
Is there a way to group shapes some other way, like through Object or ID.
I believe the best would be something like.
Set ShapeGroup = Sheet1.Shapes.Range(Array(OriginalShape, CloneShape)).Group
'OR
Set ShapeGroup = Sheet1.Shapes.Range(Array(OriginalShape.ID, CloneShape.ID)).Group
Like Tim Williams said: the code fails, as the group-array consists of equal names. What you need to do, is adding the index to the name while creating the shapes
This will work:
Sub test()
Const cntShapes As Long = 2
Dim i As Long, shp As Shape, cTarget As Range
Dim arrShapeNames(1 To cntShapes) As Variant
With Sheet1
For i = 1 To cntShapes
Set cTarget = .Cells(1, i) 'adjust this to your needs
Set shp = .Shapes.AddShape(msoShapeRectangle, cTarget.Left, cTarget.Top, 50, 50)
shp.Name = "MyShape." & i 'adding the index to the name makes it unique
arrShapeNames(i) = shp.Name
Next
End With
' Group Shapes
Dim ShapeGroup As Shape
Set ShapeGroup = Sheet1.Shapes.Range(arrShapeNames).Group
End Sub

VBA Identification of Connected Shapes in Excel

I am trying to develop a VBA solution within Excel that can identify which shapes are connected to eachother within a worksheet via a standard connector line.
For example, in the snippet attached, I need to create a code that can identify that the control square is connected to the two red circles (titled Risk 1 and Risk 2) and output the following in a message box: "Risk 1 and Risk 2 are connected to Control". I have been able to find code to add connector lines however I cannot figure out how to identify connected shapes. Any guidance would be greatly appreciated! I have also attached the code that I have been able to find thus far.
Sub QuickConnect( )
Dim s1 As Shape, s2 As Shape, conn As Shape
' Create a shape
Set s1 = ActiveSheet.Shapes.AddShape(msoShapeCube, 100, 10, 50, 60)
' Create another shape
Set s2 = ActiveSheet.Shapes.AddShape(msoShapeCan, 50, 100, 50, 60)
' Create connector with arbitrary coordinates
Set conn = ActiveSheet.Shapes.AddConnector(msoConnectorCurve, 1, 1, 1, 1)
' Connect shapes
conn.ConnectorFormat.BeginConnect s1, 1
conn.ConnectorFormat.EndConnect s2, 1
' Connect via shortest path (changes connection sites)
conn.RerouteConnections
End Sub
Therefore you need to loop through all shapes, check if they are a connector (yes, connector lines are shapes too). And then you can check which shapes are connected by this connector line:
The property .ConnectorFormat.BeginConnectedShape gives you the shape at one end of the connector line and .ConnectorFormat.EndConnectedShape the shape on the other end.
Checkout this:
Option Explicit
Public Sub TestConnections()
Dim shp As Variant
For Each shp In Shapes 'loop through all shapes
If shp.Connector = msoTrue Then 'check if current shape is a connector
'BeginConnectedShape is the shape on the beginning side of the connector
'EndConnectedShape is the shape on the ending side of the connector
Debug.Print shp.Name _
& " connects " & _
shp.ConnectorFormat.BeginConnectedShape.Name _
& " with " & _
shp.ConnectorFormat.EndConnectedShape.Name
End If
Next shp
End Sub
For the following shapes
it outputs
Curved Connector 3 connects Cube 1 with Can 2
Curved Connector 6 connects Cube 5 with Can 2
You can use ConnectorFormat.EndConnectedShape property (Excel) and ConnectorFormat.BeginConnectedShape property (Excel) to achieve what you want.
LOGIC:
Loop through all connector shapes.
Create a Unique collection of shapes to which other shapes are connected with.
Get the Beginning and the Ending shape names.
Find the relation i.e WHO is connected to WHO.
CODE:
I have commented the code but if you still have questions then feel free to ask.
Option Explicit
'~~> Change this if your shapes include the below text
Const mySep As String = "MySep"
Sub Sample()
Dim ws As Worksheet
Dim shpConnector As Shape
Dim shpConnectorCount As Long
Dim i As Long: i = 1
Dim tmpAr As Variant, itm As Variant
Dim colConnector As New Collection
Dim msg As String
Dim finalOutput As String
'~~> Change this to the relevant sheet
Set ws = Sheet1
With ws
'~~> Count the number of connector shapes
For Each shpConnector In .Shapes
If shpConnector.Connector Then shpConnectorCount = shpConnectorCount + 1
Next shpConnector
'~~> If not found then exit sub
If shpConnectorCount = 0 Then Exit Sub
'~~> Resize array based on connector count
ReDim tmpAr(1 To shpConnectorCount)
For Each shpConnector In .Shapes
With shpConnector
If .Connector Then
'~~> Unique collection of shapes to which other
'~~> shapes are connected with
On Error Resume Next
colConnector.Add CStr(.ConnectorFormat.EndConnectedShape.Name), _
CStr(.ConnectorFormat.EndConnectedShape.Name)
On Error GoTo 0
'~~> Store Starting shape and End Shape in an array
tmpAr(i) = .ConnectorFormat.BeginConnectedShape.Name & mySep _
& .ConnectorFormat.EndConnectedShape.Name
i = i + 1
End If
End With
Next
'~~> Loop through the unique collection and the array to create
'~~> Our necessary output
For Each itm In colConnector
msg = ""
For i = LBound(tmpAr) To UBound(tmpAr)
If Split(tmpAr(i), mySep)(1) = itm Then
msg = msg & "," & Split(tmpAr(i), mySep)(0)
End If
Next i
finalOutput = finalOutput & vbNewLine & Mid(msg, 2) & " is/are connected to " & itm
Next itm
End With
MsgBox Mid(finalOutput, 2)
End Sub
IN ACTION:
SCREENSHOT:

Setting the font size in a shape

I have the following macro which is supposed to create a box linking to a certain worksheet in the workbook, on each sheet of the workbook:
Option Explicit
Sub gndhnkl()
Dim ws As Worksheet
Dim sh As Shape
For Each ws In ThisWorkbook.Worksheets
If InStr(1, ws.Name, "Summering", vbBinaryCompare) <= 0 Then
For Each sh In ws.Shapes
sh.Delete
Next sh
Call Macro1(ws)
End If
Next ws
End Sub
Sub Macro1(ws As Worksheet)
Dim venstre As Double, topp As Double, breidde As Double, høgde As Double
Dim sh As Shape
venstre = ws.Range("B16").Left
topp = ws.Range("B16").Top
breidde = 110
høgde = 68
Set sh = ws.Shapes.AddShape(msoShapeRoundedRectangle, venstre, topp, breidde, høgde)
With sh.TextFrame2.TextRange
.Characters.Text = "Til summering, person"
.Font.Size = 13
.ParagraphFormat.Alignment = msoAlignCenter
.Parent.VerticalAnchor = msoAnchorMiddle
End With
ws.Hyperlinks.Add Anchor:=sh, Address:="", SubAddress:=Replace(Summering_person.Range("A1").Address(external:=True), "[" & ThisWorkbook.Name & "]", "", 1, -1, vbBinaryCompare)
End Sub
For the most part it works just like I expect it too, but for some reason the font size in the added shape is not set to 13 as I expect, but remains 11.
I.e. it seems that the line .Font.Size = 13 (sh.TextFrame2.TextRange.Font.Size = 13) is not executed.
Where is my mistake here, and what do I need to do in order for the macro to set the font size for the shape?
You have to change the order, first set the font size (and any other font properties) before you write the text. Once the text is set, it's getting trickier to change the font - every character of the TextFrame may have it's own characteristics.
.Font.Size = 13
.Characters.Text = "Til summering, person"
Update The comment of SJR is right, when using the TextFrame rather than TextFrame2, you can set the font properties of the whole text as once after the text was written.

Excel 2003, VBA not deleting all OLE/shape controls

I've written a routine that deletes checkboxes and labels which are dynamically added to a sheet. However, it doesn't realiably delete all the controls. I need to ensure they are completely removed before adding again.
Here is my routine:
Public Sub removeOLEtypesOfType()
On Error Resume Next
Dim intPass As Integer, objShape As Shape
For intPass = 1 To 2
For Each objShape In ActiveSheet.Shapes
Dim strName As String
strName = objShape.Name
If Mid(strName, 1, Len(CHECKBOX_PREFIX)) = CHECKBOX_PREFIX _
Or Mid(strName, 1, Len(LABEL_PREFIX)) = LABEL_PREFIX _
Or Mid(strName, 1, 5) = "Label" Then
objShape.Delete
End If
Next
Next
End Sub
I only added the two pass for loop to ensure the objects are deleted, but even this doesn't delete the remaining items. The issue I have is that I end up with controls that were not deleted in the workbook.
I'm only trying to delete checkboxes and labels where in the case of checkboxes the name is prefixed with:
Public Const CHECKBOX_PREFIX As String = "chkbx"
Labels are prefixed with:
Public Const LABEL_PREFIX As String = "lbl"
The 3rd search comparing with 'Label' is an attempt to mop up but even this doesn't catch all.
Is there any way to delete all shapes / ole objects within a range?
Fixed, I rewrote the sub-routine after a google search on how to delete shapes within a range:
Public Sub removeOLEtypesOfType()
On Error Resume Next
Dim objTopLeft As Range, objBotRight As Range
Dim objRange As Range, objShape As Shape
Set objRange = Sheet1.Range(COLUMN_HEADINGS)
With objRange
Set objTopLeft = .Cells(1).Address(0, 0)
Set objBotRight = .cell(.Cells.Count).Address(0, 0)
For Each objShape In ActiveSheet.Shapes
If Mid(objShape.Name, 1, Len(CHECKBOX_PREFIX)) = CHECKBOX_PREFIX _
Or Mid(objShape.Name, 1, Len(LABEL_PREFIX)) = LABEL_PREFIX Then
If Not Intersect(objTopLeft, objShape.TopLeftCell) Is Nothing And _
Not Intersect(objBotRight, objShape.BottomRightCell) Is Nothing Then
objShape.Delete
End If
End If
Next
End With
End Sub

Resources