Solved What is the correct way to linearly insert text into Word? (1 Viewer)

The_Doc_Man

Immoderate Moderator, Former MVP, Retired SysAdmin
Staff member
Local time
Yesterday, 23:34
Joined
Feb 28, 2001
Messages
17,341
I have searched this site and many others. Web searches galore, but nothing seems to work quite right for me.

I am making a document linearly. I.e. providing headers and text and bookmarks and other material in the sequence that I would expect them to be read. But it ain't working. I think I must have deeply offended the Word gods. Word from a GUI with mouse and keyboard can be a real pain, but from VBA is beginning to make me believe that we should shoot the entire Word developer group and start over. It is INSANELY difficult.

Trying to use ranges, I will try range.InsertAfter "text" but it goes in the wrong paragraph. Using range.InsertBefore puts things in the wrong order. When I try instead to use Selection.TypeText, what happens is that when I go back to apply formatting, it goes to the wrong place. Using Selection.TypeParagraph also puts things in the wrong place. I have tried various tricks to collapse the Selection object to become a simple insertion point but it won't stay that way. If I try to insert a paragraph marker, that doesn't seem to work. If I try to insert a line break, they go to the wrong place, like my insertion point isn't where I think it should be. It seems to be jumping all over the place and I can't make it behave.

I can generate ALMOST what I want by making an Access report, but that's not good enough. I want the features that include the ability to add an index entry and then pop out a names index. Not to mention that it has now become an intellectual challenge.

What is the correct way to linearly insert text and paragraph breaks in Word from VBA, more or less like I was typing it in the order it should be presented?
 

Micron

AWF VIP
Local time
Today, 00:34
Joined
Oct 20, 2018
Messages
3,409
I'm not at all familiar with the world of Word vba, but in your searching, did you visit the Office Object and VBA Language reference site? Checking out the InsertBefore method of the Range object, I expect that your text would be inserted before that range, where you seem to be saying you expect it to go before some block of text within that range, which may not be the same thing. All I can really do is point you to the resource. It is much much larger than it might originally appear to be, so be prepared to spend some time there if you're not familiar with it.

https://docs.microsoft.com/en-us/office/vba/api/overview/

Edited link - I had it pointing to the Access portion.
 

Isaac

Well-known member
Local time
Yesterday, 21:34
Joined
Mar 14, 2017
Messages
1,513
Well after a bit of reading my attention is being called to the concept of 'paragraphs' making it a bit more complex. Once you insert line breaks, I'm assuming you have to keep track of whether your range is now a paragraph, and if so, make sure to subtract 1 from the location. Given that the insertafter method specifically extends the range, that would constantly change the insertion point if the range is not re-defined.

Anyway I share your frustration...after doing some research on this, the first thing I came across was some code from Microsoft that totally didn't work. Referring to Document.Paragraphs(#).Start, when it is apparently supposed to be Document.Paragraphs(#).Range.Start .....Interesting stuff.

Does the attached file help at all? For "linearly", I was thinking if you just keep adding text to the end of Paragraphs, that may work?
 

Attachments

The_Doc_Man

Immoderate Moderator, Former MVP, Retired SysAdmin
Staff member
Local time
Yesterday, 23:34
Joined
Feb 28, 2001
Messages
17,341
I'll look over your presentation, Isaac, and thanks. To sum up the confusion, I would LOVE to just treat everything like I was typing along, inserting text and paragraph markers, for ever. Except that when you want to do an index entry, you need, not an insertion point but a range, and if you want to do anything with borders or fonts or bullet lists, you have to have a range as well. But the moment you diddle with ranges, it seems like your insertion point ain't where you think you left it. It's the switching between ranges and IPs that is driving me stark raving bonkers.
 

The_Doc_Man

Immoderate Moderator, Former MVP, Retired SysAdmin
Staff member
Local time
Yesterday, 23:34
Joined
Feb 28, 2001
Messages
17,341
I've made some headway. Isaac, I already knew about the method you were using. Thanks for your efforts, though.

The problem is that the Selection property (it is both a property and an object) gets "pissy" about where it is, so I'm getting some success by NOT allowing it to be involved any more than humanly possible. Trying to do insertions using range.InsertAfter works if you use that method you used in your demo file, but you ALWAYS have to know exactly where you are. It got so tedious that I finally wrote a couple of subroutines to help me keep track of where I was and to help me set up for the next insertion.

These turn out to be work-horses for getting this right. I made them so that I can find the last paragraph of the document (because remember I am trying to do this in one linear construction pass). I also sometimes need to know where I am because paragraph-level formatting is (of course) by paragraph number and thus you sometimes need to know the number. And finally, that range reset is something I have to type SO often that it is worth having a subroutine for it.

Code:
Public Function LastPgh(ByRef wddoc As Word.Document, ByRef WdRng As Word.Range) As Long

    LastPgh = wddoc.Paragraphs.Count
    
End Function

Public Function WhichPgh(ByRef wddoc As Word.Document, ByRef WdRng As Word.Range) As Long

    WhichPgh = wddoc.Range(0, WdRng.Paragraphs(1).Range.End).Paragraphs.Count

End Function

Public Sub SetRngPghEnd( _
        ByRef wddoc As Word.Document, _
        ByRef lPgh As Long, _
        ByRef WdRng As Word.Range)

'   move the selection point to the end of the last paragraph in the document
'   move the range to the whole of the last paragraph but not the pgh marker that ends it

    WdRng.SetRange Start:=wddoc.Paragraphs(lPgh).Range.Start, End:=wddoc.Paragraphs(lPgh).Range.End - 1

End Sub
There is more to this tale of woe because right now I'm fighting the issues of bullet lists, page-break insertion, and firing up the index. Plus I need to set the paragraphs and lines to single-space or otherwise reduce the "break" between paragraphs. But spacing is easy if I can just get the rest of the paragraphs right. That's just a paragraph property.

One thing I found that becomes INCREDIBLY important. All of those ByRef markers are needed because you can get an obscure error if you EVER use ByVal anywhere. If you define something in a subroutine (which I do at a couple of points) and pass it back as an intended "side effect" because a function can only return a single value, using a ByVal parameter as one of the intermediate steps in long qualifier string will fail. That is because of course in the sub that ByVal is a COPY of the "real" thing but it gets deleted on Exit Sub and thus the chain gets broken. So references like app.document.section.paragraph suddenly have an object that is Nothing in the middle. Finding those cases where I had omitted the ByRef indication. This same sort of thing happens a lot in Excel app objects, too.

Although my work isn't finished, this specific problem is clearly fixed. Therefore, I will mark it SOLVED but I might come back one more time to show some screen shots of what finally got produced.
 

Isaac

Well-known member
Local time
Yesterday, 21:34
Joined
Mar 14, 2017
Messages
1,513
Very interesting. You inspired me to learn a little Word vba, which I enjoyed, plus reading your discoveries, so thanks for that. ByRef is the default, so I've never specified it (and not really had much occasion to use byVal). Always good to have some useful functions you can reuse. Sounds like a "fun" project!
 

The_Doc_Man

Immoderate Moderator, Former MVP, Retired SysAdmin
Staff member
Local time
Yesterday, 23:34
Joined
Feb 28, 2001
Messages
17,341
I've still got a formatting issue and a "missing" paragraph break but for the first time I have the complete document including the index of names, with no errors of omission and no errors of the order of appearance of the required parts. To get it to work, I ABSOLUTELY NEVER use a Selection object (or property). It looks like for what I'm doing, it is an all-or-nothing-at-all case.

@Micron, you asked if I had looked at the Office documentation online. In fact I have damned near got it bookmarked. However, the problem is that in this case they show you the trees - a LOT of trees - but very rarely show you quite how to re-plant the forest.
 

Isaac

Well-known member
Local time
Yesterday, 21:34
Joined
Mar 14, 2017
Messages
1,513
To get it to work, I ABSOLUTELY NEVER use a Selection object (or property)
Interesting ... I wonder if it follows the same general thing as not using Select/Selection/Active in Excel VBA for its notorious reliability issues.
 

Uncle Gizmo

Nifty Access Guy
Staff member
Local time
Today, 05:34
Joined
Jul 9, 2003
Messages
11,797
Coincidental that you should ask! I was reviewing some of my old Smart Access PDF documents and I found one by Peter Vogel about Creating Spreadsheets Without Excel. I managed to create a simple spreadsheet with XML and opened it with Excel. There was an error or two but it did work. The reason I mention it I understand you can also create Word documents in a similar manner. I wondered if it's something you have explored? More info HERE:-

https://en.wikipedia.org/wiki/Microsoft_Office_XML_formats
 

Isaac

Well-known member
Local time
Yesterday, 21:34
Joined
Mar 14, 2017
Messages
1,513
Excellent post, @Uncle Gizmo I will tuck this one away for future use I think. I love it because when it comes to higher level stuff like .Net apps, or server-based processes like SSIS packages, they would never utilize Excel application automation (or touch the Excel app at all, or even install it on the server), generally speaking........So it's all XML. I've put off learning how to create XML, but (especially using t-sql), eventually I need to embrace it...So useful!! Thanks.
 

The_Doc_Man

Immoderate Moderator, Former MVP, Retired SysAdmin
Staff member
Local time
Yesterday, 23:34
Joined
Feb 28, 2001
Messages
17,341
Isaac, regarding reliability issues with Excel, I've managed to create spreadsheets ab initio via Access VBA and let me say RIGHT UP FRONT, it was a lot easier than Word has been for a similar exercise. But even for Excel, I frequently got the problem regarding subroutines to define something and pass it back, only to have it crash down around my head, neck, and shoulders. Usually some element in a long, multi-step qualifier string had been created improperly and therefore, some element in the middle of the qualifier had become dereferenced by exiting the subroutine. Even though the default is supposedly ByRef, there are times when it seems that might not be the case. For instance, if something gets enclosed in parentheses in the actual call sequence, that supposedly overrides an explicit ByRef.
 

Isaac

Well-known member
Local time
Yesterday, 21:34
Joined
Mar 14, 2017
Messages
1,513
Interesting, well I will keep that in mind!! in my time I have tried my best to create modular, reusable functions and so on and so forth. but I have to admit that I have not taken it so far as to a point where the Call Stack becomes truly high (or "long"). So that's a problem I probably never experienced before. (But probably would have if I had gotten in the habit of creating small reusable functions as much as I ought)
When you said ab initio, the first thing I thought was when I worked for my last company and they used that instead of ssis. But now I googled it and found out it (also) means from the beginning. Learned more new things today!!
 

The_Doc_Man

Immoderate Moderator, Former MVP, Retired SysAdmin
Staff member
Local time
Yesterday, 23:34
Joined
Feb 28, 2001
Messages
17,341
Yep. Ab Initio = Latin - from the start. Or if you cook like my dear Cajun wife, it is the same as "starting from scratch."

Another time you get "caught" in one of those byref/byval definitions is if you actually have an expression. Oh, you can pass the value, but there is no place to pass back a value, so byref has no meaning. And for word, that app.document.paragraph.range.end-1 that is part of your example - is an expression (because of the -1). Yet it is the only way to actually get things aligned correctly.
 

The_Doc_Man

Immoderate Moderator, Former MVP, Retired SysAdmin
Staff member
Local time
Yesterday, 23:34
Joined
Feb 28, 2001
Messages
17,341
I am now reporting total success. I hesitate to post the code because it is rather specialized. But I'll post snippets. Here is one hint. At NO time do I ever use the app.Selection property or the Selection object. I found a way to do this from scratch using only ranges. See also my three routines posted earlier (two functions, one sub) because what I will post here uses them a lot.

First, this puts a date/time stamp in the page header and a centered page number in the page footer. Do this early on, right after creating the app and opening the file with it. The WdHFO is a word.HeaderFooter object.

Code:
'-----------------------------------------------------------------------------
'   set up page numbering in the footer (.Footers collection)
'-----------------------------------------------------------------------------

    Set WdHFO = WdApp.Documents(1).Sections(1).Headers(wdHeaderFooterPrimary)
    WdHFO.Range.InsertBefore "Created " & Format(Date, "dddd") & ", " & Format(Date, "mmmm") & " " _
        & Format(Date, "d") & ", " & Format(Date, "yyyy") & " at " & Format(Time(), "hh:nn AM/PM")
    Set WdHFO = WdApp.Documents(1).Sections(1).Footers(wdHeaderFooterPrimary)
    WdHFO.PageNumbers.Add wdAlignPageNumberCenter, True
This puts a centered header in big, bold print. It finds out which paragraph it is still in, remembers that paragraph number for later, and then starts a new paragraph. But then it steps back to the single paragraph containing the header and formats it. In this code, WdFil is equivalent to WordApp.Documents(1), the first - and only - file open for this application. WdRng is a variable used to remember range information.

Code:
    lNPgh = WhichPgh(WdFil, WdRng)              'what paragraph are we in?
    lPPgh = lNPgh                               'remember paragraph number
   
    SetRngPghEnd WdFil, lNPgh, WdRng            'adjust range to this paragraph
    WdRng.InsertAfter "Family Directory"        'bigger (constant) header
    WdRng.InsertParagraphAfter                  'toss in a paragraph marker
   
    With WdFil.Paragraphs(lPPgh).Range.Font
        .Name = "Lucida Calligraphy"            'make it pretty
        .Size = 24
        .Bold = True
    End With
    WdFil.Paragraphs(lPPgh).Alignment = wdAlignParagraphCenter 'center text
This creates an expository lump with a paragraph break. You have to be consistent when doing this because if you tell it to do an .InsertBefore, you run the risk of writing your sentences in the wrong order. Note that the sequences of several .InsertAfter actions will tack on each contribution in the right order, and because there is no paragraph marker involved, Word forms a properly justified paragraph.

Code:
    WdRng.InsertParagraphAfter
    WdRng.InsertParagraphAfter
   
    lNPgh = LastPgh(WdFil, WdRng)               'find our place in the world
    lPPgh = lNPgh
    lPghS = lNPgh
    SetRngPghEnd WdFil, lNPgh, WdRng
   
    WdRng.InsertAfter "The contents of this document were downloaded from the Ancestry.COM web site "
    WdRng.InsertAfter "and were analyzed by XXXXXXXXXXX using Microsoft Office tools "
    WdRng.InsertAfter "including Access, Excel, and Word.  "
    WdRng.InsertAfter "Because most of the information was obtained using an Ancestry.COM license restricted to USA records, "
    WdRng.InsertAfter "the data is very limited regarding people who were born and died overseas."
   
    WdRng.InsertParagraphAfter
    lNPgh = LastPgh(WdFil, WdRng)
    SetRngPghEnd WdFil, lNPgh, WdRng
   
    WdRng.InsertAfter "The accuracy of each entry depends on the reliability of the "
    WdRng.InsertAfter "sources used by Ancestry.COM to provide that information "
    WdRng.InsertAfter "and must be considered as highly variable in quality.  "
    WdRng.InsertAfter "Older data usually depends on handwritten records from old church records, journals, or diaries and thus "
    WdRng.InsertAfter "is subject to both aging of the historical record itself and "
    WdRng.InsertAfter "the legibility of the handwriting of the person writing the entries.  "
    WdRng.InsertAfter "United States Census data was not printed based on electronic records until 1950.  As a result, anything before "
    WdRng.InsertAfter "World War II is of poorer quality and accuracy.  "
I didn't show the code that made the index entries, but this is how you put a single-colum index of names in place with right-justified page numbers and a dot-leader from the name to the number in the index.

Code:
    WdRng.InsertParagraphAfter                  'add a new marker
    lNPgh = LastPgh(WdFil, WdRng)
    lPPgh = lNPgh
    SetRngPghEnd WdFil, lNPgh, WdRng            'place for index title
   
    WdRng.InsertAfter "Index of Names"
    WdRng.InsertParagraphAfter
   
    With WdFil.Paragraphs(lPPgh).Range.Font     'applies to the page title
        .Name = "Lucida Calligraphy"            'make it pretty
        .Size = 24
        .Bold = True
    End With
    WdFil.Paragraphs(lPPgh).Alignment = wdAlignParagraphCenter 'center text
   
'   prepare for the index

    lNPgh = LastPgh(WdFil, WdRng)
    lPPgh = lNPgh
    lPghS = lNPgh
    SetRngPghEnd WdFil, lNPgh, WdRng
       
    lNPgh = LastPgh(WdFil, WdRng)
    SetRngPghEnd WdFil, lNPgh, WdRng
   
    With WdFil.Paragraphs(lPPgh)
        .LineSpacing = 8
        With .Range.Font
            .Name = "Times New Roman"
            .Size = 10
            .Bold = True
        End With
    End With
   
    WdFil.Indexes.Add Range:=WdRng, Type:=wdIndexRunin
   
    With WdFil.Indexes(1)
        .RightAlignPageNumbers = True
        .TabLeader = wdTabLeaderDots
    End With
       
'   put up one last paragraph to clean things up a bit

    WdRng.InsertParagraphAfter
    lNPgh = LastPgh(WdFil, WdRng)
    lPghE = lNPgh
    SetRngPghEnd WdFil, lNPgh, WdRng
   
    WdRng.SetRange _
        Start:=WdFil.Paragraphs(lPghS).Range.Start, _
        End:=WdFil.Paragraphs(lPghE).Range.End      'not 1st, have to assert bullets
    WdRng.Paragraphs.LineSpacing = 10               'minimize spacing
    With WdRng.Font
        .Name = "Times New Roman"
        .Size = 10
        .Bold = False
    End With
The key to all of it is to ALWAYS ALWAYS ALWAYS meticulously track where you are in the document by knowing the paragraph number and USING it for your formatting. From what I could tell, it would also be possible to do this based on individual word insertion but (a) I had line-oriented data and (b) doing it one word at a time would be incredibly tedious even for a purist like me.
 

The_Doc_Man

Immoderate Moderator, Former MVP, Retired SysAdmin
Staff member
Local time
Yesterday, 23:34
Joined
Feb 28, 2001
Messages
17,341
i played with Excel a lot to get the format I am using now. It looks like the site is using SmartArt for some of those diagrams, and I have found that to be incredibly difficult to manage from VBA. I found nothing at all smart about it. Some of the comments on the site report problems, and what I have is stable, so I'm going to leave this alone.

But thanks for thinking of me, Uncle G!
 

Uncle Gizmo

Nifty Access Guy
Staff member
Local time
Today, 05:34
Joined
Jul 9, 2003
Messages
11,797
But thanks for thinking of me, Uncle G!
You're Welcome!
I was pretty sure that you were up together with this project Richard. When I happen upon something on the internet that looks interesting, or related to a thread I've been following on AWF, I like to add the new information in the hope of making the thread more useful. Create more of a compendium of information on the subject.
 

Users Who Are Viewing This Thread (Users: 0, Guests: 1)

Top Bottom