Solved What is the correct way to linearly insert text into Word?

The_Doc_Man

Immoderate Moderator
Staff member
Local time
Today, 17:29
Joined
Feb 28, 2001
Messages
30,557
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?
 
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.
 
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

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.
 
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.
 
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!
 
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.
 
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.
 
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.
 
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.
 
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!!
 
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.
 
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.
 
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!
 
Whenever I try to automate Word (Excel is less bad), I feel like I'm playing pin-the-tail on the donkey and I'm the donkey.

Yes, Pat, but you are one of our favorite donkeys. 😁

And I smiled when I said that, pardner! (I'll leave the literary reference dangling.)
 
I'm with Doc. Whenever I try to automate Word (Excel is less bad), I feel like I'm playing pin-the-tail on the donkey and I'm the donkey.

I did manage to create a Word document from COBOL by using the .rtf format. Since this method is plain text and is done with begin/end tags, once i found the RTF spec on line and got a grip on the tags I needed, it was fairly straight forward. My task was to create a report that I could email as a Word attachment so the formatting wasn't too crazy. When I couldn't figure out the tags, I created a Word doc in .rtf format and did the formatting there. Then I opened the .rtf using NotePad to see the tags.

Wiki has a good overview

Here's the final spec for version 1.9.1
file:///C:/Data/UsefulDatabases/WordRTFDocumentation/[MSFT-RTF].pdf

Word is interesting and seems very dodgy to me, but I can't be certain that's attributed to my lack of skill or that it really is pretty dodgy.
Excel can be automated to absolute and total precision.

I spent the first few years of my tech life "doing everything" using Excel vba and gosh darn it, by the time I learned other tools I had indeed learned how to smash everything into the shape of a nail if need be. Torturing metaphors there, sorry
 
Powerpoint automation is equally flakey.

Long routines that run fine 95% of the time will error out, normally with an "Object not defined" or when trying to activate chart data worksheets "Object not available" or some other similarly vague message.

Even more frustratingly, if you click continue, it carries on running completes the action it said it couldn't and works until the next random error. Go figure.

It also slows down significantly if you have had things open for a while. One routine that updates 8 different sets of slides, if you reboot before setting it off takes approx 4 minutes per set. The last time I ran it this week (forgetting to reboot), it took 1 hour 20 minutes. 🤦‍♂️
 
I did manage to create a Word document from COBOL by using the .rtf format.
since the days of FoxPro DOS, I have been doing the bulk of reports in HTML format (in the main 20-40 tags) from all ACCESS, EXCEL,....
- code visibility
- convenient to view in the browser
- opens for revision in EXCEL if necessary
- convenient to print in WORD - provides automatic selection of column widths and row heights
 
After 3 years, I have an update, a subtle "gotcha" that reached up and bit me.

As part of my genealogy database, which I update occasionally, I generate a Word-based document (which I then save as a PDF). This document gathers data regarding people who were named in the section of code that builds a Family Tree diagram. If they are named in any of several tree-like files, they are also named in the list that shows everything I know about the person. The details that I know are included in a bulleted list after the person's full name. The genealogy project started sometime in the mid 2010s and was written using Access 2010. (No surprise there.) I got it to work and got "pretty" word documents. I would say "...with no problems" but you know it would be a lie.

Time passed and my gaming beast - which is also my development beast - died a hard death in late 2023 - after the previous post in this thread. When I replaced the beast with a newer version that included Win 11, I bit the bullet and upgraded to Office 2021. And I recently did other work on the family tree so I ran newer, updated copies with data on a few more people here and there. And the reports were ugly again. The worst part was that the left margin crept to the right with each new person. Pretty soon it became an unreadable mess.

I verified it wasn't an input problem because the data being inserted had no leading tabs or spaces. It finally resolved to a simple problem: The bulleted lists automatically indented one level (to the right), but ending the bullet list didn't revert to the indent level that had been in force before the start of the list.

This is represents an apparent change in behavior because between Office 2010 and Office 2021, VBA-driven indenting behavior changed. I can't tell you details of WHEN or WHY it changed, but in 2010, the indent for a bulleted list either did an implicit outdent OR the indent was part of the bulleted list, and when you ended that list, your next paragraph indented normally. I don't know which, but either would have the observed effect. In 2021, the indent for the bulleted list was apparently the same - but when you ended the list, the indent DIDN'T revert towards the left.

To keep this description from being a major epic, the solution is that now, you have to consider that the left-margin indent is kept separately from whatever was done previously for bulleted lists. Probably with changes to the "style" feature, the new way this is handled is that the indent level for bullet lists is just to update the "automatic" indent level. As to WHEN this occurred, the only two time references are Office 2010 (prior behavior) and Office 2021 (new behavior.) I didn't have intermediate versions of Office on my home system and my genealogy project was never on any of my Navy PCs that DID have the intermediate versions.

Here's what fixed it for me: First, somewhere in the start of creating your Word document, you need to define a range that includes some printed paragraphs for which no special formatting has yet occurred. Specifically, it must be before the first bulleted list. This gives you visibility of the document's default settings.

Code:
    WdRng.SetRange _
        Start:=WdFil.Paragraphs(lPghS).Range.Start, _
        End:=WdFil.Paragraphs(lPghE).Range.End  'not 1st, may have to assert bullets
       
    WdRng.Paragraphs.LineSpacing = 12           'spacing matches print size
    lIndent = WdRng.Paragraphs.LeftIndent       'copy the indentation level from here
    With WdRng.Font
        .Name = "Times New Roman"
        .Size = 12
        .Bold = vbFalse
    End With

The capture of the .LeftIndent occurs before any bullet lists are declared in this code. WdRng is a range object that contains one or more paragraphs. Some time later in the code, there is loop for each person being documented, and the format will be a person name followed by a bulleted list of what I know about that person. In Office 2010, this next step was NOT required, but for Office 2021, it is. This snippet emphatically resets the range based on a previously recorded starting and ending paragraph number (lPghS and lPghE).

Code:
            If lPrs <> 0 Then                   'was this the first entry?
           
                WdRng.SetRange _
                    Start:=WdFil.Paragraphs(lPghS).Range.Start, _
                    End:=WdFil.Paragraphs(lPghE).Range.End      'not 1st, have to assert bullets
                WdRng.ListFormat.ApplyBulletDefault     'this is a bulleted list, simplest bullets
                WdRng.Paragraphs.LineSpacing = 10       'minimize spacing
                WdRng.Paragraphs.LeftIndent = lIndent   'control left-indent because of bullet side-effects
                WdRng.Paragraphs.FirstLineIndent = lIndent  'and bring 1st-line indent into this alignment
               
            End If

As you can guess from this isolated snippet, when I have a new valid "person pointer" (called lPrs), I just reinstate the initial indent level (which, by the way, is a number in points, the printer's measure of 72 points/inch.) This next is a sample of the document formatted via this code.

Sarah Aylet
  • Father - Unknown
  • Mother - Unknown
  • Daughter - Elizabeth Aylet born 05 Jul 1652
  • date: abt 1693 ; place (*): Great Gaddesden, Hertfordshire, England ; date: 05 Mar 1635
I picked a SMALL sample snippet because some of my ancestors appeared to believe firmly in that Biblical admonition "Go forth and multiply." And another fact that keeps it as a small sample is that it is from so far back that I don't have a lot of details. I've got a couple of things to tweak yet, because after other format changes, somehow I cut off details regarding the death and birth dates' labels.
 
Last edited:

Users who are viewing this thread

Back
Top Bottom