PDA

View Full Version : Header altered after running code



gmaxey
05-17-2012, 03:28 AM
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_pages/vba_find_and_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/customization/ReplaceAnywhere.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:

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

Then if invoked, I toggle print preview:

lbl_Exit:
If bGhost = True Then
CleanupGhostHeader oDocToProcess
End If


Sub CleanupGhostHeader(ByRef oDoc As Word.Document)
oDoc.PrintPreview
oDoc.ClosePrintPreview
End Sub


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.

Frosty
05-17-2012, 09:34 AM
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.

Frosty
05-17-2012, 09:44 AM
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.

fumei
05-17-2012, 09:54 AM
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.

Frosty
05-17-2012, 10:01 AM
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...

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

Frosty
05-17-2012, 11:50 AM
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).


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

fumei
05-17-2012, 01:19 PM
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.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 SubThe above returns a "Does not exist." - .Exists = False. FirstPage is not visible, all you see is Primary.


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
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.

Frosty
05-17-2012, 02:10 PM
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:


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

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:

'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
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).

Frosty
05-17-2012, 03:00 PM
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.

fumei
05-17-2012, 04:02 PM
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?

Frosty
05-17-2012, 04:21 PM
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

fumei
05-17-2012, 05:33 PM
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 functionAgreed.

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.

gmaxey
05-18-2012, 04:15 AM
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.

Frosty
05-18-2012, 06:46 AM
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)

Frosty
05-18-2012, 09:40 AM
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 :)

gmaxey
05-18-2012, 02:15 PM
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:

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


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.

Frosty
05-18-2012, 02:31 PM
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).

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
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.

Frosty
05-18-2012, 02:41 PM
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).

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

Frosty
05-18-2012, 02:55 PM
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...

'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

gmaxey
05-18-2012, 03:13 PM
Jason,

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

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

gmaxey
05-18-2012, 03:15 PM
Prepare yourself for eye strain :(


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).

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

Frosty
05-18-2012, 03:58 PM
So I glanced over it... I don't know that my structure is any better, technically, than yours. But, you could replace the ProcessStoryTypes, ProcessShape functions with my structure, maybe adjust my AddShapesRangesIn sub to be a bit more robust the way your ProcessShape works (to add ranges of canvasitem shapes, etc), and then you wouldn't have to have the SrchAndRplInStry function buried at multiple places... you'd just have a For Each loop of the StoryRanges_Real be an accurate collection of ranges to search through.

It doesn't really get around some of the other issues (although I notice my structure doesn't seem, at least in 2010, to leave the blank paragraph in there post-processing), but it's a somewhat more clear structure.

But it might just be re-arranging the deck-chairs... *shrug*

gmaxey
05-18-2012, 04:46 PM
Oops, It didn't find text in shapes located in canvass items, but it does now:

Option Explicit
'Proof of concept
Public Sub RealReplace()
Dim rngSearch As Range
For Each rngSearch In colStoryRanges_Real
With rngSearch.Find
.Text = "Test"
.Replacement.Text = "Test sat"
.Execute Replace:=wdReplaceAll
End With
Next
End Sub
'The MS StoryRanges collection is fundamentally broken, this is an attempt to generate an accurate
'collection range of stories for the entire document
Public Function colStoryRanges_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 colStoryRanges_Real = colRet
End Function
'Adds any shape ranges to the collection passed in
Public Sub AddShapeRangesIn(rngWhere As Range, Optional colRet As Collection)
Dim oShpIn As Shape
Dim oShape As Shape
On Error Resume Next
For Each oShape In rngWhere.ShapeRange
If oShape.TextFrame.HasText Then
colRet.Add oShape.TextFrame.TextRange
ElseIf oShape.Type = msoCanvas Then
For Each oShpIn In oShape.CanvasItems
If oShpIn.TextFrame.HasText Then
colRet.Add oShpIn.TextFrame.TextRange
End If
Next oShpIn
ElseIf oShape.Type = msoGroup Then
For Each oShpIn In oShape.GroupItems
If oShpIn.TextFrame.HasText Then
colRet.Add oShpIn.TextFrame.TextRange
End If
Next oShpIn
End If
Next
On Error GoTo 0
End Sub



When I get a chance, I'll see if I can drop it in the add-in and make it work.

Thanks.

gmaxey
05-18-2012, 05:30 PM
Yack!! Spoke too soon. It seems that Word 2010 can't deal with a For ... Each loop for processing the CanvassItems and GroupItems.

Public Sub AddShapeRangesIn(rngWhere As Range, Optional colRet As Collection)
Dim oShpIn As Shape
Dim oShape As Shape
Dim i As Long
On Error Resume Next
For Each oShape In rngWhere.ShapeRange
If oShape.TextFrame.HasText Then
colRet.Add oShape.TextFrame.TextRange
ElseIf oShape.Type = msoCanvas Then
For i = 1 To oShape.CanvasItems.Count
'For Each oShpIn In oShape.CanvasItems
Set oShpIn = oShape.CanvasItems.Item(i) 'Added
If oShpIn.TextFrame.HasText Then
colRet.Add oShpIn.TextFrame.TextRange
End If
'Next oShpIn
Next i
ElseIf oShape.Type = msoGroup Then
For i = 1 To oShape.GroupItems.Count
'For Each oShpIn In oShape.GroupItems
Set oShpIn = oShape.GroupItems.Item(i) 'Added
If oShpIn.TextFrame.HasText Then
colRet.Add oShpIn.TextFrame.TextRange
End If
Next i
'Next oShpIn
End If
Next
On Error GoTo 0
End Sub


I'll have to test and probably fix that in my published add-in :-(

gmaxey
05-19-2012, 06:18 AM
After reading through all of this again, I think the answers to my original questions can be summed up by the following demonstration:

Option Explicit
Sub SetUpExample()
Dim oDoc As Word.Document
Dim oRngStory As Word.Range
Dim lngJunk As Long
Set oDoc = Documents.Add
Stop
'Step through code and switch between code and document to follow the process.
Debug.Print oDoc.StoryRanges.Count
'A new document based on a clean normal template contains 1 storyrange. _
'If formatting marks are showing, the headers and footers will be empty (i.e., no visible paragraph mark.)
'This is because the header and footer ranges are not defined at this point.

oDoc.Sections.Add
Debug.Print oDoc.StoryRanges.Count
'Adding a section does no effect the storyrange collection. This step is used just for setup.
'Isolate section 2 header from section 1.
oDoc.Sections(2).Headers(wdHeaderFooterPrimary).LinkToPrevious = False
'Reference a header.
oDoc.Sections(2).Headers(wdHeaderFooterPrimary).Range.Text = "Section 2 Header text"
Debug.Print oDoc.StoryRanges.Count
'Simply referencing a header or footer defines and expands the storyrange collection to include the six header and footer storyranges.
For Each oRngStory In oDoc.StoryRanges
Debug.Print oRngStory.StoryType
Next oRngStory
'As Gerry stated, every range has at least one paragraph so defined, formally (empty) headers or footers will contain a paragraph mark.

'The following code similates a user clicking in the section one header (defined with a single paragraph and no text).
oDoc.ActiveWindow.View.SeekView = wdSeekCurrentPageHeader
oDoc.ActiveWindow.View.SeekView = wdSeekMainDocument

Debug.Print oDoc.StoryRanges.Count
'This action removes (kills) the six header and footer storyranges from the storyrange collection as the following illustrates!!
For Each oRngStory In oDoc.StoryRanges
Debug.Print oRngStory.StoryType
Next oRngStory
On Error GoTo Err_Handler
Err_ReEntry:
Set oRngStory = oDoc.StoryRanges(wdPrimaryHeaderStory)
Do Until oRngStory Is Nothing
Debug.Print oRngStory.Text
Set oRngStory = oRngStory.NextStoryRange
Loop
'Since we've reference the section 1 headers and created the range, we are left with empty paragraph marks. Remove them as demostrated above.
oDoc.ActiveWindow.View.SeekView = wdSeekCurrentPageHeader
oDoc.ActiveWindow.View.SeekView = wdSeekMainDocument
oDoc.Close wdDoNotSaveChanges
Exit Sub
Err_Handler:
'There is no wdPrimaryHeaderStory defined even though we can see the text in the section 2 header.
'As Jason has discovered, section 1 defines the 6 header/footer storyranges. To ensure they are defined all you need to do is reference them:
lngJunk = oDoc.Sections(1).Headers(1).Range.StoryType
Resume Err_ReEntry
End Sub


I think one of the flaws in my earlier understanding was that lngJunk does "NOT" fix a skipped header/footer, rather is ensures the the 6 header/footer story ranges are in fact defined.

Jason and Gerry, thanks for your interest in this post and your comments.

fumei
05-21-2012, 10:30 PM
I find headers/footers to be, and have been, one of the more fascinating part of the Word object model. I am still learning myself.

Frosty
05-22-2012, 11:09 AM
I don't know about you, Gerry... but if I already knew everything, I selfishly probably wouldn't participate in this forum. Learning new stuff (and helping others learn it) is the majority of the fun :)

Previous to this post, I didn't know how story ranges worked... and now I have a better understanding (for instance, I thought -- since I hadn't previously found the way to disapprove it-- that the header/footer paragraphs always existed, they just weren't shown).

I still find it strange that the footnote continuation and separator stories are created at the same time as the header/footer story ranges... and that those don't go away via the same process...

fumei
05-22-2012, 10:24 PM
I still find it strange that the footnote continuation and separator stories are created at the same time as the header/footer story ranges... and that those don't go away via the same process...From talking to a few of the people at MS, I suspect that they simply forgot.

Agreed, learning IS the majority of the fun. Which is why, as you know, I try hard to encourage posters to actually learn and, at minimum, attempt to apply that in some demonstratable manner. They may get it wrong, but if there is sincere effort shown at trying to learn THAT makes me happy, and I am much much more willing to help further.

I do want to point out that Greg's statement "isolate" is in fact not quite accurate.

'Isolate section 2 header from section 1.
oDoc.Sections(2).Headers(wdHeaderFooterPrimary).LinkToPrevious = False

Only Primary is "isolated". Only Primary is LinkToPrevious=False. FirstPage and OddEven of Section 2 are STILL Link(ed)ToPrevious to Section 1. You must explicitly unlink all six header/footer objects from the previous Section for a different Section to be truly "isolated".

I can not think of how many times I have had people get discombobulated by this.

gmaxey
05-23-2012, 02:58 AM
Gerry,

True. I was referring to the visible headers of interest in the demo document. I typically use and advise:

Sub ScratchMacro()
Dim oSec As Section
Dim i As Long
For Each oSec In ActiveDocument.Sections
For i = 1 To 3
oSec.Headers(i).LinkToPrevious = False
oSec.Footers(i).LinkToPrevious = False
Next i
Next oSec
End Sub

http://answers.microsoft.com/en-us/office/forum/office_2010-word/header-issues-with-word-2010/244c9a30-b521-4978-a5a5-d5bb03328e74

fumei
05-23-2012, 03:27 AM
Oh I am aware that YOU would deal with it; I wanted to make sure anyone else reading this would (hopefully) understand that it may have to be dealt with.

Also as a comment that while it is possible to make the empty paragraphs become invisible again, and that apparently the storyrange is gone, IF there is content in nonvisible headerfooter objects (FirstPage or OddEven) - something that is exremely useful to do in some document structures - then in fact the storyrange is NOT gone.

gmaxey
05-23-2012, 04:10 AM
Gerry,

That is true and to illustrate (plus attempt to clarify the header/footer isolation), I've modified the demo:

Sub SetUpExampleII()
'This code demonstrates peculiarities in the Word "StoryRanges" collection and how to deal with them.
Dim oDoc As Word.Document
Dim oRngStory As Word.Range
Dim i As Long
Dim lngJunk As Long
Set oDoc = Documents.Add
Stop
'Step through code and switch between code and document to follow the process.
Debug.Print oDoc.StoryRanges.Count
'Notice that a new document based on a clean normal template contains only 1 storyrange.
'Depending on the version of Word there are a total of 11 to 17 storyRanges

'Ensure formatting marks are showing.
'Notice the headers and footers will be empty (i.e., no visible paragraph mark.)
'This is because the header and footer ranges are not defined at this point.

'Add a new section.
oDoc.Sections.Add
Debug.Print oDoc.StoryRanges.Count
'Notice that adding a section does no effect the storyrange collection.

'Isolate section 2 header and footers from section 1.
For i = 1 To 3
oDoc.Sections(2).Headers(i).LinkToPrevious = False
oDoc.Sections(2).Headers(i).LinkToPrevious = False
Next i
'Reference a header (do something with it, anything).
oDoc.Sections(2).Headers(wdHeaderFooterPrimary).Range.Text = "Section 2 Header text"
Debug.Print oDoc.StoryRanges.Count
'Notice that simply referencing a header or footer (in any section) defines and expands the storyrange collection
'to include the six header and footer storyranges.
For Each oRngStory In oDoc.StoryRanges
Debug.Print oRngStory.StoryType
Next oRngStory

'Every range has at least one paragraph defined.
'Accordingly, the formally (empty) section 1 primary header and footer will now contain a paragraph mark.

'Add some text to the section 1 first page header. Note - Even if the page setup does not include a first page or even
'page header/footer, they still exist.
oDoc.Sections(1).Headers(wdHeaderFooterFirstPage).Range.Text = "You can't see me"
Debug.Print oDoc.Sections(1).Headers(wdHeaderFooterFirstPage).Range.Text
'Create a first page layout
oDoc.PageSetup.DifferentFirstPageHeaderFooter = True
oDoc.Sections(1).Headers(wdHeaderFooterFirstPage).Range.Text = "You can see me now."

'The following code similates a user clicking in the section one header (defined with a single paragraph and no text).
oDoc.ActiveWindow.View.SeekView = wdSeekCurrentPageHeader
oDoc.ActiveWindow.View.SeekView = wdSeekMainDocument

Debug.Print oDoc.StoryRanges.Count
'Notice how this action, which could easily be done by the user in with the UI, removes (and kills) the
'section 1 header and footer ranges. The paragraph marks are gone!

'More troublesome is that the six header and footer storyranges from the storyrange collection
'have also been killed as the following illustrates!!

For Each oRngStory In oDoc.StoryRanges
Debug.Print oRngStory.StoryType
Next oRngStory

'We know that there is a header range defined in section 2 because we can still see the text. This means that a header range does
'not necessarily mean a header storyrange.

'The following illustrates that in order to process all ranges, you must first ensure that all the storyranges are defined.
For Each oRngStory In oDoc.StoryRanges '(wdPrimaryHeaderStory)
Do Until oRngStory Is Nothing
Debug.Print oRngStory.Text
Set oRngStory = oRngStory.NextStoryRange
Loop
Next oRngStory

'So why was the content in the section 2 primary header not returned?
On Error GoTo Err_Handler
Set oRngStory = oDoc.StoryRanges(wdPrimaryHeaderStory)
Err_ReEntry:
For Each oRngStory In oDoc.StoryRanges '(wdPrimaryHeaderStory)
Do Until oRngStory Is Nothing
Debug.Print oRngStory.Text
Set oRngStory = oRngStory.NextStoryRange
Loop
Next oRngStory
'Since we've reference the section 1 headers and created the range, we are left with empty paragraph marks. Remove them as demostrated above.
oDoc.ActiveWindow.View.SeekView = wdSeekCurrentPageHeader
oDoc.ActiveWindow.View.SeekView = wdSeekMainDocument
oDoc.Close wdDoNotSaveChanges
Exit Sub
Err_Handler:
'There is no wdPrimaryHeaderStory so the error occurred.
'It is content in Section 1 that defines the 6 header/footer storyranges.
'To ensure they are defined (all of them) all you need to do is reference one of them:
lngJunk = oDoc.Sections(1).Headers(1).Range.StoryType
Resume Err_ReEntry
'What I've learned is that lngJunk and the reference to the section 1 header does "NOT" ensure that skipped headers are processed.
'It is actually section 1 that defines the storyrange collection.
End Sub

fumei
05-23-2012, 11:19 PM
'It is actually section 1 that defines the storyrange collection.

Indeed, and there is always a Section 1, although its content may not always be what you intended to start with, vis-a- vis the deletion of Section later, working backwards.

Gotta love this stuff.

'Isolate section 2 header and footers from section 1.
For i = 1 To 3
oDoc.Sections(2).Headers(i).LinkToPrevious = False
oDoc.Sections(2).Headers(i).LinkToPrevious = False
Next i


Does not in fact isolate headers and footers. It only isolates headers. Unless that is a typo and it should be...

'Isolate section 2 header and footers from section 1.
For i = 1 To 3
oDoc.Sections(2).Headers(i).LinkToPrevious = False
oDoc.Sections(2).Footers(i).LinkToPrevious = False
Next i

BTW that is one of the standard operations that I have as a procedure to Call.

gmaxey
05-24-2012, 02:40 AM
It is a typo :-(

fumei
05-24-2012, 09:25 PM
I figured. Just a little razzin'. I knew you would not be doubling lines of instructions, you are way too good for that.

DaveM
03-22-2022, 08:43 AM
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)?


Hmmm. I would call it the Heisenberg variable........ the mere act of observation changes the results. :yes

I came to this thread when trying to understand anomolies when clearing out a textframe in footers of converted PDFs of weekly public notices. I wanted to remove the shaperange containing the textframe and then add a standard footer of "Public Notice [tab] Issue Date [tab] Page #". No problem deleting the shaperange but the insertion (rng.TEXT) of the standard text caused it to disappear off screen. Blindly trying to delete any hidden paragraph marks before inserting my standard footer text, I found, that doing a .select on the range caused the display to flip, annoyingly, to Outline View.

Finally fell into a kludge of using MOVEUNTIL (twice) to select and delete the hidden paragraph marks, so insertion of my standard text now appeared on screen. A lot of work to work around the hidden nature of Word. And once again reinforces the power of the paragraph mark.

Thanks, to all you MVPs in VBA for helping to make the mysterious knowable. Be damned, Heisenberg!