Consulting

Page 1 of 2 1 2 LastLast
Results 1 to 20 of 35

Thread: Header altered after running code

  1. #1
    Microsoft Word MVP 2003-2009 VBAX Guru gmaxey's Avatar
    Joined
    Sep 2005
    Posts
    3,333
    Location

    Header altered after running code

    I've have posted and been polishing code for a comprehensive find and replace Word add-in for several years. The current published verion is located here: http://gregmaxey.mvps.org/word_tip_p...d_replace.html

    Yesterday I learned that when the process is executed it introduces an empty paragraph into headers and footers that were previously blank.

    Steps to produce problem.

    1. Download and install the add-in
    2. Open an new Word document and type in a word or two.
    3. Call the add-in and use it to find one of the typed words.
    4. When finished the previously "empty" header and footer now contains an empty paragrph.

    I'm not sure what, if any, issues this may cause in printing documents with complex layouts, and I would like to understand the cause and find a solution.

    The cause appears to be the execution of a line of code that I have long understood is required to process linked header and footer storyranges and prevent an "empty" header or footer from breaking the process. This is explained here with the help of people far smarter than me:
    http://word.mvps.org/faqs/customizat...ceAnywhere.htm

    The line of code in my addin is located in the "Private Sub ProcessStoryTypes(ByVal oDocToProcess As Word.Document)" procedure of the UserInterface form and reads:

    lngJunk = oDocToProcess.Sections(1).Headers(1).Range.StoryType

    Can anyone explain why setting a long variable equal to a storytype value results in the physical change in the header (i.e., going from blank/empty to containing an empty paragraph)?

    While I don't understand the physical change, I think I will have to a) live with the fact that it occurs or b) accept that header and footer storyranges may not be fully processed if empty header/footers are involved.

    Option b being unacceptable, I did some further tinkerings and found that by simply toggling PrintPreview I can eliminate the empty paragraph and restore the header back to "empty." Once again being a tinkerer, I don't understand why this does what it does. Can anyone explain?

    To work around this problem I added a new variable bGhost to my process and then modified the code to check for multiple sections and only invoke the lngjunk line if they are present:

    [VBA]bGhost = False
    If oDocToProcess.Sections.Count > 1 Then
    lngJunk = oDocToProcess.Sections(1).Headers(1).Range.StoryType
    bGhost = True
    End If[/VBA]

    Then if invoked, I toggle print preview:

    [VBA]lbl_Exit:
    If bGhost = True Then
    CleanupGhostHeader oDocToProcess
    End If
    [/VBA]

    [VBA]Sub CleanupGhostHeader(ByRef oDoc As Word.Document)
    oDoc.PrintPreview
    oDoc.ClosePrintPreview
    End Sub
    [/VBA]

    The revised code is contained in the attachment (sorry can't attach a .dotm file).

    Would appreciate any insight/technical explaination anyone has on 1) Why is the lngjunk line really required, 2) Why does toggling print preview make the empty paragraph disappear, 3) A better way to resolve the issue.

    Thanks.
    Attached Files Attached Files
    Greg

    Visit my website: http://gregmaxey.com

  2. #2
    VBAX Master
    Joined
    Feb 2011
    Posts
    1,480
    Location
    I have always found it to be the case that a truly "empty" header/footer (where it only contains a paragraph mark) behaves differently when access via code vs. access via typical end-user.

    The easiest way to describe, for me, is...

    1) In a completely blank new document, no first page different... use code to reference the range (don't have to manipulate it... just reference it). Something like...
    Msgbox ActiveDocument.Sections(1).Headers(wdHeaderFooterPrimary).Range

    If you are showing paragraph marks, you will see a paragraph mark appear.

    2. If you subsequently double-click into that header showing the paragraph mark... Word will then remove the hidden paragraph mark.

    You can actually mimic this "double-click" behavior using the .SeekView methods of
    ActiveWindow.View.SeekView = wdSeekCurrentPageHeader
    and
    ActiveWindow.View.SeekView = wdSeekMainDocument

    (i.e., the above will make that empty paragraph mark disappear).

    In my experience, this has been consistent behavior for many many versions of Word.

    The strategies to handle it have been one or more of the following:

    1. Create an empty space in the header (this will make the paragraph mark "stick", since it is no longer perceived by Word as "empty" when an legitimate "end-user" action exits the Header (such as double-clicking the main document, or using .SeekView methodology).

    2. Use absolute margin values rather than relative values (negative values in the Top/Bottom margin values of page set up). Use of absolute values can be useful in removing the "bad" effect of the paragraph mark appearing and disappearing, which is an effective "jump" of main story text.

    Those are my top two work-arounds, I'm sure others might have some.

  3. #3
    VBAX Master
    Joined
    Feb 2011
    Posts
    1,480
    Location
    Hmm, further reading the link describing the problem... I would have to do some testing, but I suspect some of the work-arounds being dealt with may or may not exist in other versions of word.

    Specifically, I would question whether the "lngJunk" line fixes what seems to me to be a different issue.

    I wish they would document the actual bug in a reproduceable format, rather than just describe it. I love mvps, and it has a lot of great info... but I'm not sure all of the work-arounds are useful anymore.

    For example-- the "Flush bad wildcard karma" code is something I've used for years, but I actually don't see it breaking anymore, so I'm wondering if that issue is fixed. And I can't seem to recreate the problem-- is it my faulty memory, or did MS fix that issue and I can stop running the code that fixed that bug?

    Maybe others will have more insight... on the actual bug being dealt with. I have yet to download your addin, so when I do that I may have something specific to your code.

  4. #4
    VBAX Wizard
    Joined
    May 2004
    Posts
    6,713
    Location
    As Frosty mentions this has been an issue with Word for a long time. Technically speaking even a new "empty" header always contains a paragraph. You can check this by doing a paragraph count on a new "empty" header. So even if the mark is not visible, from VBA perspective there is a count = 1. This is because ANY range must have at least one paragraph mark.

    So, when you do smething like Frosty's simple messagebox in VBA, the action of using VBA makes the invisible paragraph become visible, as it is directly referenced.

    The visibility/non-visibility issue is buried deep in Word, and yes the use of PrintView toggles it. Further, I have found that if you ever put "real" text into a header (and then remove it) the "empty" now visible paragraph persists. PrintView can toggle it back to invisible, but not in all versions.

    I am not sure I would call it a bug.

  5. #5
    VBAX Master
    Joined
    Feb 2011
    Posts
    1,480
    Location
    Actually, I would say it doesn't exist... until you check it to see if it exists in a specific manner. This is some kind of Heisenberg Uncertainty Principle.

    Looking to see if it exists in a different way (for example, checking the .StoryRanges.Count property) proves that it doesn't exist.

    The way I've always understood it... for reasons unknown to me (probably some kind of memory efficiency thing), Word doesn't create the header range until it is actually "needed" -- which can be triggered in a couple of different ways.

    And Word "removes" the header range when it doesn't think the range is "needed" -- but only in certain cases when it thinks the end-user has performed the action. The simplest way of getting Word to behave in that way I described above: reference the range via VBA which creates the range and the blank paragraph... then double-click as an end-user into and back out of the header.

    Obviously, the easiest way to test this behavior is in a "clean" version of Word-- how your own Normal.dot is set up will cause differences in this behavior. But out of the box, my simple tests can be shown thusly...
    [vba]
    Sub SimpleStoryRangesDemo()
    Dim oTestDoc As Document
    Dim sTest As String

    Set oTestDoc = Documents.Add

    With oTestDoc
    'how many initial story ranges on a new document?
    MsgBox .StoryRanges.Count

    'add in the paragraph mark, and check ranges (increased)
    sTest = .Sections(1).Headers(wdHeaderFooterPrimary).Range.Text
    MsgBox .StoryRanges.Count

    'get rid of the paragraph mark and check (ranges removed)
    .ActiveWindow.View.SeekView = wdSeekCurrentPageHeader
    .ActiveWindow.View.SeekView = wdSeekMainDocument
    MsgBox .StoryRanges.Count

    'add in a new section and check (new ranges)
    .Sections.Add
    MsgBox .StoryRanges.Count

    'break the same as previous and check (creates new ranges)
    .Sections(2).Headers(wdHeaderFooterPrimary).LinkToPrevious = False
    MsgBox .StoryRanges.Count

    'turn the same as previous back on and check (doesn't remove the ranges)
    .Sections(2).Headers(wdHeaderFooterPrimary).LinkToPrevious = True
    MsgBox .StoryRanges.Count

    'delete the section and check (this does)
    .Sections(2).Range.Delete
    MsgBox .StoryRanges.Count

    .Saved = True
    .Close
    End With
    End Sub
    [/vba]

  6. #6
    VBAX Master
    Joined
    Feb 2011
    Posts
    1,480
    Location
    EDIT: Better test, with some explanation of the values... I'm still playing around with the testing (this is in 2003 at the moment, although I'll try in a couple of other scenarios).

    But I still think that the reason a "For...Each" loop on story ranges isn't reliable, is because the stories don't actually exist when a document is first created (i.e., the document created by a "Documents.Add" in a Word started via "winword /a" (no addins, no normal.dot loaded... so this is "vanilla" word).

    This is actually pretty interesting, at least to me... a couple of points:
    1. A for each loop is almost required, since not all story ranges exist all the time, and you are otherwise beholden to a For i = 1 to 16 (max number of stories) with an on error resume next requirement if a particular story range doesn't exist, but I think a better structure might be to actually specifically iterate through the named stories (that would allow some control of the order... so that troubleshooting is easier).

    2. The footnotes/endnotes separator and continuationseparator get created at the same time you the header/footers are created... but they don't get removed by either the seekview or th printpreview method (haven't found a way to get back to 1 storyrange yet).

    [vba]
    Sub SimpleStoryRangesDemo()
    Dim oTestDoc As Document
    Dim sTest As String
    Dim bForEachLoop As Boolean

    Set oTestDoc = Documents.Add

    bForEachLoop = True

    With oTestDoc
    .ActiveWindow.View.ShowAll = True

    'how many initial story ranges on a new document?
    Debug.Print .StoryRanges.Count '1
    MsgBox fReportStoryRangesAvailable(bForEachLoop)

    'add in the paragraph mark in the header, creates all the ranges
    sTest = .Sections(1).Headers(wdHeaderFooterPrimary).Range.Text
    Debug.Print .StoryRanges.Count '11
    MsgBox fReportStoryRangesAvailable(bForEachLoop)

    'get rid of the paragraph mark and check (some ranges removed)
    .ActiveWindow.View.SeekView = wdSeekCurrentPageHeader
    .ActiveWindow.View.SeekView = wdSeekMainDocument
    Debug.Print .StoryRanges.Count '5
    MsgBox fReportStoryRangesAvailable(bForEachLoop)

    'add in a new section and check (no new ranges)
    .Sections.Add
    Debug.Print .StoryRanges.Count '5
    MsgBox fReportStoryRangesAvailable(bForEachLoop)

    'break the same as previous and check (creates new ranges)
    .Sections(2).Headers(wdHeaderFooterPrimary).LinkToPrevious = False
    Debug.Print .StoryRanges.Count '11
    MsgBox fReportStoryRangesAvailable(bForEachLoop)

    'turn the same as previous back on and check (doesn't remove the ranges)
    .Sections(2).Headers(wdHeaderFooterPrimary).LinkToPrevious = True
    Debug.Print .StoryRanges.Count '11
    MsgBox fReportStoryRangesAvailable(bForEachLoop)

    'delete the first section
    .Sections(1).Range.Delete
    Debug.Print .StoryRanges.Count '11
    MsgBox fReportStoryRangesAvailable(bForEachLoop)

    'get rid of the paragraph mark and check again
    .ActiveWindow.View.SeekView = wdSeekCurrentPageHeader
    .ActiveWindow.View.SeekView = wdSeekMainDocument
    Debug.Print .StoryRanges.Count '5
    MsgBox fReportStoryRangesAvailable(bForEachLoop)

    .PrintPreview
    .ClosePrintPreview
    Debug.Print
    MsgBox fReportStoryRangesAvailable(bForEachLoop)

    .Saved = True
    .Close
    End With
    End Sub
    'Return a string about the stories that exist in the document, optional to show a messagebox
    Function fReportStoryRangesAvailable(Optional bUseForEachLoop) As String
    Dim sRet As String
    Dim rngStory As Range
    Dim i As Integer

    'use a for each loop
    If bUseForEachLoop Then
    For Each rngStory In ActiveDocument.StoryRanges
    sRet = sRet & fGetStoryRangeReport(rngStory)
    Next
    Else
    For i = 1 To 16
    On Error Resume Next
    sRet = sRet & fGetStoryRangeReport(ActiveDocument.StoryRanges(i))
    Next
    End If

    fReportStoryRangesAvailable = sRet
    End Function
    Function fGetStoryRangeReport(rngStory As Range, Optional sDelimiter As String = " exists" & vbCr) As String
    Dim sRet As String
    Select Case rngStory.StoryType
    Case wdMainTextStory
    sRet = sRet & "wdMainTextStory" & sDelimiter

    Case wdPrimaryHeaderStory
    sRet = sRet & "wdPrimaryHeaderStory" & sDelimiter
    Case wdFirstPageHeaderStory
    sRet = sRet & "wdFirstPageHeaderStory" & sDelimiter
    Case wdEvenPagesHeaderStory
    sRet = sRet & "wdEvenPagesHeaderStory" & sDelimiter

    Case wdPrimaryFooterStory
    sRet = sRet & "wdPrimaryFooterStory" & sDelimiter
    Case wdFirstPageFooterStory
    sRet = sRet & "wdFirstPageFooterStory" & sDelimiter
    Case wdEvenPagesFooterStory
    sRet = sRet & "wdEvenPagesFooterStory" & sDelimiter

    Case wdTextFrameStory
    sRet = sRet & "wdTextFrameStory" & sDelimiter

    Case wdCommentsStory
    sRet = sRet & "wdCommentsStory" & sDelimiter

    Case wdEndnotesStory
    sRet = sRet & "wdEndnotesStory" & sDelimiter
    Case wdEndnoteSeparatorStory
    sRet = sRet & "wdEndnoteSeparatorStory" & sDelimiter
    Case wdEndnoteContinuationNoticeStory
    sRet = sRet & "wdEndnoteContinuationNoticeStory" & sDelimiter
    Case wdEndnoteContinuationSeparatorStory
    sRet = sRet & "wdEndnoteContinuationSeparatorStory" & sDelimiter

    Case wdFootnotesStory
    sRet = sRet & "wdFootnotesStory" & sDelimiter
    Case wdFootnoteSeparatorStory
    sRet = sRet & "wdFootnoteSeparatorStory" & sDelimiter
    Case wdFootnoteContinuationNoticeStory
    sRet = sRet & "wdFootnoteContinuationNoticeStory" & sDelimiter
    Case wdFootnoteContinuationSeparatorStory
    sRet = sRet & "wdFootnoteContinuationSeparatorStory" & sDelimiter

    End Select
    fGetStoryRangeReport = sRet
    End Function
    [/vba]
    Last edited by Frosty; 05-17-2012 at 12:20 PM.

  7. #7
    VBAX Wizard
    Joined
    May 2004
    Posts
    6,713
    Location
    For those who are confused by headers/footers Frosty's code demonstrates very well the multiples existence - NOT the use thereof - of ALL the header/footer ranges within any given Section.

    When the header/footer range is created HeaderPrimary, HeaderFirstPage, HeaderOddEven, FooterPrimary, FooterFirstPage and FooterOddEven are all created - thus the jump in number from 1 to 7 in the first code.

    Sections have ALL of them, or none as the code shows. If there is any header range at all, then all SIX types of headers and footers are there as well. Whether there is content or not. Even if you do not have anything in HeaderFirstPage, it exists.

    Which is why using .Exists for it is a incorrect way to test.[vba]Sub FirstPageYadda_Test1()
    Dim oTestDoc As Document
    Dim sTest As String

    Set oTestDoc = Documents.Add

    If oTestDoc.Sections(1).Headers(2).Exists Then
    MsgBox "Exists" & oTestDoc.Sections(1).Headers(2).Range.Text
    Else
    MsgBox "Does not exist."
    End If

    With oTestDoc
    .Saved = True
    .Close
    End With
    End Sub[/vba]The above returns a "Does not exist." - .Exists = False. FirstPage is not visible, all you see is Primary.

    [vba]
    Sub FirstPageYadda_Test2()
    Dim oTestDoc As Document
    Dim sTest As String

    Set oTestDoc = Documents.Add

    ' programmatically add text to FirstPage
    oTestDoc.Sections(1).Headers(2).Range.Text = "Yadda"

    If oTestDoc.Sections(1).Headers(2).Exists Then
    MsgBox "Exists" & oTestDoc.Sections(1).Headers(2).Range.Text
    Else
    MsgBox "Does not exist, but the text is: " & _
    oTestDoc.Sections(1).Headers(2).Range.Text
    End If


    With oTestDoc
    .Saved = True
    .Close
    End With
    End Sub
    [/vba]The above still returns a "Does not exist." - .Exists = False - BUT also returns the contents of the non-existent FirstPage. FirstPage is not visible, all you see is Primary.

    .Exists test if in Page Setup DifferentFirstPage is on, or off. NOT the header itself.

  8. #8
    VBAX Master
    Joined
    Feb 2011
    Posts
    1,480
    Location
    Ahh... I've discovered the real "bug" in this process.

    You can't use a For...Each loop on the story ranges reliably as your top loop. This is because the StoryRanges collection is based on what has been created in Section 1 of a document. So consider the following code:

    [vba]
    Sub SetUpBadDocument()

    Documents.Add

    ActiveDocument.Sections.Add
    ActiveDocument.Sections(2).Headers(wdHeaderFooterPrimary).LinkToPrevious = False
    ActiveDocument.Sections(2).Headers(wdHeaderFooterPrimary).Range.text = "Hello, you can't find me"

    MsgBox ActiveDocument.StoryRanges.Count

    ActiveDocument.ActiveWindow.View.SeekView = wdSeekCurrentPageHeader
    ActiveDocument.ActiveWindow.View.SeekView = wdSeekMainDocument

    MsgBox ActiveDocument.StoryRanges.Count

    End Sub
    [/vba]
    It doesn't appear to me that .LinkToPrevious really has anything to do with it... it depends on how the first section is set up, which may or may not adversely impact your "For Each..." loop of story ranges. (i.e., the end-user, through normal word-usage, may cause the Header/Footer story ranges for the first section to "disappear" if those headers/footers are empty... but all subsequent sections may actually have text in them, but will not be described in the storyranges collection). This may very well have broader implications for other story ranges in multi-section documents as well.

    I think you have to do something along the following:
    [vba]
    'Really cycle through all story ranges
    Sub IterateThroughAllStories_FORREAL()
    Dim rngStory As Range
    Dim iRangesSearched As Integer

    'cycle through all stories
    For Each rngStory In ActiveDocument.StoryRanges
    Select Case rngStory.StoryType
    Case wdPrimaryHeaderStory, wdFirstPageHeaderStory, wdEvenPagesHeaderStory, _
    wdPrimaryFooterStory, wdFirstPageFooterStory, wdEvenPagesFooterStory
    'don't handle headers/footers in this loop, because it can't be relied upon
    'if we find
    Case Else
    iRangesSearched = iRangesSearched + 1
    iRangesSearched = iRangesSearched + fIterateShapeRangesIn(rngStory)
    End Select
    Next

    'headers/footers can be relied upon in this loop
    iRangesSearched = fIterateHeaderFooterRanges

    MsgBox "Number of ranges searched: " & iRangesSearched, , "Story Tester"
    End Sub
    'return the number of headers/footers iterated
    Function fIterateHeaderFooterRanges() As Integer
    Dim oSec As Section
    Dim hf As HeaderFooter
    Dim rngWhere As Range
    Dim iRet As Integer

    For Each oSec In ActiveDocument.Sections
    'cycle through non-linked headers
    For Each hf In oSec.Headers
    If hf.LinkToPrevious = False Then
    Set rngWhere = hf.Range
    iRet = iRet + 1
    iRet = iRet + fIterateShapeRangesIn(rngWhere)
    End If
    Next

    'and non-linked footers
    For Each hf In oSec.Footers
    If hf.LinkToPrevious = False Then
    Set rngWhere = hf.Range
    iRet = iRet + 1
    iRet = iRet + fIterateShapeRangesIn(rngWhere)
    End If
    Next
    Next
    'return the number of ranges iterated through
    fIterateHeaderFooterRanges = iRet
    End Function
    Function fIterateShapeRangesIn(rngToSearch As Range) As Integer
    Dim oShape As Shape
    Dim iRet As Integer

    For Each oShape In rngToSearch.ShapeRange
    If oShape.TextFrame.HasText Then
    iRet = iRet + 1
    End If
    Next
    fIterateShapeRangesIn = iRet
    End Function
    [/vba] I don't think you can get away from "resetting" the blank paragraph problem, because there is no way to programmatically reference an "empty" header/footer without leaving a "mark" (so to speak *grin*)... but you can remove the marks the way we've described earlier.

    And then you have to make particular story range loops more robust if the are ranges which are likely/possible to have "nested" ranges within them (i.e., you need to check the ShapeRange collection within each of those ranges).

    But, fundamentally, the For Each... loop on word story ranges isn't reliable.
    EDIT: added in the method to iterate the text frames using the .HasText property (which doesn't cause a paragraph mark to appear in a previously "untouched" shape... if I check for a single vbcr on the .Text property, I start having paragraph marks appear in the shapes, which is annoying).
    Last edited by Frosty; 05-17-2012 at 02:31 PM.

  9. #9
    VBAX Master
    Joined
    Feb 2011
    Posts
    1,480
    Location
    Hmm, sometimes one should just RTFM.

    The link is good. The line of code about the lngJunk is necessary, because of situations like the one created by my "SetUpBadDocument" code above. And I'm sure it's okay to "rejiggle" the document so that the blank paragraph created by the lngJunk line of code is safely removed, after having used it in order to iterate through the story ranges.

    I still think there is something fundamentally flawed about the approach, but that may be because I didn't think of it

    Do Loops within For Each loops where you're using the Do Loop to set the iterator of the For Each loop is goofy to me. I would never instinctively go there.
    That it works... is great, but it's still a goofy approach (to me).

    Either a for each loop works because the collection describes what it should, or the collection isn't right. Why does the storyranges collection also get populated with a couple of the footnote/endnote things by the lngJunk line of code? And why can I remove the header/footer related story ranges with the .SeekView process, but not the footnote/endnote story ranges?

    That's all a bit goofy... especially since I don't think I'd particularly care about doing a find/replace all on the endnoteseparator story range anyway... which then begs the question, is the StoryRanges collection really helping, or obsfuscating my code?

    Personally, I would rather iterate through the sections, and the hf.range objects within those sections...if for no other reason than I understand better what it's doing. I don't know how story ranges work (or don't work) well enough, I guess.

    And as soon as I see a bug in the collection, which is easily replicable... I start thinking that entire area of the object model is something to avoid as a general rule, since it's simply not reliable. And I know enough (now) to know that getting a StoryRanges loop to work is so goofy, I would probably just not design a "SuperFindReplaceAll" function which uses StoryRanges.

    BUT-- thanks for bringing up this bug, I learned a bit about an area I wasn't that familiar with.

  10. #10
    VBAX Wizard
    Joined
    May 2004
    Posts
    6,713
    Location
    Agreed. I thought StoryRanges to be goofy right from the get go.

    "it depends on how the first section is set up"

    It is worse than that. I understand it, but I think it is very unintuitive to have THREE Sections, with different content in the headers, delete #3 Section, then #2 Section...and Section #1 header now has the content from what was Section #3 header.

    How intuitive is THAT?

  11. #11
    VBAX Master
    Joined
    Feb 2011
    Posts
    1,480
    Location
    Well, it would be intuitive to people who used word 6.0... Because deleting the paragraph mark worked the same way as deleting the section break... The one deleted is the formatting you lost. But the MS kluge to retain the "top" paragraph formatting (even though that's the paragraph mark you delete) made the section level stuff a little harder to explain.

    That said, my favorite screwy scenarios always involve something like the following:
    Section 1, first page different, content in both visible headers.
    Section 2, no different first page, no link to previous, content in header
    Section 3, different first page... Linked to previous... Delete content... Wonder why section 1 gets affected but not section 2 ("but I checked link to previous!")

    This is why, in my opinion, it's dangerous to have macros automatically adjust stuff in "all" headers/footers... Because you can create metadata situations which won't be picked up by any meta sweeper function

  12. #12
    VBAX Wizard
    Joined
    May 2004
    Posts
    6,713
    Location
    This is why, in my opinion, it's dangerous to have macros automatically adjust stuff in "all" headers/footers... Because you can create metadata situations which won't be picked up by any meta sweeper function
    Agreed.

    Also agree with comment re: Word 6.0, which I used. I was thinking of those whose Word experience does go back that far. The assumption is that if you have content in Section X header...it stays that way unless you change it.

    Re; the three Section scenario you mention, LOL. Yes, that is also not very intuitive, is it?

    I ti si no wonder that of all things in Word, I have found that headers and footers is one of the most confusing for people.

  13. #13
    Microsoft Word MVP 2003-2009 VBAX Guru gmaxey's Avatar
    Joined
    Sep 2005
    Posts
    3,333
    Location
    Jason, Gerry

    I was out all yesterday and until late at night so I've just had a chance to glance through your comments. Most of the stuff about storyrange count etc. I've been familiar with but never seen it demonstrated so well. Good job Jason.

    While I don't necessarily agree that storyranges are goofey, I'll tinker with your alternative method and see how it performs in the add-in.

    Thanks.
    Greg

    Visit my website: http://gregmaxey.com

  14. #14
    VBAX Master
    Joined
    Feb 2011
    Posts
    1,480
    Location
    I think certain story ranges are really helpful... But it seems that the issue with story ranges and headers/footers makes a for each loop goofy. To me. Just to clarify

    But, knowing what I know now... I could certainly see using them to handle the ones that work. But do they all have the same limitation (I haven't yet researched). Does the comments story range not show up if you have comments starting in section 2? Text frame?

    Since I know it can't be trusted on at least one area (which we have a kluge for), what about all the others (and my structure doesn't address that either)

  15. #15
    VBAX Master
    Joined
    Feb 2011
    Posts
    1,480
    Location
    I don't understand the TextFrame story.. using the document I created with "SetupBadDocument" -- I create two comments in section 2, and 2 text boxes in section 2.

    The comments story appears to be useful, as does the footnotes story (as I add additional items, the .Text of the storyrange appropriately encompasses the whole story). I'm guessing that endnotes will work similarly. The story range is created when I first enter a comment or a footnote, even though it is in the second section...

    So a Find/Replace which searches that particular story would work.

    However, the TextFrame storyrange does not behave this way... the .Text of that range is only the text of the first text box created (I wouldn't be surprised to find that linked text boxes may expand some of this, but I don't think it really matters). So, from this, it seems to me that the following story ranges are useful for the purposes of an expanded find/replace:
    MainStory
    FootnotesStory
    EndnotesStory
    CommentsStory

    And for headers/footers, I would approach by cycling through each section and each header/footer in that section, and anything not-linked I'd check that range.

    And then I would check the .ShapeRange of the main story and each of the header/footer ranges above which would take care of textboxes in all the locations we care about.

    A quick test shows me I can't insert (at least in 2010), comments or footnotes/endnotes into headers and footers, so I don't have to worry about that.

    I guess, after that... then I'd start looking for whacky ways that someone could put text somewhere that the word native replaceall wouldn't work, and my function would, and I'd demo that.

    And then maybe MS would fix their find/replace? Hehe

  16. #16
    Microsoft Word MVP 2003-2009 VBAX Guru gmaxey's Avatar
    Joined
    Sep 2005
    Posts
    3,333
    Location
    Jason,

    I created a document using your SetupBadDocument and placed two text boxes in section 2.

    Running the following code with the
    the text in both boxes are found and replaced:

    [vba]Sub Test()
    Dim oStory As Range
    For Each oStory In ActiveDocument.StoryRanges
    Do '***
    With oStory.Find
    .Text = "Test"
    .Replacement.Text = "Test Sat"
    .Execute Replace:=wdReplaceAll
    End With
    Set oStory = oStory.NextStoryRange '***
    Loop Until oStory Is Nothing '***
    Next
    End Sub
    [/vba]

    This is how my VBA find and replace addin is set up.

    If you stet out the '*** lines then only the first textbox is processed. Look up NextStoryRange in the help and you should follow what is happening.
    Greg

    Visit my website: http://gregmaxey.com

  17. #17
    VBAX Master
    Joined
    Feb 2011
    Posts
    1,480
    Location
    I know it works, I just think it's funky to read... because a nested Do...Loop contained in a For Each...Loop solely for the purpose of iterating "correctly" through the collection is a confusing logic structure. And if there's ever a bug to be troubleshot (a particularly quirky story range), you have to do a lot of breaking apart to figure out which story has broken.

    The following code also works for that specific scenario (and is doing the exact same thing, except that it's spelling out what it's actually doing).
    [vba]
    Sub Test()
    Dim oShape As Shape
    For Each oShape In ActiveDocument.StoryRanges(wdMainTextStory).ShapeRange
    With oShape.TextFrame
    If .HasText Then
    With .TextRange.Find
    .text = "Here I am"
    .Replacement.text = "Found it"
    .Execute Replace:=wdReplaceAll
    End With
    end if
    End With
    Next
    End Sub
    [/vba] In addition, since neither of those blocks of code handles the text box anchored to the "Hello you can't find me" header (even with the addition of "lngJunk" type code, to make the header/footer story ranges appear), the funky structure doesn't provide additional functionality over something which is more clear and spells out its limitations (wdMainTextStory).

    Just my two cents-- all things can be spelled out via comments, but it would be a very long comment (and a link to a help file) to explain what is being achieved by the Do... Loop and .NextStoryRange structure.

  18. #18
    VBAX Master
    Joined
    Feb 2011
    Posts
    1,480
    Location
    The following even makes more sense to me... as an alternative (I know, I need to take a look at how your addin is structured... I promise, I will).
    [VBA]
    Sub Test3()
    Dim rngStory As Range

    Set rngStory = ActiveDocument.StoryRanges(wdTextFrameStory)
    Do Until rngStory Is Nothing
    With rngStory.Find
    .text = "Here I am"
    .Replacement.text = "Found it"
    .Execute Replace:=wdReplaceAll
    End With
    Set rngStory = rngStory.NextStoryRange
    Loop
    End Sub
    [/vba]

  19. #19
    VBAX Master
    Joined
    Feb 2011
    Posts
    1,480
    Location
    Here's a quick proof-of-concept... basically, I just approach it as a way to get a "real" collection of StoryRanges in the document...
    [vba]
    'Proof of concept
    Public Sub RealReplace()
    Dim rngSearch As Range

    For Each rngSearch In StoryRanges_Real
    With rngSearch.Find
    .text = "Can you find me?"
    .Replacement.text = "Yes I can"
    .Execute Replace:=wdReplaceAll
    End With
    Next
    End Sub
    'The MS StoryRanges collection is fundamentally broken, this is an attempt to generate an accurate
    'range of stories for the entire document
    Public Function StoryRanges_Real() As Collection
    Dim colRet As Collection
    Dim rngStory As Range
    Dim hf As HeaderFooter
    Dim oSec As Section

    Set colRet = New Collection
    'these are the easy ones
    For Each rngStory In ActiveDocument.StoryRanges
    Select Case rngStory.StoryType
    Case wdCommentsStory, wdFootnotesStory, wdEndnotesStory
    colRet.Add rngStory

    Case wdMainTextStory
    colRet.Add rngStory
    'we also have shape ranges to deal with
    AddShapeRangesIn rngStory, colRet

    Case Else
    'anything not on the above list, we need to add separately
    End Select
    Next

    'Headers/Footers
    For Each oSec In ActiveDocument.Sections
    For Each hf In oSec.Headers
    If hf.LinkToPrevious = False Then
    colRet.Add hf.Range
    'also add shape ranges
    AddShapeRangesIn hf.Range, colRet
    End If
    Next

    For Each hf In oSec.Footers
    If hf.LinkToPrevious = False Then
    colRet.Add hf.Range
    'also add shape ranges
    AddShapeRangesIn hf.Range, colRet
    End If
    Next
    Next
    Set StoryRanges_Real = colRet
    End Function
    'Adds any shape ranges to the collection passed in
    Public Sub AddShapeRangesIn(rngWhere As Range, colRet As Collection)
    Dim oShape As Shape

    For Each oShape In rngWhere.ShapeRange
    If oShape.TextFrame.HasText Then
    colRet.Add oShape.TextFrame.TextRange
    End If
    Next
    End Sub
    [/vba]

  20. #20
    Microsoft Word MVP 2003-2009 VBAX Guru gmaxey's Avatar
    Joined
    Sep 2005
    Posts
    3,333
    Location
    Jason,

    That seems to work to find anything that I can throw at it. I just added an error handler:

    [VBA]Public Sub AddShapeRangesIn(rngWhere As Range, colRet As Collection)
    Dim oShape As Shape
    On Error Resume Next 'Added GKM
    For Each oShape In rngWhere.ShapeRange
    If oShape.TextFrame.HasText Then
    colRet.Add oShape.TextFrame.TextRange
    End If
    Next
    On Error GoTo 0 'Added GKM
    End Sub
    [/VBA]
    Greg

    Visit my website: http://gregmaxey.com

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •