10

I am attempting to create a function that will return a specific shape, based on the known Name property assigned to the CustomLayout.Shapes.Placeholder object. I can't use the shape .Name because this is not known in advance, even when creating slides from template/layout.

The challenge seems to be how the custom layout is related to the actual slide. For instance, when I iterate the slide's .CustomLayout.Shapes.Placeholders, I can easily identify the particular placeholder by it's .Name property.

HOWEVER if I return this shape, it will be the custom layout placeholder, which affects ALL slides on this layout (e.g., if I add text to this placeholder, it updates all slides using this layout!). Obviously this is undesirable!

If instead, I index the collection, and attempt to return the shape at that index position, from the slide's .Shapes.Placeholders, it appears that they are not maintaining the same index, i.e., .Shapes.Placeholders(i) <> .CustomLayout.Shapes.Placholders(i)

Attempted workaround:

Thought I might be able to manipulate the custom layout to add a Tag to the shapes. I tried, and it fails for the same reasons (i.e., the CustomLayout.Shape is somehow not the "same" shape as the Slide.Shape...). In any case, I'm hoping to avoid a "workaround" in favor of a more proper way to do this, if such a thing exists.

This is the function I have so far:

Function GetShapeByPlaceholderName(sName As String, sld As Slide) As Object
Dim plchldrs As Placeholders
Dim shp As Shape
Dim ret As Shape
Dim i As Long

For Each shp In sld.CustomLayout.Shapes.Placeholders
    i = i + 1
    If shp.Name = sName Then
    '#### 
    '    This can easily identify the CustomLayout.Shapes.PLACEHOLDER
    '
    '    But I need to return the SHAPE in the Slide.Shapes collection
    '####

        '###
        Set ret = shp  'This will return the CustomLayout.Placeholder, which affects ALL slides

        '###
        'Set ret = sld.Shapes.Placeholders(i) 'the index of the Shapes.Placeholders is NOT the same

        '###
        'Set ret = sld.Shapes.Placeholders.FindByName(sName) 'This returns an error/specified shape name does not exist

        '###
        'Set ret = sld.Shapes.Placeholders.FindByName(i) 'This observes same failure that the index of the collections is not the same


        Exit For
    End If
Next

Set GetShapeByPlaceholderName = ret

End Function
David Zemens
  • 53,033
  • 11
  • 81
  • 130
  • 1
    If you consider a copy of the PlaceHolder shape on the slide not the same, you may want to check all the `Name`, `ID`, `Type` & `AutoShapeType` to be the same for both shapes in the slide and in the placeholder. I did a rough test and all 4 matches if the shape on a slide is indeed from the placeholder. Not sure if it's the same on yours. – PatricK Mar 13 '15 at 06:27
  • @PatrickK I'm not worried about copies, I'm building slides from a template with several CustomLayouts. The only shapes on any slide are defined in the SlideMaster.CustomLayout. It seems impossible to identify the shapes by name (because PPT assigns whatever name it feels like) or placeholder name (because modifying this affects ALL slides using that layout), unless I'm missing something. – David Zemens Mar 13 '15 at 06:29
  • 3
    I am not very familiar with PPT's object model but +1 for well asked question and good answers. –  Mar 13 '15 at 19:46

3 Answers3

12

I have a potential solution for you.

The problem is the footer, page number, and date placeholders on the slide master. They are included in the placeholder collection on the slide master, but when an individual slide is created they become their own properties of the slide (under the .HeaderFooter property). This results in a different count of placeholders on the Master and on the slides, and because these placeholders can be in the middle of the collection, the indexes don't align.

So, one possible solution is to remove these three placeholders from your Master, which is done by opening the Slide Master and unchecking the footers checkbox. If you do this, you'll find that the number of placeholders on the Master and on the Slides are the same, and all of the index numbers line up. You still can't use the SlideMaster.CustomLayouts(n).Shapes.Placeholders(m).Name property to access the correct placeholder on the actual slide. However, once you know the index of the placeholder ("m" in my example in the last sentence), you should be able to access the correct placeholder on the slide via SlideObj.Shapes.PlaceHolders(m). You could iterate through your SlideMaster.Shapes.PlaceHolders first and store the index for later use.

If you want footer fields, simply add new Text placeholders to your slide master, put them at the bottom of the slide, and then insert the page number, date, or fixed text into them.

Summary:

  1. Uncheck the Footers checkbox on all slide masters that you care about. Not sure if this can be done programatically.

  2. Iterate through ActivePresentation.SlideMaster.CustomLayout(n).Shapes.Placeholders for each of your Slide Masters (Custom Layouts) looking at the .Name property to find the placeholder(s) you are interested in. Store that in an array (would use name of the placeholder as the array name, so if the placeholder name was "datatable" I would use datatable[n])=index # of the placeholder on the CustomLayout/Master. Do this once and store it in a global variable.

  3. When you want to access the placeholder on a slide, get the SlideMaster index for the slide with SM_index=SlideObj.CustomFormat.Index. Then access the placeholder "datatable" using SlideObj.Shapes.Placeholders(datatable[SM_index])

If you only have a single SlideMaster for all of your slides then you don't need an array and can use a simple variable instead.

If you need actual code, let me know -- but I expect you don't. Let me know if this works in your real world project.

hpf
  • 428
  • 2
  • 9
  • This sounds promising... I think a simpler solution using these concepts may be possible, I'll have to tinker with it, but +1 for the time being. Thanks! – David Zemens Mar 13 '15 at 18:41
  • So I did some tinkering but ultimately I'm not sure this is really going to solve my problem; of course I *could* manipulate the `CustomLayouts` to avoid using `HeadersFooters` but I just don't think this is really any better than a one time hash of the shapes (my current approach) and using a custom function. Perhaps it is *slightly* more versatile, but of course that doesn't prevent anyone else from changing the SlideMaster, which would then put everything awry, again. It's shocking, to say the least, that MS provides no way of "linking" or otherwise identifying a Shape by its Placeholder. – David Zemens Mar 18 '15 at 15:32
  • 1
    My only other thoughts are to loop through the placeholders on the master and save the Left and Top settings for each of the shapes you're interested in. Then, on the slides, you could compare Left/Top to find the right shape. But, if anyone moves anything, this fails. So pretty weak. One last "crazy" workaround is to loop through your shapes on the Master, and insert the name of the shape into the textframe. Then insert a new slide, and loop through the shapes looking at the contents of the textframes. Then delete the slide, and loop back and clear the text. It works... total hack. – hpf Mar 19 '15 at 00:24
  • That last comment is such a hack bit it just might work – David Zemens Mar 19 '15 at 00:25
  • No dice. Writing to the `.SlideMaster.CustomLayouts(i).Shapes.Placeholder.TextFrame.TextRange.Text`, though you can *visually* see the placeholder's text, is not a property that permeates through to the actual `Shape` in the slide's `.Shapes` collection. Same if I do `.SlideMaster.CustomLayouts(i).Shapes.TextFrame.TextRange.Text`... Confounding... – David Zemens Mar 19 '15 at 15:11
  • Same with `sld.CustomLayout.Shapes` and `sld.CustomLayout.Shapes.Placeholders`. :( – David Zemens Mar 19 '15 at 15:14
  • 2
    I feel pretty dumb. I wrote "it works", yet failed to actually check it. Since I could see the text there, I couldn't imagine MS would be DUMB enough to not have the text in the textrange object. Apparently, Microsoft has out-dumbed me again! – hpf Mar 19 '15 at 16:27
  • hahah yeah... man, it's just inconceivable that they created Placeholders without any way to identify the corresponding Shape, or vice-versa. Where this is an issue is an application that I'm modifying (I would've created it differently... but it is what it is). In this Excel sheet, the user specifies the *name* of a Placeholder, where they desire specific content should appear. But there's no way to *connect* the placeholder name to the shape that is produced on the slide! – David Zemens Mar 19 '15 at 16:51
  • Note also that even if this did work, it would not work for some certain placeholders, like Content Placeholders where `.HasTextFrame = False`. – David Zemens Mar 19 '15 at 16:52
  • But still, +1 to you and you'll probably get half the bounty unless someone else swoops in with a definitive answer before the deadline :) – David Zemens Mar 19 '15 at 16:52
3

My current workaround is to do the following:

Delcare a module-level Dictionary object, which creates a sort of hash table based on the slide's CustomLayout and the known index of each placeholder within the Slide.Shapes collection. (This I obtain through a simple FOr/Next iteration in a throwaway subroutine).

Since I am building slides from template, I think this is relatively safe and reliable, but not flexible (the whole point of working with POTX template files should be ease of use and flexibility...).

Dim dictShapes As Object 'Dictionary

Then a procedure to establish it based on CustomLayout

Sub SetShapeDict(cLayout as Object)

    Set dictShapes = CreateObject("Scripting.Dictionary")

    Select Case cLayout.Name
        Case "layout_one"
            dictShapes("chart RIGHT") = 1
            dictShapes("chart RIGHT title") = 2
            dictShapes("chart LEFT") = 5
            dictShapes("chart LEFT title") = 6
        Case "layout_two"
            dictShapes("chart RIGHT") = 1
            dictShapes("chart RIGHT title") = 2
            dictShapes("q text") = 4
            dictShapes("source text") = 5 
     End Select

 End Sub

I call this function like:

Dim shp as Object 'PowerPoint.Shape

Set shp = GetShapeByIndex(shp.Parent, dictShapes("chart RIGHT"))

The dictionary is initialized in such a manner that I can pass a string argument and it will return the index of the shape, which all should work.

Function GetShapeByIndex(chartSlide As Object, i As Long) As Object

    Dim ret
    Dim s As Long

    'if slide #1, there is no  "Slide Number Placeholder"
    ' this placeholder appears in the shapes' 3rd index for
    ' both Vertical Master no Background AND Horizontal Master

    If chartSlide.SlideNumber = 1 Then
        If i > 2 Then
            s = i - 1
        Else
            s = i
        End If
    Else
        s = i
    End If

    On Error Resume Next
    Set ret = chartSlide.Shapes(s)
    If Err.Number <> 0 Then Set ret = Nothing
    On Error GoTo 0

    Set GetShapeByIndex = ret

End Function
David Zemens
  • 53,033
  • 11
  • 81
  • 130
2

I have another workaround. I iterate trough all the shapes in the slide and compare them with some shape properties of the shape in the Custom Layout. I took width, height and autoshapetype. If they are exactly the same I have found the corresponding shape in the slide.

     For Each sh In sl.Shapes

        With sl.CustomLayout.Shapes("Name of shape in layout")
            If sh.Width = .Width And _
                sh.Height = .Height And _
                sh.AutoShapeType = .AutoShapeType Then
                bFound = True
                Exit For
            End If
        End With
    Next sh
    If bFound Then
        'sh is the shape you are looking for
    End If
  • Seems like you should probably also check the `sh.Left` and `sh.Top` at a bare minimum, because it's easy to imagine a slide which contains several similar or identical shapes, at different positions on the slide (arrows, callouts, textboxes, etc.). – David Zemens Mar 20 '17 at 13:55
  • I agree. You can add a lot of other properties to it to compare. Still it is not flawless! – martinitram Mar 21 '17 at 14:29
  • Right, it's definitely not flawless @martinitram and all of the approaches here so far just seem a bit "hack"-y to me. I can't believe that PPT doesn't contain any way of identifying a Shape by its Placeholder or vice-versa ¯\_(ツ)_/¯ – David Zemens Mar 21 '17 at 14:33