PDA

View Full Version : [SOLVED:] LinkToPrevious



glencoe
01-27-2014, 06:32 AM
Hello,

I read in the help that HeaderFooter.LinkToPrevious is supposed to be a read/write boolean. In other words, I should be able to use this to check whether a header is linked or not to the previous one, right?
I work in a Word document and move from section to section. In every section, I need to check the state of the header (when it exists). That's where I use the lines

Dim i, SectionCount as integer

SectionCount = ActiveDocument.Sections.Count
For i = 1 to SectionCount
Selection.GoTo What:=wdGoToSection, Which:=wdGoToAbsolute, Count:=i
ActiveWindow.ActivePane.View.SeekView = wdSeekCurrentPageHeader
If Selection.HeaderFooter.LinkToPrevious = False Then
'Do something
end if
next i

Now, the "if" line seems to give more or less random results. So I am wondering if this property should be used only with the Headers property, but I fear it would be more difficult to do the same.
Any thought?

fumei
01-27-2014, 03:01 PM
1. "the state of the header (when it exists)" Headers ALWAYS exist. It is not possible for Headers to not exist.
2. EVERY section ALWAYS has three headers and three footers. Always.
3. using Selection to work with Sections headers is a poor idea. you can action all the headers in any section directly.
4. your IF statement will only ever action the first header Selection come to. If that is the FirstPage, that is what actions; if that Section does not have FirstPage active, then Primary header gets actioned. This may explain your "random" results.

perhaps explain what it is - exactly - you are trying to do.

glencoe
01-27-2014, 03:15 PM
You are perfectly correct indeed...
Is there a way to know what type of header the cursor is in? That would actually help me a lot. I read indeed about the different types of headers. Basically, I am searching for specific strings of characters in the headers and footers, but I first need to make sure the header (or footer) the cursor is in is not linked to the previous one.
Therefore, I need to:
- find out what kind of header (among the 3 possible choices) I am in
- find out the state (linked or not) of the header.

I guess that I would have to use the form .Headers() to work this out?

fumei
01-27-2014, 04:55 PM
Again, please state - exactly - what it is you want to happen.

Again, using Selection is not usually a good idea. Selection means what header you are "in". If you do not use Selection, you are never "in" any of them, but you can still perform action on ANY of them directly.

With code, I am never "in" any header; I never use View. So I never need to know what type of header the cursor is in...the cursor is never "in" one.

glencoe
01-27-2014, 05:48 PM
To summarize what I am trying to achieve, I need to focus on all headers/footers of the document to search for specific strings and do some actions depending on the strings I find. I only want to skip those headers and footers that are linked, to save processing time as I know they have already been processed before.
I thought that going through the different sections would be a good start, so I simply go to the header of each section to process whatever I need to do. If the header is linked to the previous one, I want to skip it. That's it.

I am perfectly that my code may not be the best, and that most of you would be able to improve it a lot. I am not a professional programmer! ;-)

Now, when the selection (i.e. the strings I am playing with) is in a header, I need to know what state the header is. I obviously got confused by the vba help, and I understand my method is completely wrong.
Please advise. What would be the best and most efficient way to do what I need?

fumei
01-27-2014, 08:56 PM
How can I put this? Please state - exactly - what it is you want to happen. Exactly means...exactly. For example, you state you want to find specific strings. OK. Where are you getting those strings? Are you selecting some text elsewhere in the document and then using THAT to search for? Is the string from a different document? Is there going to be more than one string to search for?


Sub ReplaceStuff()
Dim strCheck As String
Dim oHF As HeaderFooter
Dim oSection As Section
strCheck = "old stuff"
For Each oSection In ActiveDocument.Sections
For Each oHF In oSection.Headers
If InStr(1, oHF.Range.Text, "old stuff") Then
oHF.Range.Text = _
Replace(oHF.Range.Text, "old stuff", "new and improved stuff")
End If
Next
Next
End Sub The above goes through ALL headers and if the string "old stuff" is there, it is replaced with "new and improved stuff". No Selection. No View. Note you can also use Find\Replace. But again, what - exactly - is supposed to happen. BTW, I am not a professional programmer either. However, a basis for any code is to define - exactly - what it is that is supposed to happen.

Note that the code does not test for Link to Previous, because for what it does, that is not needed. However, maybe you DO need it, it is impossible to tell without knowing what exactly is supposed to happen.

glencoe
01-28-2014, 08:13 AM
fumei,
I appreciate so much your help! Thanks also for pushing me... I don't always get the whole point, and though I know where I am heading, I don't always understand what the best path is...

I'll try to be more specific. The strings I search for are given by the user. I don't actually know what they are, since they can be different every time. I can only say that they are actually tags that surround text. I have to find 2 sets of strings, a bit like brackets (opening tag, closing tag) and if everything seems fine (those tags are found in the correct order, i.e. if 2 opening tags are found consecutively with no closing tag in-between, they are ignored), the text is processed through another function (let's say it is highlighted and tags are deleted).

As a side note, those tags have a minimum of 2 characters each (e.g. #< and #>), but they can also be the same ones (e.g. ### and ###), in which case I only make sure I have an even number of tags within the header, to avoid processing the wrong text.
The tags are sent to the function through a string, a bit like you did with strCheck (though I found it funny that you actually don't use it in your example, but I got your point).

Now, I do love your approach, but I have a few questions/concerns with it:
- inStr is a great function indeed that I could make use of in other cases (and I certainly will), but is there a way to keep track of the number of replacements your algorithm is doing, as I need to keep track of it?
- using the whole header range is very interesting, but replacing it actually overwrites the whole header, including any text box. That doesn't work in my case as I need to preserve everything but the strings I am looking for.
- though your "if" line can go fast indeed, I do have a concern with processing speed, since the documents that are used with the macro can have any number of pages/sections (from 1 to 1000's of pages), in which case I want to avoid searching those sections that are linked, since the strings have already been searched for in the parent sections. Consider the fact that if a 500 pages document has 100 different sections (i.e. say 100 different headers and 400 "linked headers"), going through each header would be time-consuming, while skipping those linked headers would help processing the algorithm faster. I assume that using the line "for each oHF in oSection.Headers" goes indeed into each header, whatever it is, and whatever its state is. So it is really great, but not perfect in my case, since it may fall in a time-consuming approach, which I want to avoid...

I hope my explanation is now clear enough. But even if that doesn't solve my issue yet, you did give me a few interesting ideas that I may use elsewhere in my macro! :-)

glencoe
01-29-2014, 01:11 PM
As a side note, I found on the web (http://word.mvps.org/faqs/MacrosVBA/GetNoOfReplacements.htm) a method that can be used to keep track of the number of replacements done through the function Replace(), so this part is not an issue anymore.
Now, I still have the same concern regarding processing speed in big documents, as all headers are checked without consideration to their state. If anyone can help on this part, it would be great...
Using the algorithm fumei wrote, I assume there must be a way to add a few lines to check if ".LinkToPrevious = False" in the different headers? I'm not quite sure how to write this, as there are 3 possible headers in a Word file...

glencoe
01-29-2014, 08:24 PM
Can anyone confirm that this procedure would be skipping any linked header, and process only "parent" headers?

Dim oHF As HeaderFooter
Dim oSection As Section
Dim LinkToPreviousFlag As Integer
For Each oSection In ActiveDocument.Sections
For Each oHF In oSection.Headers
LinkToPreviousFlag = 0
If oSection.Headers(wdHeaderFooterPrimary).LinkToPrevious = True Then
LinkToPreviousFlag = 1
ElseIf oSection.Headers(wdHeaderFooterFirstPage).LinkToPrevious = True Then
LinkToPreviousFlag = 1
ElseIf oSection.Headers(wdHeaderFooterEvenPages).LinkToPrevious = True Then
LinkToPreviousFlag = 1
End If

If LinkToPreviousFlag = 0 Then 'process only the section if it is not linked to the previous one
oHF.Range.Select
'process whatever is required
End If
Next oHF
Next oSection

Please advise!
Also, I am wondering if there is a way to match the "section number" oSection with the corresponding section in Word? I noticed that this procedure gives more sections than the actual Word doc contains. I assume this is because each header/footer is considered as a unique section, which is not the same as a Word section.

fumei
01-29-2014, 11:58 PM
Yikes you need to understand headers and sections! But first:


I still have the same concern regarding processing speed in big documents, as all headers are checked without consideration to their state. Perhaps, but checking their state also takes processing. I am not sure how much a InStr test takes more than a If LinkToPrevious = False test.


- using the whole header range is very interesting, but replacing it actually overwrites the whole header, including any text box. First of you never mentioned any text box. Second my good (sans any textbox) does not replace the whole header, only the "old stuff" string. Everything else remains.

I remain a bit off put by what SEEMS to be happening. Let me see if I have this.

You have users that will VERY OFTEN need to change existing headers. I say very often because if it is NOT often, then are you concerned about performance times. if it is a uncommon event, hey start it and go to lunch. Big deal.

The user will use RANDOM strings of text.

I don't actually know what they are, since they can be different every time. So...there is no logic behind what the string will be. Right?

If any given section header is linked to the previous (same) header type in the previous section it is assumed that that header is correct and NO test of the current header content needs to be done. Really? Not sure why I am sceptical about that, but what do I know? BTW "for in the parent sections" - there is no such thing as "parent" sections

Somehow the user can get a search string into the procedure (although how is not stated...inputbox, the user selects some text in the document and THAT is used???), and in some manner "tags" are added to that string.

Consider the fact that if a 500 pages document has 100 different sections (i.e. say 100 different headers and 400 "linked headers"), Umm, that is not a fact. First, the number of pages is completely irrelevant. You seem to think there is a header for each page. Not so. The number of pages has nothing to do with sections. Nothing. You can have a 1000 page document with ONE section, in which case the code will run pretty darn fast. Secondly a document with 100 sections will have 300 headers whether they are linked, or not. LinkToPrevious has NOTHING to do with the number of headers. Or the number of pages.

Anyway, you seem to be moved along fairly well. I am not sure I can help much more.

fumei
01-30-2014, 12:03 AM
Can anyone confirm that this procedure would be skipping any linked header, and process only "parent" headers?

Dim oHF As HeaderFooter
Dim oSection As Section
Dim LinkToPreviousFlag As Integer
For Each oSection In ActiveDocument.Sections
For Each oHF In oSection.Headers
LinkToPreviousFlag = 0
If oSection.Headers(wdHeaderFooterPrimary).LinkToPrevious = True Then
LinkToPreviousFlag = 1
ElseIf oSection.Headers(wdHeaderFooterFirstPage).LinkToPrevious = True Then
LinkToPreviousFlag = 1
ElseIf oSection.Headers(wdHeaderFooterEvenPages).LinkToPrevious = True Then
LinkToPreviousFlag = 1
End If

If LinkToPreviousFlag = 0 Then 'process only the section if it is not linked to the previous one
oHF.Range.Select
'process whatever is required
End If
Next oHF
Next oSection

Please advise!
Also, I am wondering if there is a way to match the "section number" oSection with the corresponding section in Word? I noticed that this procedure gives more sections than the actual Word doc contains. I assume this is because each header/footer is considered as a unique section, which is not the same as a Word section.

Yikes. OK. each header/footer is NOT a unique section. Each section has three headers and three footers. Always. Two of them can used, or not. One is ALWAYS used...although its displayed name changes.

I noticed that this procedure gives more sections than the actual Word doc Did you now? What makes you say that?

fumei
01-30-2014, 12:05 AM
BTW, please use the code tags when posting code.

Also, why are you selecting ANYTHING???????

oHF.Range.Select


You do not need to select anything, as stated previously, you can action them directly.

glencoe
01-30-2014, 04:37 AM
checking their state also takes processing. I am not sure how much a InStr test takes more than a If LinkToPrevious = False test.

That's a good point indeed... One or the other does take time, it is pretty hard to calculate this. I would need to have a huge document (with a big number of different headers) in order to see if there is any difference!


First of you never mentioned any text box. Second my good (sans any textbox) does not replace the whole header, only the "old stuff" string. Everything else remains.

You're right, I never mentioned any text box. It just popped up in my mind when I did a test in a document that happened to have text boxes in the header. And as a result, I have to say (why, I am not sure) that your nice procedure did delete (or, should I say, seems to delete, as I couldn't see them anymore) those text boxes. I assume (don't get me wrong, as you understand, I am quite a beginner here) that this is due to the line "oHF.Range.Select", which selects the whole header, right?


You have users that will VERY OFTEN need to change existing headers. I say very often because if it is NOT often, then are you concerned about performance times. if it is a uncommon event, hey start it and go to lunch. Big deal.
The user will use RANDOM strings of text.
So...there is no logic behind what the string will be. Right?

One more information about what the documents are: they are translated documents and tags are added by the user in the translation software as a "comment marker". In other words, the string that is between the "tags" is something the user wants to double check later on in the final Word file. Now, why tags are chosen by the user is because there always is a risk to use an internal tag used by the translation software, that will compromise, or be confused as a "marker tag". Since the files come from different translation software (just to add to the complexity), I can't use a "fixed" tag, as it may be already used by the software (how could I guess?). I hope you get my point. Now, I assume that most of the time, tags will look similar to ## ##, #< #>, ?%$ $%? or strings like that (any combination of symbols, starting with 2 characters), since those are examples given to the users. As a side note, the editor of vbaexpress does use tags like those "QUOTES", that could be considered as "comment tags" in my situation, if you get my point.



If any given section header is linked to the previous (same) header type in the previous section it is assumed that that header is correct and NO test of the current header content needs to be done. Really? Not sure why I am sceptical about that, but what do I know? BTW "for in the parent sections" - there is no such thing as "parent" sections


My understanding was that, in a Word file, you would have headers that are either linked to the previous one, or not linked (so new, different, headers). If the algorithm checks every single header and meets a new header, it will process it, then any consecutive linked header will be processed at the same time, right? So there is no need to process them further, since what needed to be done (tags checked/deleted) is already done...


Somehow the user can get a search string into the procedure (although how is not stated...inputbox, the user selects some text in the document and THAT is used???), and in some manner "tags" are added to that string.

I guess that you now understand how tags are added to the file. Then, there is an interface that the user runs to select what function needs to be processed in the file, among which the procedure we are currently talking about (i.e. check and delete any "comment tag" and process the text in-between). So yes, there is an input box in which the user types tags that were used in the translation software to add comments.


Umm, that is not a fact. First, the number of pages is completely irrelevant. You seem to think there is a header for each page. Not so. The number of pages has nothing to do with sections. Nothing. You can have a 1000 page document with ONE section, in which case the code will run pretty darn fast. Secondly a document with 100 sections will have 300 headers whether they are linked, or not. LinkToPrevious has NOTHING to do with the number of headers. Or the number of pages.

I understand that, I was only trying to give you an example of what may happen with a big document. I do see from time to time big documents myself (say 300 pages, with different sections), so I know I have to take this into account. As for stating that there is a header for each page, I was also referring to your previous statement that
Headers ALWAYS exist. It is not possible for Headers to not exist. EVERY section ALWAYS has three headers and three footers. Always.. Whether a header is not shown doesn't mean that it doesn't exist, so I was wondering if the code you suggested would be going through each of them, whether it is displayed or not in the file.

glencoe
01-30-2014, 04:55 AM
Yikes. OK. each header/footer is NOT a unique section. Each section has three headers and three footers. Always. Two of them can used, or not. One is ALWAYS used...although its displayed name changes.
That was more clear to me indeed, though I may not have expressed myself properly.

I created a small 4 pages Word file as a test, including 3 different sections (with 3 different headers). Running your procedure (step by step) went in the loop 9 times, so I found it confusing, but thinking about it, I assume this is because each section has 3 header.

glencoe
01-30-2014, 04:58 AM
BTW, please use the code tags when posting code.
I just found the tags, sorry! ;-)


Also, why are you selecting ANYTHING???????

oHF.Range.Select
Because I took this line in your code. I thought I was selecting the whole header with it...


You do not need to select anything, as stated previously, you can action them directly.
I'd love to, but as I said, your code seems to delete text boxes, so there must be something I don't get...

fumei
01-30-2014, 04:42 PM
Yes, as stated, each section has three headers: Primary, FirstPage and OddEven. If the SETTING for first page and OddEven are unchecked (in Page Setting), then Primary is displayed for all pages in the section. Obviously if First Page is checked, then FirstPage is displayed; if Different Odd Even is checked OddEven is displayed for Even pages, and Primary is displayed for odd pages.


Because I took this line in your code. I thought I was selecting the whole header with it... It does select the whole header, but I never used that in my code

So there could be these comment tags in the header? As well as textboxes. This adds a fairly large amount of complexity. I am still confused about things. Are these `tags`to remain...or not.

glencoe
01-30-2014, 06:56 PM
If the SETTING for first page and OddEven are unchecked (in Page Setting), then Primary is displayed for all pages in the section. Obviously if First Page is checked, then FirstPage is displayed; if Different Odd Even is checked OddEven is displayed for Even pages, and Primary is displayed for odd pages.

So, if I check (through the code) the page settings of the document, I should be able to find out whether First Page or OddEven is checked, and therefore use it (or not) in my code?


It does select the whole header, but I never used that in my code
Humpf... you're right, I copied the first part (oFF.Range) and selected the header instead.


So there could be these comment tags in the header? As well as textboxes. This adds a fairly large amount of complexity. I am still confused about things. Are these `tags`to remain...or not.
Basically, anything that may exist in a Word file may happen to be in the documents processed by the algorithm, so yes, text boxes should be checked in headers, BUT I am not worrying about those right now, since I already wrote a while ago a procedure that goes through each text box (in the headers/footers and in the main body), so I will implement this later on, once the main part related to the headers is fixed.
As for tags, my first point is to:
1)a) Make sure tags are found in an even number (if both opening/closing tags are the same),
OR
1b) Make sure tags are found in the correct order (i.e. opening tag, always followed by a closing tag BEFORE another opening tag is found, if both tags are different)
2) Delete those tags and highlight text in-between (I know how to do it)

So, I'd love to stick to your great procedure, but my statement in 1b) makes me think I cannot process the algorithm "blindly", without checking the order first, in which case I need to consider some extra lines for that. I already got something working, but my code is being done with selections (which you want to avoid, as I understand) rather than actioning directly all headers.
Also, because text boxes may exist in headers, I guess that I cannot use the "oHF.Range.Text" function either...

Sorry for being so picky and making things complicated, but I must say that you're really kind, trying to help me with my issue!

fumei
01-30-2014, 07:26 PM
I have to take a break from this, now that I know that opening and closing tags are involved. This kind of stuff has come up many time (mostly by people who try to pretend that Word is some kind of HTML editor). It has always been a royal pain, ridiculously complicated and tricky to get right. It certainly moves it well away from the original impression of a normal string. Working with tags is not normal. They give me a headache.

I have to say that having such tags in the header at all is an abomination. Yuck. Bleeech. BTW, checking the order is not much help when you are also tossing textboxes into the mix. Bleeech.

glencoe
01-30-2014, 07:41 PM
LOLLLLLLL!!!
Well, I certainly don't want to give you more headaches (at least you understand what I am going through). Trying hard to get things as simple as possible, but those text boxes + tags being potentially mixed up are definitely a pain...
I have to use those tags as some translation software don't enable the user to comment anything so far, so those tags are a trick users found to get comments through.
BTW, if you want to work with "normal strings", it would work exactly the same way, as those tags do appear as strings in Word, and not like HTML tags!

Anyway, I'll try to work from the few good tips you gave me, and we will see how things go... Thanks so much for your help!

fumei
01-31-2014, 08:45 PM
Why on earth are users making any comments on what is in a header!!!! That is absurd. It is not their document, tell them to mind their own business. Just kidding.

Yes, I know the string with the tags are normal strings, it is annoying thing of having test random length starting and ending portions.

glencoe
02-01-2014, 07:05 AM
Why on earth are users making any comments on what is in a header!!!! That is absurd. It is not their document, tell them to mind their own business. Just kidding.

Yes, I know the string with the tags are normal strings, it is annoying thing of having test random length starting and ending portions.

Well, the good news is: whatever those tags are, and it doesn't matter why they are in headers, since they are only temporary! The algorithm is designed to find them and delete them... :-)
Now, don't bother trying to figure out how to find each opening/closing tag, I've got that and it works. The only part that I am still struggling with is how to detect linked headers. I might give up if I see it's not worth it, but still, I'd like to know how to do it properly.
My current code is failing:


Dim oHF As HeaderFooter
Dim oSection As Section
Dim LinkToPreviousFlag As Integer
For Each oSection In Application.ActiveDocument.Sections
For Each oHF In oSection.Headers
LinkToPreviousFlag = 0
oHF.Range.Select
If oSection.PageSetup.OddAndEvenPagesHeaderFooter = False And oSection.PageSetup.DifferentFirstPageHeaderFooter = False And oSection.Headers(wdHeaderFooterPrimary).LinkToPrevious = True Then 'if FirstPage and OddEven are unchecked and header is linked
LinkToPreviousFlag = 1
ElseIf oSection.PageSetup.DifferentFirstPageHeaderFooter = True And oSection.Headers(wdHeaderFooterFirstPage).LinkToPrevious = True Then 'if FirstPage is checked and header is linked
LinkToPreviousFlag = 1
ElseIf oSection.PageSetup.OddAndEvenPagesHeaderFooter = True And oSection.Headers(wdHeaderFooterEvenPages).LinkToPrevious = True Then 'if OddEven is checked and header is linked
LinkToPreviousFlag = 1
End If


those "if" don't give the right result... Any suggestion?

Otherwise, I found another discussion on a similar issue (http://www.vbaexpress.com/forum/showthread.php?16561-LinkToPrevious-Property&highlight=linktoprevious), and the suggestion is


For Each S In ActiveDocument.Sections
If S.Index > 1 Then
For Each HF In S.Headers
If HF.Exists Then
MsgBox Choose(HF.Index, "Primary", "First Page", "Even Page") & " Header" & _
" in Section " & S.Index & _
" is " & IIf(HF.LinkToPrevious, "", "not ") & "linked to previous"
End If
Next
For Each HF In S.Footers
If HF.Exists Then
MsgBox Choose(HF.Index, "Primary", "First Page", "Even Page") & " Footer" & _
" in Section " & S.Index & _
" is " & IIf(HF.LinkToPrevious, "", "not ") & "linked to previous"
End If
Next
End If
Next S

I will have to check this as well. Not sure how the msgbox works here though...

fumei
02-01-2014, 08:12 PM
Do NOT - repeat NOT - use Exists in this case. Exists only is whether FirstPage or OddEven is being displayed. It has nothing whatsoever to do with whether it exists, or not.

The only part that I am still struggling with is how to detect linked headers. I might give up if I see it's not worth it, but still, I'd like to know how to do it properly. You were correct in the first place, Link is Boolean, therefore use it as Boolean. And stop using Select!

Dim oHF As HeaderFooter
Dim oSection As Section
Dim LinkToPreviousFlag As Integer
For Each oSection In Application.ActiveDocument.Sections
For Each oHF In oSection.Headers
If oHF.LinkToPrevious = False Then
'it is NOT linked so
' do whatever it is you are doing
End If

macropod
02-01-2014, 11:31 PM
There are basically two ways of processing all headers/footers in a document. The following two subs demonstrate them.

The first sub loops through all sections, then through all headers/footers in the section, testing whether they're linked to a previous one.

The second sub achieves the same result more simply and more efficiently, without the need to check the LinkToPrevious state, by processing the corresponding story-ranges.

There are, of course, advantages to the first approach in some situations.

Sub Demo1()
Application.ScreenUpdating = False
Dim Fld As Field, Sctn As Section, HdFt As HeaderFooter
With ActiveDocument
For Each Sctn In .Sections
For Each HdFt In Sctn.Headers
With HdFt
If .LinkToPrevious = False Then
With .Range.Find
.ClearFormatting
.Replacement.ClearFormatting
.Format = False
.Forward = True
.Wrap = wdFindStop
.Text = ""
.Replacement.Text = ""
.MatchCase = True
.MatchAllWordForms = True
.MatchWholeWord = False
.MatchWildcards = False
.Execute Replace:=wdReplaceAll
End With
End If
End With
Next
For Each HdFt In Sctn.Footers
With HdFt
If .LinkToPrevious = False Then
With .Range.Find
.ClearFormatting
.Replacement.ClearFormatting
.Format = False
.Forward = True
.Wrap = wdFindStop
.Text = ""
.Replacement.Text = ""
.MatchCase = True
.MatchAllWordForms = True
.MatchWholeWord = False
.MatchWildcards = False
.Execute Replace:=wdReplaceAll
End With
End If
End With
Next
Next
End With
End Sub
Sub Demo2()
Dim Stry As Range
With ActiveDocument
For Each Stry In .StoryRanges
With Stry
Select Case .StoryType
Case wdEvenPagesHeaderStory, wdFirstPageHeaderStory, wdPrimaryHeaderStory
With .Find
.ClearFormatting
.Replacement.ClearFormatting
.Format = False
.Forward = True
.Wrap = wdFindStop
.Text = ""
.Replacement.Text = ""
.MatchCase = True
.MatchAllWordForms = True
.MatchWholeWord = False
.MatchWildcards = False
.Execute Replace:=wdReplaceAll
End With
Case wdEvenPagesFooterStory, wdFirstPageFooterStory, wdPrimaryFooterStory
With .Find
.ClearFormatting
.Replacement.ClearFormatting
.Format = False
.Forward = True
.Wrap = wdFindStop
.Text = ""
.Replacement.Text = ""
.MatchCase = True
.MatchAllWordForms = True
.MatchWholeWord = False
.MatchWildcards = False
.Execute Replace:=wdReplaceAll
End With
End Select
End With
Next
End With
End Sub

I too would prefer not to entangle myself in yet another attempt to (mis)use Word as an HTML editor.

fumei
02-02-2014, 01:52 AM
And there ya go. I was hoping macropod would put in a post. Glencoe I think you have enough information to action the headers you want, as required. WHAT that action is, is up to you. Messing with tags is a pain in the butt. Good luck.

glencoe
02-02-2014, 12:45 PM
Thank you very much to both of you, fumei and macropod for your precious help!
I just managed to get something working without going too far from your original algorithms, but I will study closely all your advise and loops to see what would be the best.
I obviously do not want to get my code more complex than it should, and the whole point is not to go against common sense, but more to process the data I am given the best possible way.

Again, big applause to both of you. I will mark the case as solved shortly, once I have gone through all your points again, and I will probably leave my "final" code in here as well if it may be useful to anyone else.

fumei
02-02-2014, 04:50 PM
Yes, please post whatever code you do come up with.

Frosty
02-03-2014, 03:30 PM
Paul, I'm not sure you should rely on the .StoryRanges collection in Word when dealing with headers/footers-- to me it seems completely broken when using it in a For each loop. Turn off Link to Previous in the primary header of section 2, drop some text in there.. and then try your code. You won't find it, becaue the for each loop completely skips it. I *think* you can use the .NextStoryRange method to get around this bug, but I use some sort of structure like the following function to iterate through all of theI think the .StoryRanges collection is broken in Word (at least, in Word 2010). I believe it creates the .StoryRanges collection based on the setup of the first section, and so can ignore other sections when they aren't linked... so if you have the following set up:1. Section 1, no first page different, no even pages2. Section 2, first page different, primary header not linked to previousThe for each loop for .StoryRanges fails to access the Primary Header in section 2.If you set up a multi-section document put some text in the headers/footers that you are showing... it should pretty easily demonstrate the problem.For the OP -- I would use my fGetStoryRanges function with the default options (the way the second test routine does), and then have a separate function process each unique range. In terms of optimizing the code -- it's definitely better to check a boolean property of whether a header/footer range is linked than to use a string test on the range. That said, if you end up with something working- then go for it. But I would *not* trust the For Each loop on StoryRanges, especially on large documents where you're unlikely to notice a failure (as opposed to the simple documents you'd probably test on).I agree with Fumei -- post whatever you come up with, the below
Sub TestBadStoryRanges() Dim r As Range For Each r In ActiveDocument.StoryRanges Select Case r.StoryType Case wdEvenPagesFooterStory, wdEvenPagesHeaderStory, wdFirstPageFooterStory, wdFirstPageHeaderStory, _ wdPrimaryFooterStory, wdPrimaryHeaderStory ' r.Select Debug.Print Replace(r.Paragraphs.First.Range.Text, vbCr, "") End Select NextEnd SubSub TestGoodStoryRanges() Dim r As Range For Each r In fGetStoryRanges Debug.Print Replace(r.Paragraphs.First.Range.Text, vbCr, "") NextEnd Sub'----------------------------------------------------------------------------------------------'The MS StoryRanges collection is fundamentally broken when trying to search headers/footers'as well, since a for each loop doesn't return unlinked headers'This is an attempt to generate an accurate collection of story ranges for the passed document.'Uses the activedocument (if any) if no document is passed'doesn't currently return ranges of any textboxes'----------------------------------------------------------------------------------------------Public Function fGetStoryRanges(Optional oDoc As Document, _ Optional bHeadersFootersOnly As Boolean = True, _ Optional bAddHeadersFootersOnlyIfShowing As Boolean = True) As Collection Dim colRet As Collection Dim rngStory As Range Dim hf As HeaderFooter Dim oSec As Section On Error GoTo l_err Set colRet = New Collection If oDoc Is Nothing Then Set oDoc = ActiveDocument End If 'easy story ranges, if desired If bHeadersFootersOnly = False Then For Each rngStory In oDoc.StoryRanges Select Case rngStory.StoryType Case wdCommentsStory, wdFootnotesStory, wdEndnotesStory, wdMainTextStory colRet.Add rngStory.Duplicate End Select Next End If 'headers/footers (for each not reliable!!) For Each oSec In oDoc.Sections For Each hf In oSec.Headers 'don't add linked ranges If hf.LinkToPrevious = False Or oSec.Index = 1 Then 'don't add hidden ranges, unless specified If bAddHeadersFootersOnlyIfShowing And hf.Exists _ Or bAddHeadersFootersOnlyIfShowing = False Then colRet.Add hf.Range.Duplicate End If End If Next For Each hf In oSec.Footers 'don't add linked ranges If hf.LinkToPrevious = False Or oSec.Index = 1 Then 'don't add hidden ranges, unless specified If bAddHeadersFootersOnlyIfShowing And hf.Exists _ Or bAddHeadersFootersOnlyIfShowing = False Then colRet.Add hf.Range.Duplicate End If End If Next Nextl_exit: Set fGetStoryRanges = colRet Exit Functionl_err: 'any errors, blackbox and return an empty collection Set colRet = New Collection Resume l_exitEnd Function

Frosty
02-03-2014, 03:55 PM
I don't know what's going on with the formatting here... but I can't make it hold any line breaks... sorry for the mess. If an admin can solve, great... maybe this is an IE11 "feature"

glencoe
02-03-2014, 04:17 PM
Frosty,

Thanks for your input and code. I'll check it as well. Don't worry about the "mess", I can still easily copy and paste your code.
Macropod, I actually found a glitch in your code (first option). I can't explain why, but though the loop works perfectly fine, the first section is not always processed properly. When checking step by step, I find that the line " If .LinkToPrevious = False Then" is considered as True on the first section (first page), while it actually is False! Therefore, that section is skipped, while it should be processed as well... Then, going through the loop again will process the First page just fine!
I thought it may be a "speed" issue (the code being processed too fast), so I added a DoEvents line, without success either...
Will check further, but if you can think of anything, I'd be happy to hear about the reason why it would fail!

Frosty
02-03-2014, 04:26 PM
Well, I'll keep my response short and sweet... you should change the logic test to be if the .LinkToPrevious = False OR Sctn.Index = 1

macropod
02-03-2014, 06:25 PM
There does indeed seem to be a bug in the StoryRanges objects.

However, for the Demo1 code, there should be no need to change the logic or the test - the .LinkToPrevious value for the first Section is always false. You can verify that by inserting:
MsgBox "Section: " & Sctn.Index & vbTab & "Link To Previous: " & .LinkToPrevious
after:
With HdFt
If that's not processing correctly, we have yet another bug...

Frosty
02-03-2014, 06:37 PM
In my testing, even though the .LinkToPrevious was false, it still skipped the logic block. I think that's indeed another bug.

glencoe
02-03-2014, 07:21 PM
Macropod,
Well, I am afraid that I did see .LinkToPrevious as true for Section 1! How come, I don't know. The button is obviously disabled in Word for this section, so it cannot be a user setting (and certainly not changed in my code either)...
Anyway, the solution offered by Frostly counteracts the bug, so I'm happy with it, even though your code should be working fine all the way... I guess it's some sort of glitch due to Microsoft, and the behaviour is not always consistent!

Frosty
02-06-2014, 11:17 AM
Well, at least I can clean up the code from another browser... I hate that ugly block of unformatted code.

Sub TestBadStoryRanges()
Dim r As Range
For Each r In ActiveDocument.StoryRanges
Select Case r.StoryType
Case wdEvenPagesFooterStory, wdEvenPagesHeaderStory, wdFirstPageFooterStory, wdFirstPageHeaderStory,_
wdPrimaryFooterStory, wdPrimaryHeaderStory
' r.Select
Debug.Print Replace(r.Paragraphs.First.Range.Text, vbCr, "")
End Select
Next
End Sub


Sub TestGoodStoryRanges()
Dim r As Range
For Each r In fGetStoryRanges
Debug.Print Replace(r.Paragraphs.First.Range.Text, vbCr, "")
Next
End Sub
'----------------------------------------------------------------------------------------------
'The MS StoryRanges collection is fundamentally broken when trying to search headers/footers
'as well, since a for each loop doesn't return unlinked headers
'This is an attempt to generate an accurate collection of story ranges for the passed document.
'Uses the activedocument (if any) if no document is passed'doesn't currently return ranges of any textboxes
'----------------------------------------------------------------------------------------------
Public Function fGetStoryRanges(Optional oDoc As Document, _
Optional bHeadersFootersOnly As Boolean = True,
Optional bAddHeadersFootersOnlyIfShowing As Boolean = True) As Collection
Dim colRet As Collection
Dim rngStory As Range
Dim hf As HeaderFooter
Dim oSec As Section
On Error GoTo l_err
Set colRet = New Collection
If oDoc Is Nothing Then
Set oDoc = ActiveDocument
End If
'easy story ranges, if desired
If bHeadersFootersOnly = False Then
For Each rngStory In oDoc.StoryRanges
Select Case rngStory.StoryType
Case wdCommentsStory, wdFootnotesStory, wdEndnotesStory, wdMainTextStory
colRet.Add rngStory.Duplicate
End Select
Next
End If
'headers/footers (for each of story ranges not reliable!!)
For Each oSec In oDoc.Sections
For Each hf In oSec.Headers
'don't add linked ranges
If hf.LinkToPrevious = False Or oSec.Index = 1 Then
'don't add hidden ranges, unless specified
If bAddHeadersFootersOnlyIfShowing And hf.Exists _
Or bAddHeadersFootersOnlyIfShowing = False Then
colRet.Add hf.Range.Duplicate
End If
End If
Next


For Each hf In oSec.Footers
'don't add linked ranges
If hf.LinkToPrevious = False Or oSec.Index = 1 Then
'don't add hidden ranges, unless specified
If bAddHeadersFootersOnlyIfShowing And hf.Exists _
Or bAddHeadersFootersOnlyIfShowing = False Then
colRet.Add hf.Range.Duplicate
End If
End If
Next
Next
l_exit:
Set fGetStoryRanges = colRet
Exit Function
l_err:
'any errors, blackbox and return an empty collection
Set colRet = New Collection
Resume l_exit
End Function


For the OP - for your purposes, you could comment out the block which returns ranges not in the header/footer -- but I leave that in for this sample code, so others may see the parts of the story ranges collection which works, and the ones that don't

glencoe
02-06-2014, 11:33 AM
Thanks Frosty!

glencoe
02-07-2014, 05:23 AM
Hi guys,

OK, after testing the different loops in my code, I have finally picked up macropod's first option, as it is easier to integrate within my code. Now, I did find lots of valuable information in all your suggestions, so thank you very much everyone for sharing on this topic! Frosty, your code is amazing indeed! fumei, you spent lots of time trying to educate me, and I really appreciate it! I know it was not an easy task ;-)

macropod, the only major point I changed in your code was adding Frosty's suggestion "If .LinkToPrevious = False Or Sctn.Index = 1 Then", which seems to do the trick for the first section... ;-)

Now, I still have a minor issue to solve.
For my own purpose, I adapted the "find and replace" part, so I had to add first another loop, to make sure all my "tags" can be replaced before processing them. This is the part where I need further help:



Dim TempStart As Integer
Dim r, fnd As Range

Set r = HdFt.Range
Set fnd = HdFt.Range
With fnd.Find
.Execute (Tag1)
TempStart = fnd.End
End With


Is there a way to know whether .Find found something, in which case I would update TempStart; if nothing is found, I don't want to update it...

Frosty
02-07-2014, 06:29 AM
You should be able to use either .execute (which returns true if it's found) or .Found. Read up on help on the find object as well as the execute method. They tell you what they can do. This will also help you learn other items too.

for section never... You already have that answer ;) it's the .index property of the section object (which you're already using to make sure you process the first section's headers/footers (my addition to your code)

glencoe
02-07-2014, 07:23 AM
Frosty,

I actually tried adding this to the loop:


With fnd.Find
.Execute (Tag1)
if .Execute = True then
TempStart = fnd.End
End if
End With

but this actually executes the search again! So I do need to read more about that indeed. Thanks for the tips, I will check them. I guess that .Found may probably work better (and is safer as well).

As for my other issue, I found indeed that the .index property is showing the section number, so I deleted this part from my last post as I managed to solve it. But thanks for catching and confirming it anyway!

glencoe
02-07-2014, 07:38 AM
Confirmed: this is my "final" code regarding all the issues I posted on this thread. As a note, Tag1 is given by the user in a textbox:


Dim oSection As Section
Dim HdFt As HeaderFooter
Dim r, fnd As Range
Dim Tag1 as string

With ActiveDocument
For Each oSection In .Sections
For Each HdFt In oSection.Headers
With HdFt
DoEvents
If .LinkToPrevious = False Or oSection.Index = 1 Then
HdFt.Range.Select

'Find 1st Tag
Set r = HdFt.Range
Set fnd = HdFt.Range
With fnd.Find
.Execute (Tag1)
If .Found = True Then
TempStart = fnd.End
End If
End With

'Do whatever else is required...

End if
End With
Next HdFt
Next oSection
End With

Frosty
02-07-2014, 07:53 AM
I'm not sure that you need do events.

Also, I really don't like multiple declares in a single line, at least one of the reasons you've demonstrated:
dim r, fnd as range
does not declare both items as a range. R is declared as a variant. You end up setting it to a range later, but this is probably not what you intended.

Frosty
02-07-2014, 07:55 AM
Oh, and why do you need to select the range? Is that a left over from testing? No reason to have that, except while trying to understand working with ranges (very limited exceptions to this rule. Very)

glencoe
02-07-2014, 08:08 AM
Good catch!
DoEvents was a leftover from a previous debugging attempt, but you're correct, I can remove it now, since the loop is working.
As for my wrong declaration, I truly didn't know that! Thanks for the teaching. I'll have to go through all my code to fix every other wrong declaration... :-(

As for selecting the range, well, I tried (really hard) without it, as everyone said it was not the right way to do it indeed. That part of code does work without this line indeed, but I have to say that after this part of the code, I am doing other actions on the text, and I need to play directly with the text within the document, and that's the only way I found it to work. But at least, it's only 1 extra line, while all my code has been reduced by at least 70% using the posted method (compared to the "manual" action I was using previously, which was a nightmare and awfully slow).

Frosty
02-07-2014, 08:30 AM
I would hazard a guess that you just don't quite understand working with the range object yet... 99% of the time, that's the reason why .Select is "needed" -- but the main reason to get rid of .Select is processing speed... (If word doesn't have to actually select an area of the document to work on, it doesn't have to refresh the computer screen, and verify the new selection has all the right buttons like bold/underline pressed or not pressed, etc etc).

So if you're happy with your processing speed, leave the next step to understanding range objects to another day.

The better improvement to your code, at this point, would be to encapsulate functionality.

Work on a routine that accepts a range argument (which you would pass from all your finding stuff), then build that routine to *only* work on whatever text manipulation you're going to do.

Basically, try to separate the actions of:
1. Looping through headers/footers in a document and getting each unique range
2. Finding a particular range within the range from #1 (which will really be the space between two other ranges)
3. Manipulation of the text within each range from #2

the benefit to this structure (as opposed to one big single routine which does all of the above) is that you can test the individual components without having to run the whole routine.

You can just use the immediate window to type
mytestsub Selection.Range

where mytestsub accepts one range parameter.

this may be too much for now... But down the road, keep the thought about encapsulating functionality and at some point, it'll click. Good luck!

glencoe
02-07-2014, 08:59 AM
I know you are right, but I have already come a long way from my original code (which you DO NOT want to see, you'd be really ***ed off) to the one I have now. I still need to get my mind on all this range stuff, which I certainly do not master well now, obviously. But you sound encouraging, and I like to think that way that working hard to have a clean code that is 100% optimised can gain a lot of speed in the end!
I will get there at some point, in which case I will probably need more help from you all!

If you want to give me more insight, you are more than welcome to. Here is the whole loop as it is right now. Just a few notes about it:
- I may have forgotten to include some declarations here, as I am copying/pasting them from different places, but every variable is declared in my code
- I copied only part of the code that is processing the text. I have a few other things not related to it here and there in the code
- you may see some French in the comments or variable names, as French is my first language ;-) I tried to translate quickly everything, but I may have forgotten a few things here and there



Dim oSection As Section
Dim HdFt As HeaderFooter
Dim r as range
Dim HdFt as range
Dim Tag1 as string
Dim Tag2 as string


With ActiveDocument
For Each oSection In .Sections
For Each HdFt In oSection.Headers
With HdFt
If .LinkToPrevious = False Or oSection.Index = 1 Then
HdFt.Range.Select
TempPrev = 1

'Find position of 1st Tag1
Set r = HdFt.Range
Set fnd = HdFt.Range
With fnd.Find
.Execute (Tag1)
If .Found = True Then
TempStart = fnd.End
End If
End With

While TempStart >= TempPrev + Len(Tag1)

'Find position of next tag1
Set fnd = HdFt.Range
fnd.Start = TempStart
With fnd.Find
.Execute (Tag1)
If .Found = True Then
PosTag1 = fnd.Start
End If
End With

If Tag1 <> Tag2 Then
'Find position of Tag2
Set fnd = HdFt.Range
fnd.Start = TempStart
With fnd.Find
.Execute (Tag2)
If .Found = True Then
PosTag2 = fnd.Start
End If
End With
Else 'if tag1 = tag2
PosTag2 = PosTag1
End If

'THIS IS WHERE THE .SELECT IS REQUIRED! From here, you may find ways to improve my code
If PosTag2 <= PosTag1 And PosTag2 > TempStart Then 'correct order, replacing tags
'Highlighting text between tags
Pos = Application.Selection.MoveLeft(, 1, wdMove) 'place cursor at the beginning of the header
Pos = Application.Selection.MoveRight(, TempStart - 1, wdMove) 'move cursor after tag1
Pos = Application.Selection.MoveRight(, PosTag2 - TempStart, wdExtend) 'select text between both tags
Selection.Range.HighlightColorIndex = Couleur1_Selectionnee 'highlighting text with chosen color
Application.ScreenRefresh

'Deleting tags
Set r = HdFt.Range
Set fnd = HdFt.Range

'Replace Tag1
fnd.Start = TempStart - Len(Tag1)
With fnd.Find
.Forward = True
.Wrap = wdFindStop
.Text = Tag1
.Replacement.Text = ""
.Execute Replace:=wdReplaceOne, Forward:=True, Wrap:=wdFindContinue
End With

'Replace Tag2
fnd.Start = PosTag2 - Len(Tag1)
With fnd.Find
.Forward = True
.Wrap = wdFindStop
.Text = Tag2
.Replacement.Text = ""
.Execute Replace:=wdReplaceOne, Forward:=True, Wrap:=wdFindContinue
End With

TempStart = PosTag2 - Len(Tag1)
TempPrev = TempStart
HdFt.Range.Select
Else
'wrong order, skip case
TempStart = PosTag2
TempPrev = TempStart
End If

'Find next tag1
Set r = HdFt.Range
Set fnd = HdFt.Range
fnd.Start = TempStart
With fnd.Find
.Execute (Tag1)
If .Found = True Then
TempStart = fnd.End
End If
End With
Wend
End If
End With
Next HdFt
Next oSection
End With
End If

Frosty
02-07-2014, 06:05 PM
Code never irritates me. Everyone learns at a different pace. The reason I am on this forum (and I would imagine many of the other contributors) is to help teach and also learn ourselves. I'm always learning new things, and I've been working and programming in Word for almost 20 years.

It looks to me like the reason you're using Selection is because you haven't explored the analogous .MoveStart and .MoveEnd methods available off the range object (you're using .MoveLeft and .MoveRight methods off the selection object). With minor caveats (.MoveDown and .MoveUp aren't available off a range object, whereas they are available off the Selection object), you can achieve the same thing you're doing (which is basically translating .MoveLeft with .MoveStart and .MoveRight with .MoveEnd

That said, when you're working with ranges, you have to understand that you're working with an actual number... so while .MoveLeft wdcharacter, 1 will collapse the selection (if it's not an insertion point) and move it one character to the left... you would need to use a negative number with the range, so .MoveStart wdcharacter, -1 would do the same thing. But if your range wasn't an insertion point, you might need to use the .Collapse method on the range as well.

You should play around with this stuff... you'll get the hang of it if you stick with it. And you'll end up with much faster processing (especially since you said this typically operates on long documents).

Couple other notes...

1. Try to avoid giant (and useless) With... End With blocks. The entire code snippet contained above is within a With ActiveDocument block. You only use it once, at the top of your For Each loop... just do a For Each oSection In ActiveDocument.Sections -- and you can remove one level of indenting. Same with the HdFt with chunk... you rarely use it, and it's not helpful to your coding... so just get rid of that With statement as well -- and you've removed another indented chunk for no reason.

2. You can save your .Found logic and simply use If .Execute (Tag1) = True Then... The .Execute function returns True if it's found.

3. Prefixes prefixes prefixes. Use appropriate prefixes for your variables... it's much easier to debug code that way, and easier for others to read. Tag1 should be sTag1 (since it's a string) or strTag1 (those are the two common prefixes for a string type). Range should be "rng" or "r" or "rg" .... TempStart and TempEnd should be lStart/lngStart or lEnd or lngEnd. The actual prefix doesn't matter, but consistency does...

There are lots of little rid-bits... but take them in small chunks, apply, and watch your programming get easier and easier!

Good luck!

gmaxey
02-07-2014, 08:02 PM
Jason,

You and I have had a discussion about the apparent StoryRange bug before :yes

I've gone back through my notes and found the following commented code that does (I think) a fair job of illustrating the issue and necessary fixes:


Sub SetUpExample()
'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 lngBugExterminator 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 not 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 defines and expands the storyrange collection
'to include the six header and footer storyranges (6 through 11).
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 header and footer will now 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
'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 (6 through 11) 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.
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 so the error occurred.
'It is content in Section 1 that defines the 6 header/footer storyranges.
'To ensure they are defined all you need to do is reference one of them:
lngBugExterminator = oDoc.Sections(1).Headers(1).Range.StoryType
Resume Err_ReEntry
'What I've learned is that lngBugExterminator 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

Sub PracticalExample()
Dim oRngStory As Word.Range
Dim i As Long
Dim lngBugExterminator As Long
'Create reference to one of the first section header storyranges. This ensures the six header/footer storyranges are defined.
lngBugExterminator = ActiveDocument.Sections(1).Headers(1).Range.StoryType
For Each oRngStory In ActiveDocument.StoryRanges
'Iterate through all linked stories
Do Until oRngStory Is Nothing
With oRngStory
Select Case .StoryType
Case wdEvenPagesHeaderStory, wdFirstPageHeaderStory, wdPrimaryHeaderStory, wdEvenPagesFooterStory, wdFirstPageFooterStory, wdPrimaryFooterStory
If Len(.Text) > 1 Then
Debug.Print .Text
End If
End Select
End With
Set oRngStory = oRngStory.NextStoryRange
Loop 'Until oRngStory Is Nothing
Next
End Sub

fumei
02-08-2014, 12:25 AM
glencoe, I do wish you would actually SAY what it is you want to do, with some example of before and after. You have not done this and I strongly suspect that this is much more complicated than it needs to be. AND I still find the idea of what USERS need to do really debatable. But then, again, you do not actually state (with examples) what it is you need to happen.

fumei
02-08-2014, 12:27 AM
oops, double post, now deleted.

glencoe
02-08-2014, 06:55 AM
Frosty,
Thank you so much for your valuable help again! You are 100% right on everything. Lots of things to improve here (and to learn, of course). As you know, fumei was the first to teach me on this thread, and you guys then hopped in and went even further. The only thing that scares me actually is that I have already done quite a bit of programming on this project, and though I always knew from the start that it would require lots of optimization, I'm afraid that what you show me here is only a small proof of what I will need to do next... Lots of work involved indeed. But I am thrilled to imagine what would be the result! Speed and performance will be so much improved! :-)
I will go through your suggestions and see how I can improve my code. I know I haven't gone through all the .range methods that would do the same, but only better. But I will now.

gmaxey,
Does your code suggest that the approach I followed here is also "bugged" because of the StoryRange method? I will have to study your code closely.

fumei,
I didn't tell you everything indeed, I only said what was strictly necessary to this thread. But you are right, this whole discussion is only part of a much bigger project. So here you go...
To start with, I am a technical translator (from English to French, though this is irrelevant here). I used to be an electronics engineer, but moved to the translation industry a couple of years ago. To make a short story, a client asked me to develop (for free, and for their internal resources) a macro that would clean French Word documents generated by different translation software. My strength here is not as much programming, but rather being able to understand the issues behind the French language and apply them in a code (though very awkwardly, I must say).

Now, what do I mean by "clean Word docs"?
As you understand, every language has grammar rules. Some of them are simple. French is rather complex. We pinpointed about 40 rules that could be automated. Rules like deleting extra (unnecessary) spaces, applying punctuation rules (like using a hard space in front of a : symbol or between specific strings of characters), replacing English quotation marks (" ") by French quotation marks (« »), applying "French" as the language of the whole document (for the spellchecker), and so on. Basically, the whole idea is to accelerate the proofreading process and deliver a clean file. Yes, we do use powerful spellcheckers (more powerful than Word's), but their process is not fully automated, so using this macro does optimize the task. And, as you probably guessed it, one of the tasks is to clean those "tags" and highlight text in-between. Now, those "tags" are not tags as such (like html coding), but only markers input by the translator in some translation software that cannot handle highlighting features. Those markers are therefore processed through the macro to highlight text. That's it. I may come up with other features later on with those markers, but right now, that's the task I am working on.
Now, because translation software are evolving and also use their own tagging system (and here, I mean tags like html coding that are used internally by the software), I didn't want to force users using specific tags (like ##text to be highlighted##), but rather let them chose them. The only restriction I am giving is to use a minimum of 2 characters per tag. Users are advised to use tags like ## ##, or #< ># or other similar strings.
Of course, the whole function is integrated within the macro and displays a real-time log of processed tasks/replacements and so forth.

As a side note, exporting files like PDFs in Word can give very unpredictable and messy results, like adding text boxes in headers/footers! So this partly answers your concern about messy text boxes in headers. I am not responsible for them| I have no choice but to process them as well...

Now, to come back to the project, I have already programmed most of it, and it works nicely (though I know speed is far from optimized due to my poor programming approach), and I will have to tackle the big task to optimize the whole thing at the end; that's what scares me most!
That's it. You know the whole story now... ;-)

gmaxey
02-08-2014, 08:14 AM
gmaxey,
Does your code suggest that the approach I followed here is also "bugged" because of the StoryRange method? I will have to study your code closely.

From the example code you provided here, it does not appear that you are using StoryRanges. If you are, and if you don't create a reference to a section 1 header or footer (i.e., using lngBugExterminator) then you could have a situation where the header footer storyranges don't exist and therefor are not processed.

I'm in Gerry's camp on with his assessment of over complicated. You have told us what you are doing with tags in the headers, but you haven't shown us.

I added text like Text ##Test## and **Test** to a header and ran (after fixing compile errors) your code and nothing happens.

Can you provide an example of before and after header text?

glencoe
02-08-2014, 08:55 AM
I added text like Text ##Test## and **Test** to a header and ran (after fixing compile errors) your code and nothing happens.

Can you provide an example of before and after header text?

First, the code as it is does work, even though it can probably be simplified indeed.
Tag1 and Tag2 are those ## or whatever else you want them to be.
The example you gave is correct: assuming you have the following in a header:

##text to be highlighted##

then the code I provided will replace it with

text to be highlighted

and the highlight color is selected by the user (in an interface before running the macro).
I'm not sure what else I could provide exactly...

gmaxey
02-08-2014, 09:43 AM
glencoe,

Of course I won't argue if you code is working for you. Here is didn't work with the tags defined:

Tag1 = "##"
Tag2 = "**"

When I changed that to:
Tag1 = "##"
Tag2 = "##"

It still did nothing. Then I changed:
TempPrev = 1
to:
TempPrev = 0

and I had "mixed" results, but at least something happened.

Now looking at the example you have just provided, something like this works (for that example):



Sub ScratchMacro()
'A basic Word macro coded by Greg Maxey
Dim oSection As Section
Dim oHF As HeaderFooter
Dim oRng As Range, oTextRng As Range
Dim strTag As String
strTag = "##"
For Each oSection In ActiveDocument.Sections
For Each oHF In oSection.Headers
With oHF
If .LinkToPrevious = False Or oSection.Index = 1 Then
Set oRng = oHF.Range
With oRng.Find
.Text = "(" & strTag & ")(*)(" & strTag & ")"
.MatchWildcards = True
.Replacement.Text = "\2"
While .Execute(Replace:=wdReplaceOne)
oRng.HighlightColorIndex = wdBrightGreen
oRng.Collapse wdCollapseEnd
Wend
End With
End If
End With
Next oHF
Next oSection
End Sub

fumei
02-08-2014, 04:57 PM
Which is a heck of lot simpler.

Sorry, but I still find this almost unbelievable. Are you telling me that you could - with virtually no rules apparently ("## ##, or #< ># or other similar ") - that you could have:

##text blah blah##
#<text blah blah>#

Because if there ARE rules, surely you can not have 40 DIFFERENT start and end identifiers. And even if you DO, then applying reverse rules is still going to be easier than some of the stuff that is being attempted.

fumei
02-08-2014, 05:05 PM
And I will repeat myself for the third or fourth time...

If these tags (or identifiers) NEED to be changed, why why why why why why are the users doing ANY of this? This should be an automated process untouched by the users. Or are you saying SOME of these identifiers will stay, and some need to be changed.

If they all need to be changed, then I will flat tell you that any concerns about processing time are nothing compared to what is needed when users are involved. The time to take an input from the users will more than cover any time by processing.

glencoe
02-08-2014, 05:10 PM
gmaxey,

Your code looks so much simpler indeed! I will try it shortly. You would probably faint seeing the original code I had for the same loop (basically it was all manual search, character by character, using the same kind of ".moveright/.moveleft" procedures as I still have in the code I produced here. The whole loop must have gone from 200 lines to... less than 20! That's a heck of an optimization! :-) And I am not even talking about the effect on speed...

fumei, maybe my explanation was not clear, as I am confused at what you say. I have about 40 different tasks for the whole project (most of them being applying grammar rules). The current code of this thread is only processing ONE of the tasks, that is, processing the "commented part" added by the translator. There is no grammar rule involved here. The only "rule" concerns the markers (tags) being used to find those comments.
Does it make more sense to you?

fumei
02-08-2014, 05:16 PM
Oh, OK. That does make sense. I will repeat what Frosty has mentioned. Break off functions into workable chunks. Trying to put everything into one routine is a BAD BAD idea. It leads to confusion and terrible debugging problems.

One confusion that still remains is why users are involved at all.

glencoe
02-08-2014, 05:21 PM
fumei,

The user (translator) adds tags in the translation software where he wants to comment text. Then he exports the translation into Word, which is the document processed through this macro, and that's where the automation starts.

As every tag is added by a human in the original document, errors are possible. Such errors are for example adding an opening tag, but forgetting to close it; as if you were opening a bracket in your sentence, but oops, you forget to close it... In such a case, I cannot process ALL tags, as one pair is not complete; my choice is to ignore tags that are not properly paired. I could possibly highlight tags that are single, to show the user something is missing.

Otherwise, if the document contains matched pairs of tags, then yes, ALL of them should be replaced (deleted and text highlighted).

fumei
02-08-2014, 05:39 PM
Then there should be no user involvement whatsoever. Whoever has control of the files does an automated batch processing. And I have to say this is horribly sloppy. So even after all this crap, you can have instances of text with single "tags'. Obviously you do not want that (otherwise why bother trying to clear out matching tags), but you have to accept this?

And talk about time involved, the tags (identifiers of comments) are manually entered. Oh boy. And if the SAME user is adding the tags:

The user (translator) adds tags in the translation software where he wants to comment text. Then he exports the translation into Word, which is the document processed
Then to me this is a training issue. Tell them to use ONE type of "tag".

Sheeech.

This reminds me of the silliness in documentation and translation when I worked for the federal government.

Oh and going back (again) WHY is this going on in the headers. Do you have code to deal with the main body. I am assuming this stuff is going on there as well.

glencoe
02-08-2014, 05:53 PM
fumei,

Please don't take it too personally! ;-)


Then there should be no user involvement whatsoever. Whoever has control of the files does an automated batch processing.
Indeed, it is automated batch processing... I didn't say otherwise.


So even after all this crap, you can have instances of text with single "tags'. Obviously you do not want that (otherwise why bother trying to clear out matching tags), but you have to accept this?
I didn't say I accept this, I only say that I cannot simply delete single tags like that. The idea, really, is to leave it so the user sees he forgot the matching tag, in which case he can add it along and run the automated task again. As I said, if I don't take human error into account, I would have a bigger mess whatsoever...


And talk about time involved, the tags (identifiers of comments) are manually entered. Oh boy. And if the SAME user is adding the tags. Then to me this is a training issue. Tell them to use ONE type of "tag".

Yes, it is a training issue, but everyone makes mistakes as well, so even the most rigorous person may forget to close **some open bracket** in a sentence, and in my case, may forget to add the closing tag where it belongs... Got it?
And, as I said, I do not want to use ONE type of "tag", as it might be used as an internal code by translation software. This is a risk I don't want to take, as I do not know/use/have control on the software used to produce the Word file. Let's assume one moment that ## ## is an internal tag used by such software. What would happen to the file once it is exported into Word? I have absolutely no idea! Maybe the text would be underlined, or put in upperscript? Who knows? I'd rather let this open, so that any evolution made to translation software won't impact my code.


This reminds me of the silliness in documentation and translation when I worked for the federal government.
Don't laugh. Government might be involved behind this as well.


Oh and going back (again) WHY is this going on in the headers. Do you have code to deal with the main body. I am assuming this stuff is going on there as well.
Currently, I am working with the headers. But yes, the main body is also involved, just like any other part of the file (textboxes, end notes, etc.).

glencoe
02-08-2014, 07:20 PM
gmaxey, you will need to explain me a bit what you are doing in your code, as this is beyond my understanding...

I don't understand why, but the line Set oRng = oHF.Range deletes the content of my first header, while placing the mouse on "oHF.Range" says it is = "".
So I updated this line to if oHF.Range <> "" then Set oRng = oHF.Range
The setting then works.

Now, I am not very knowledgeable with wildcards, so I am not sure how your .Text works. Same for .Replacement. What is the \2 supposed to do?
Actually, I tried your code and it does work with a tag that is fixed (same opening and closing tag), but as soon as I use different opening/closing tags, the code doesn't work as expected. Of course, I would then have strTag# = "#<" and strTag2 = "#>" and the .Text would be updated to .Text = "(" & strTag1 & ")(*)(" & strTag2 & ")"

Please advise.

gmaxey
02-09-2014, 01:12 AM
glencoe,

I can't explain the range issue because it isn't happening here.

As for the wildcard search it works like this.

The "( )" in the .Text string are wildcard grouping symbols. So we are searching for three groups 1) the opening tag, 2) any text 3) the closing tag.
The "\2" in the .Replacement.Text string simply means to replace what was found (i.e., the opening tag, any text, and the closing tag) with the content of group 2 (i.e., just the text)

The following code will find text tag like this "##Test**"

Notice the closing tag string "\*\*" may look a little odd. This is because "*" in a wildcard search means "anything" where in this case we want to search for the literal "**" so we add the "\" before each literal character.

Hope this helps.

Sub ScratchMacro()
'A basic Word macro coded by Greg Maxey
Dim oSection As Section
Dim oHF As HeaderFooter
Dim oRng As Range, oTextRng As Range
Dim strOTag As String, strCTag As String

strOTag = "##"
strCTag = "\*\*"
For Each oSection In ActiveDocument.Sections
For Each oHF In oSection.Headers
With oHF
If .LinkToPrevious = False Or oSection.Index = 1 Then
Set oRng = oHF.Range
With oRng.Find
.Text = "(" & strOTag & ")(*)(" & strCTag & ")"
.MatchWildcards = True
.Replacement.Text = "\2"
While .Execute(Replace:=wdReplaceOne)
oRng.HighlightColorIndex = wdBrightGreen
oRng.Collapse wdCollapseEnd
Wend
End With
End If
End With
Next oHF
Next oSection
End Sub

glencoe
02-09-2014, 05:30 AM
gmaxey,

Thanks for the explanation. Makes more sense to me now! ;-)
I also found a great page referencing in detail wildcards use in Word (not sure if there is a place on the forum to reference useful documentation) here: http://www.gmayor.com/replace_using_wildcards.htm

Now, I also foresee a problem using wildcards search in this case: as you perfectly explained, we need to use a \ to really search the literal * character.
In other words, all wildcards "special" characters should be avoided in the tags, or I would have to add a \ before any of them: ( [ ] { } < > ( ) - @ ? ! * \ )
I assume this makes sense, right? I found this issue by using #< and ># as opening/closing tags.

Now, though your code is great, it replaces all occurrences blindly without doing any order check (to make sure that there is no strOtag in between a correct pair). I am a bit concerned by this issue, and I'd like to find a nice fix for it. Maybe the solution would be the check in the .Find result if the string strOtag exists, in which case I could highlight it in red or any other color, and skip to the next occurrence... Any better idea?

gmaxey
02-09-2014, 09:05 AM
glencoe,

Yes using wildcards does present its problems. This doesn't look as clean, but you can avoid them if you want:


Sub ScratchMacro2()
'A basic Word macro coded by Greg Maxey
Dim oSection As Section
Dim oHF As HeaderFooter
Dim oRng As Range, oTextRng As Range
Dim oRng2 As Range
Dim strOTag As String, strCTag As String

strOTag = "##"
strCTag = "**"
For Each oSection In ActiveDocument.Sections
For Each oHF In oSection.Headers
With oHF
If .LinkToPrevious = False Or oSection.Index = 1 Then
Set oRng = oHF.Range
With oRng.Find
.Text = strOTag
While .Execute
Set oRng2 = oHF.Range
oRng2.Start = oRng.End
With oRng2.Find
.Text = strCTag
If .Execute Then
Set oTextRng = oRng.Duplicate
oTextRng.Collapse wdCollapseEnd
oTextRng.End = oRng2.Start
oRng.Delete
oRng2.Delete
If Not InStr(oTextRng.Text, strOTag) > 0 Then
oTextRng.HighlightColorIndex = wdBrightGreen
Else
oTextRng.HighlightColorIndex = wdRed
End If
End If
End With
Wend
End With
End If
End With
Next oHF
Next oSection
End Sub

fumei
02-09-2014, 05:03 PM
Yes, it is a training issue, but everyone makes mistakes as well, so even the most rigorous person may forget to close **some open bracket** in a sentence, and in my case, may forget to add the closing tag where it belongs... Got it?
And, as I said, I do not want to use ONE type of "tag", as it might be used as an internal code by translation software. This is a risk I don't want to take, as I do not know/use/have control on the software used to produce the Word file. Let's assume one moment that ## ## is an internal tag used by such software. What would happen to the file once it is exported into Word? I have absolutely no idea! I will tell you one thing...you would be much much much much better off if none of this commenting "tagging" was done in your translation software, and ALL of it was done in Word.

You could make a unique tagging string (or no tagging and just highlight, since that seems to be what you end up with), fired by a keyboard shortcut (or a icon click, whatever), that would ALWAYS use open and closing "tags". You would never, ever have to deal with checking for a closer, or if there was an extra one.

Now, though your code is great, it replaces all occurrences blindly without doing any order check (to make sure that there is no strOtag in between a correct pair). I am a bit concerned by this issue, and I'd like to find a nice fix for it. Maybe the solution would be the check in the .Find result if the string strOtag exists, in which case I could highlight it in red or any other color, and skip to the next occurrence... Any better idea?
I do. Do all your tagging in Word to start with.

fumei
02-09-2014, 05:11 PM
Oh, and then you could do REAL comments. In any case, I will leave you in the capable hands of Greg, as he knows his stuff and is better suited to help you.

glencoe
02-09-2014, 07:45 PM
Thank you Greg! Will check your code tomorrow.

fumei, I understand your point, but this is a client's request... Later on, it may be a suggestion indeed to transfer those comments as real Word comments through the macro (but probably never commenting/highlighting manually directly in Word). There are a few good reasons to do it this way, but I don't think there is any need for me to explain them, as it would be totally off-topic here. Point is, I need to find a solution, and gmaxey seems to be guiding me on the right track... Plus, I like challenges! ;-)

glencoe
02-10-2014, 07:04 PM
gmaxey, I love your solution! The only thing I would actually change is highlighting only the strOTag found in instr instead of highlighting the whole range, only to give the user less manipulations to do manually after (i.e. manually clear the highlighted part, add the missing tag, run the macro again). Highlighting only the tags only requires the user to add the missing tag and run the macro again.
Anyway, I really took the time to consider your suggestion seriously, and while it is excellent, that is the only change I'd like to add. Now, don't just give me the answer yet, I want to do my homework before! ;-) Otherwise, I will never learn if you just give me everything on a plate... lolll

gmaxey
02-10-2014, 07:27 PM
glencoe,

Glad I could help and you are correct. Sometimes I give away too many fish. It is nice to know that you want to learn to catch your own fish.
If you need some help though, just post back.

glencoe
02-11-2014, 07:46 PM
OK, I've got it...

Now, I noticed something interesting: though the code is working exactly as expected, I didn't realize before running it through a test that it would actually erase the next correct pair of tags (following a bad pair)!

e.g. original text with tags

#<correct tags>#
#<wrong closing tag#
#<correct tags>#

would give at the very end of the loop:

correct tags
#<wrong closing tag#
correct tags

And when I think about it, this makes more sense than what I actually wanted to accomplish... So I thought I could get the code to do it directly instead. The code is also simpler that way!
I copied below both options (of course, it is either one or the other). The first one is the one I was talking about in my previous post. And I applied what I learnt with your help! Hopefully I got it right.

I also commented each line to make sure I understood properly its meaning and action.
I would really appreciate if you could double check both the code and my comments and let me know if I made a mistake somewhere!



Dim oRng As Range, oRng2 As Range, fnd As Range, oTextRng As Range, oTextRng2 As Range

For Each oSection In ActiveDocument.Sections
For Each oHF In oSection.Headers
With oHF


'1st option
If .LinkToPrevious = False Or oSection.Index = 1 Then
Set oRng = oHF.Range 'Whole header
Set oRng2 = oHF.Range 'Whole header
With oRng.Find
.Text = strOTag 'Opening tag
While .Execute 'Find opening tag (oRng)
oRng2.Start = oRng.End 'Starting after opening tag found above
With oRng2.Find
.Text = strCTag 'Closing tag
If .Execute Then 'Find closing tag (oRng2)
Set oTextRng = oRng.Duplicate 'Duplicating oRng. Now oTextRng = opening tag
oTextRng.Collapse wdCollapseEnd 'Starting position of oTextRng is now the end of Opening tag
oTextRng.End = oRng2.Start 'End of oTextRng = before Closing tag. Therefore oTextRng is the string between opening/closing tags
If Not InStr(oTextRng.Text, strOTag) > 0 Then 'No extra opening tag found in oTextRng, therefore pair of tags is correct. Processing tags and highlighting text
oRng.Delete 'Delete opening tag
oRng2.Delete 'Delete closing tag
oTextRng.HighlightColorIndex = Couleur1_Selectionnee 'Highlight string (this color is chosen by the user)
Else 'opening tag found within the string. Highlighting tags only in another color.
oRng.HighlightColorIndex = Couleur2_Selectionnee 'Highlight opening tag (this color is chosen by the user, though it will probably be red)
oRng2.HighlightColorIndex = Couleur2_Selectionnee 'Highlight closing tag
Set oTextRng2 = oTextRng.Duplicate 'Duplicate string range
With oTextRng2.Find
.Text = strOTag 'Opening tag
If .Execute Then 'Find opening tag in string
oTextRng2.HighlightColorIndex = Couleur2_Selectionnee 'Highlight extra opening tag
End If
End With
End If
End If
End With
Wend
End With
End If


'2nd solution
If .LinkToPrevious = False Or oSection.Index = 1 Then
Set oRng = oHF.Range 'Whole header
Set oRng2 = oHF.Range 'Whole header
With oRng.Find
.Text = strOTag 'Opening tag
While .Execute 'Find opening tag (oRng)
oRng2.Start = oRng.End 'Starting after opening tag found above
With oRng2.Find
.Text = strCTag 'Closing tag
If .Execute Then 'Find closing tag (oRng2)
Set oTextRng = oRng.Duplicate 'Duplicating oRng. Now oTextRng = opening tag
oTextRng.Collapse wdCollapseEnd 'Starting position of oTextRng is now the end of Opening tag
oTextRng.End = oRng2.Start 'End of oTextRng = before Closing tag. Therefore oTextRng is the string between opening/closing tags
If Not InStr(oTextRng.Text, strOTag) > 0 Then 'No extra opening tag found in oTextRng, therefore pair of tags is correct. Processing tags and highlighting text
oRng.Delete 'Delete opening tag
oRng2.Delete 'Delete closing tag
oTextRng.HighlightColorIndex = Couleur1_Selectionnee 'Highlight string
Else 'opening tag found within the string. Highlighting tags only in another color.
oRng.HighlightColorIndex = Couleur2_Selectionnee 'Highlight opening tag
End If
End If
End With
Wend
End With
End If
End With
Next oHF
Next oSection


Also, I still haven't gone through all the process renaming variables in all of my code. If you have more advise about a good rule of thumb... string variables would start by "str". integers, by "int" I assume? any other suggestion?

gmaxey
02-11-2014, 09:15 PM
Looks like you've achieved your goal and understand how you did it.

As for variables, I suppose each person has their own style. I usually proceed all objects (things that must be set) with o (e.g., oRng, oFld, oShp, oBM, etc.)

I use str text e.g., strText
I rarely use integers and use longs instead lngCount, lngIndex etc.

bSet, bState etc. for bollean

Just for you to compare, this is what I thought you might be after:


Sub ScratchMacro2()
'A basic Word macro coded by Greg Maxey
Dim oSection As Section
Dim oHF As HeaderFooter
Dim oRng As Range, oTextRng As Range
Dim oRng2 As Range, oRngTag As Range
Dim strOTag As String, strCTag As String

strOTag = "##"
strCTag = "**"
For Each oSection In ActiveDocument.Sections
For Each oHF In oSection.Headers
With oHF
If .LinkToPrevious = False Or oSection.Index = 1 Then
Set oRng = oHF.Range
With oRng.Find
.Text = strOTag
While .Execute
Set oRng2 = oHF.Range
oRng2.Start = oRng.End
With oRng2.Find
.Text = strCTag
If .Execute Then
Set oTextRng = oRng.Duplicate
oTextRng.Collapse wdCollapseEnd
oTextRng.End = oRng2.Start
oRng.Delete
oRng2.Delete
oTextRng.HighlightColorIndex = wdBrightGreen
If InStr(oTextRng.Text, strOTag) > 0 Then
Set oRngTag = oTextRng.Duplicate
With oRngTag.Find
.Text = strOTag
While .Execute
oRngTag.HighlightColorIndex = wdRed
oRngTag.Collapse wdCollapseEnd
Wend
End With
End If
oRng.Start = oTextRng.End
End If
End With
Wend
End With
End If
End With
Next oHF
Next oSection
End Sub

glencoe
02-12-2014, 05:54 AM
I'm glad you updated the oRng.Start to the end of the strings, as I was actually wondering yesterday if this was required or not in ranges! I was also wondering how the .Find function really works on ranges. Would the execution be faster if the position of the "cursor" is closer to the next result (rather than at the beginning of the range)? I know the cursor is not really involved here, though, but it is more the range being shortened.

I also wonder what is the difference between oTextRng.Collapse wdCollapseEnd and oTextRng.Start = oTextRng.End. I assume both do the same? I only ask for my own information, as that question popped my mind when seeing the collapse function in your code.

Also, you raised another good point I didn't think: checking if the opening string appears several times within the string. I forgot to implement that one indeed!

Now, I don't fancy the idea of highlighting the whole string when extra opening tags exist, but that's only a matter of "taste"... I managed to find a good compromise, and the final code is both optimized and working well, due to your help! Thank you very much Greg! :-)

gmaxey
02-12-2014, 06:23 AM
glencoe,

The little I know about VBA is self-taught so anything I say or do is based just on experience and what I've picked up along the way. Jason is probably more qualified to answer the technical questions.

You might find this interesting: http://gregmaxey.mvps.org/word_tip_pages/words_fickle_vba_find_property.html

I don't know if there is any performance difference between .Start = .End or .Collapse wdCollaspeEnd or not. If there is I wouldn't think it would matter a nit.

Glad you have learned something from the experience.

glencoe
02-12-2014, 07:43 AM
Well, whatever your experience is in VBA, it certainly is much greater than mine. So, the "little" you know cannot be compared to the little I know! What I know is also pretty much self-taught, I am just 20 years behind you! :-p

I will certainly check your page shortly! I also really appreciate learning more about ranges, as I do see the major benefits it brings to coding... It's like switching a horse for a Ferrari (ok, a Ferrari also has a "horse" logo, but you got my point).
As we have gone extensively through the topic, I think I will post the thread as "solved" shortly (unless Jason wants to add something).
I will certainly request more help when time comes. And hopefully I will be able to help others as well, once my own knowledge is more robust!

fumei
02-12-2014, 09:04 AM
I don't know if there is any performance difference between .Start = .End or .Collapse wdCollaspeEnd or not. If there is I wouldn't think it would matter a nit.
Not a nit, as they are numerically - and ranges are just numbers - identical. There is no change to the memory allocation. That is the advantage of ranges over Selection; a range of 1,000 characters (.Start = .End -1,000) uses exactly the same memory as a range of 5 characters (.Start = .End -5). Unlike Selections of the same number of characters, as Selection uses GUI memory allocations.

I rarely use integers and use longs
We should, as a best practise, only use Long as Integers are no longer used at all by VBA. You can declare them, but the parser converts them to Long on its initial parse pass. While I suppose you could say that the conversion itself takes time, and is true that the memory blocks (being larger for Long) need destroying and reallocation (and thus you really should never use integers), but I suspect that you would have to declare thousands and thousands of integers before you would be able to even detect anything. So yes you CAN declare integers, but why not just use what VBA uses...Longs.

glencoe
02-12-2014, 09:20 AM
Very interesting, fumei! Thank you for sharing!

Please all of you, give yourself a "good job tap on the shoulder" as you did convince me to forget (as much as possible) about Selection, and to move to Range instead... The memory allocation is just another extra benefit!

And I will also move all Integers to Longs instead. Now, I mostly use Integers for loops like "For i=0 to MaxValue Next", where MaxValue would be a low number (e.g. less than 100). In this case, would you still recommend moving to Long as well?


is true that the memory blocks (being larger for Long) need destroying and reallocation
Is this an operation that is done by the parser (I assume so), or that we have to add in the code?

fumei
02-12-2014, 12:59 PM
No the VBA garbage collector takes care of most memory allocations (including destroying). Allocation happens the moment you declare a variable such as a Long. You do not need to do anything else. It has been a hot debate for years whether you should explicitly destroy objects with a Set object = Nothing. For all "minor" objects IMO you do not need to, but it IS a good idea with ranges, and definitely a good idea with application objects. When you use application instances you should always destroy them when you are done. As for ranges, there are circumstances where it is best to explicitly destroy them as part of your code blocks. In theory, when you exit a routine with a Set object (for example Set oHF = a headerfooter object), that object is destroyed. But there is still persistent issues that pop up with application objects - i.e. "extra" instances of Word.

For loops like that I still use Long. I never use Integers at all. Like I said, VBA converts all integers to long anyway.

glencoe
02-12-2014, 01:12 PM
How about local variables declared in a sub routine? Is it still necessary to "destroy" them, knowing that they should disappear as soon as the code exits the sub routine, or is it possible that they still remain in memory, unless they have been set to Nothing through the code?

There will be lots of changes to implement in my current macro in order to follow the proper "rules" and rules of thumb! And I'm not even talking about rethinking the code to optimize it with Range functions!

fumei
02-12-2014, 02:58 PM
No, you can ignore local variables. VBA will tidy things up on its own. As stated, the only things of any realistic concern are application objects. You are not using any.

The only things you can destroy on your own are objects you give a value to with the Set instruction. In Greg's code there a few, all Range objects. As is, there is no need to explicitly destroy them with a Set = Nothing. VBA can handle things. I only really mentioned this subject as a comment on the nada performance issue regarding .Start = .End versus .Collapse.

Local variables (or even Public ones) such a Longs, or Strings are assigned a memory block when declared. For example, a Long is assigned 4 bytes. Once it is assigned, it remains (persists) until the routine that holds it terminates. So, regardless of VALUE ( 1 or 1,500 or 2,000,000), the declared Long uses 4 bytes for the Scope of the procedure. BTW, you should (if you have not done so) take a good look at what Scope means. When the procedure terminates, the block of 4 bytes is released by the VBA garbage collector. You do not have to do anything.

Obviously with our modern computers with gobs of memory 4 bytes is almost meaningless. Thus it is true that the old best practices are in some ways being made pointless. However, the principles of both understanding what is going on, and doing good housekeeping still seem valid to me. Some may argue otherwise.

glencoe
02-12-2014, 03:05 PM
Your explanation makes perfect sense, and I also think that by keeping everything tidy, you can't mess up your code! So I'd better go that way.
At least, if I tend to follow those simple rules, you won't be mad at me next time I show you some code I produced! ;-)

Thank you so much again! Let's close and solve officially this topic for now! :-)

gmaxey
02-12-2014, 08:15 PM
Gerry,

"Jason is probably more qualified to answer the technical questions."

No slight intended. It is just that Jason seems to enjoy explaining the technical side and usually is spot on.

A more appropriate statement would be: "Jason or Gerry are more qualified to answer the technical questions."

fumei
02-13-2014, 08:16 AM
"Jason is probably more qualified to answer the technical questions."

No slight intended.


Ummm Greg, it never even occurred to me to think that. Relax dude. I was simply responding to something in my nerdy way I thought I could comment on. It is more a reflection of my big mouth than anything else. No slight taken...even slightly. I have too high an opinion of you to go there (thinking there was an intended slight). It's all good.

gmaxey
02-13-2014, 08:43 AM
I'm relaxed. Really I'm relaxed!

glencoe
02-13-2014, 09:00 AM
You're funny guys.
From what I can tell, all of you have great knowledge in this field. Though you may have possibly gathered it from a different background, and some may be more technical than others, I doubt there is any "competition" between you (or intend to show off or to offend others in any way).
I assume that what brought you on this forum is, before anything else, your passion for programming and helping others! It really is great to be able to communicate with experts (as you are) without having to contact some technical hotline with people who first want to collect money before even hearing what your problem is.

Though I understand that beginners' questions (or points of view) may sometimes be frustrating to you, you keep helping us and leading us to the right direction.
So thank you for your help! Keep it that way!

fumei
02-13-2014, 10:46 AM
You are welcome. And questions are never really frustrating. What IS frustrating are people who clearly do not know things who refuse to listen, and insist that their incorrect ideas are correct. And insist that we are wrong. Well if you are so sure, why are you even asking. Greg for one has a massive store of practical knowledge - his web site shows that - and it is so weird to see someone blindly ignoring that.

You are doing well, willing to learn with a good attitude. You will do well.

gmaxey
02-13-2014, 11:12 AM
Gerry,

Well said. glencoe appears not only eager to catch his own fish, but to fish well and become a better fisherman! Things are starting to get a little gooey here so I'm going to step out :outtahere

glencoe
02-13-2014, 11:25 AM
Greg, I see that you like serving people, but feelings are not to be shown. Sounds like the good old times in the Navy? *Just saying it to show you I did have a quick look at your website! I haven't had time yet to go into much detail, but this certainly is a great resource to use! :yes

Frosty
02-13-2014, 01:30 PM
Glad you've all worked out the technical details, so I didn't have to come in and explain everything. Hehe.

I'm pretty self-taught as well, and I'm always learning things from others. For example: I didn't know that integers get converted to longs by VBA. Thanks, Fumei!

I would also guess that modifying the property of a range object (.Start = .End) is going to be exactly the same performance as use of the .Collapse method, and which to use is probably more preference of coding style... BUT -- you never really know. The thing about optimizing code for actual performance is that some actual bottlenecks in the processing of code will surprise you. But changing from use of Selection to Range is a huge benefit on performance by any measure, and should always be attempted at the outset.

But here's an example of a "waste" of performance optimization energy:
If Len(strText) = 0 Then
vs
If strText = "" Then

*Technically* -- testing the length of a string uses less processing power than checking the content of a string... but unless you were doing thousands upon thousands operations, the noticeable gain in performance will be significantly less (to me) than the obsfucation of code involving a string using a number value vs just checking to see if that string is blank or not. It also prevents easily converting to other logic structures (like a Select Case statement) without additional hoops to jump through. But if you were really really used to seeing that Len(strText) = 0, it would be both an optimization increase with no downside.

The lesson? Don't do granular performance optimization until you're at the stage of "polishing the cannonball" -- try to write your code as easily-readable as possible, and then find out where the bottlenecks are later.

This has been an enjoyable thread to participate in-- thanks for bringing such a good attitude, glencode.
- Jason aka Frosty

glencoe
02-13-2014, 02:11 PM
ha ha, "glencode"! That's a good one! My nickname is "glencoe" (after some great place in the Scottish Highlands), but I appreciate the joke, even if it was not made on purpose! ;-)

As for finding out what's the best code, I'm not sure how VBA works "behind the scene". I remember when I learnt a bit of C/C++ and Assembler (a while ago, don't ask me anything now, I forgot all of it), we were taught that each line of C is converted in Assembler, and therefore, one function in C will give more or less Assembler lines (*or something like that*), i.e. simple move/add operations.

Anyway, I only mean to say that it is not an easy task to guess which code will perform faster for the same result. It's a bit like picking up one solution among all the suggestions you guys gave me for my issue! All your codes looked great. Assuming they gave the *same* result (with some minor changes), which one would be the fastest? Hard to say. We may assume that a shorter line or a shorter number of lines may be processed faster, but as you pointed it out, it may not always be the case...
Also considering that PCs are always faster, bottlenecks may not bother some users as much as before (though that's not a reason to produce a low quality code).

I was also told once that it may be more efficient to code everything in a single routine rather than using many subroutines/functions with extra arguments. You guys said otherwise and I'm happy with that, since optimizing code is not just finding ways to use more efficient functions, but also writing the code cleverly.
I assume that it is better to call one routine 10 times rather than write it 10 times within the same project...

Daniel (glencoe)

fumei
02-13-2014, 05:50 PM
I was also told once that it may be more efficient to code everything in a single routine rather than using many subroutines/functions with extra arguments. You guys said otherwise and I'm happy with that, since optimizing code is not just finding ways to use more efficient functions, but also writing the code cleverly.
I assume that it is better to call one routine 10 times rather than write it 10 times within the same project... Hmmmm. I wonder about that person's background. I would strongly disagree with that. If efficient covers all aspects of writing code (including debugging), then it is an absurd statement.

I would agree with Jason. Do not sweat micromanaging tiny efficiencies. As a personal opinion, if you can work out using Ranges and the use of objects, those two things alone will improve the efficiency by multiple orders of magnitude. Oh, and two other things: Scope and the best methods for code routing and execution (e.g. when to use things like Select Case).

glencoe
02-14-2014, 08:51 PM
I got your point!
As for scope, I assume you talk about global/public or local declarations?

fumei
02-15-2014, 03:32 AM
Yes.

Speaking of frustrating...

There was a guy who posted code where the value of variable did not persist where he thought it should. We pointed out that it has fallen out of Scope. He would not believe that was the reason and went on and on that we were deliberately trying to keep something from him. Why aren't we telling him the real reason, telling him instead about some minor problem that could not possibly be the reason. He kept on insisting there was a "bug" in Word. He would not admit that a) he did not understand Scope; and b) he could possibly make such a newbie error. After all, didn't we know...he was an IT professional. maybe dude, but that does not mean you know crap.

glencoe
02-15-2014, 04:47 AM
Well, I'm glad I'm NOT an IT professional! :-p Though this would still be stupid, such an error from me would not be as bad! ;-)

If you notice an out-of-scope error from me, ask me to watch the variable through the debugging process, it should be a good hint! ;-)