We reimplement a previous macro to select all or part of the current heading content. The resulting utility functions easily apply to several other useful editing tasks.
Thanks for your interest
This content is part of a paid plan.
Select all or part of heading content
Much like Word gives use tools to manipulate words and paragraphs, it is also convenient to quickly manipulate heading content. Word seems to fall short on tools to manipulate some document elements, but we can make our own with VBA. This article also sets up a better framework when implementing editing macros rather than just creating a host of stand-alone, sometimes near carbon-copy variations.
Why worry about specialized heading macros?
I often sketch out my new novels before writing (which is a story for another day), but I do generate a detailed working outline as I write. I find it helps me keep track of the story as it grows. My working outline styles include Acts, Chapters, Scenes, and Subscenes. Each of these styles is based on a standard Word heading level 1 through 4, respectively, so it shows up in the Navigation pane. With that in mind, it’s convenient to be able to quickly select all or a portion of a chapter or scene to delete or move it elsewhere.
What are headings in Word?
In Word, headings are best described as a document range, but they’re a little more than that. In this article, I’m using the word “heading” to refer to the heading paragraph and the heading content. The “heading paragraph” is the top paragraph with the heading style, and the “heading content” is all content below the heading paragraph possibly including any subheadings.
Range reminder
A range in VBA is a connected region of spanned document content. As with any Word object, range variables have their own data (properties) and actions (methods) we can reference when manipulating them. In particular, ranges have Start and End positions (as counted in characters from the beginning of the document) marking the spanned content. We'll use these positions repeatedly in this article.
Headings as ranges
Headings exist in Microsoft Word somewhere between a general document range (spanned content like a word or sentence) and a standard Word document element (such as a Paragraph or Section). Of course, a heading paragraph generally precedes one or more content paragraphs. Roughly speaking, headings are larger than a paragraph but smaller than a section.
Why does it matter?
It affects how we work with headings in VBA.
Paragraphs and Sections are actual “Objects” in Word VBA. In effect, they are their own data type with their own set of properties and methods specific to what they can store or do. For example, a range variable has Collections of contained Paragraphs and Sections we can reference.
Words and sentences are more conceptual things in a document. Collections of them also exist as part of a range variable, and we can access them directly through the Sentences or Words properties, respectively. However, they exist only as collections of ranges.
Headings are more like the latter but without a range related collection to access them. It gets a little more complicated when we further look at how to select or navigate between them. For example, a range Move method cannot jump between headings automatically, but the Range GoTo method can navigate between standard Word headings something akin to a regular Word object or document element unit (see below for more details).
It’s a mixed bag for how we can work with them in regular VBA, so having a few of our own tools would be helpful.
Standard versus derived heading styles
For headings, the various range GoTo navigation methods are restricted to the standard Word heading styles, but I regularly use derived heading styles in my novels and note documents. Thus, I need my heading macros to also work for derived heading levels. Another reason to bake my own implementations.
Range GoTo and Move methods restrictions
We cannot use Range Move and related methods to navigate between headings. The Move commands use move units defined in the WdUnits “enumeration.” Enumerations in VBA (and other languages) are essentially a list of predefined constant values with more readable names. Word, sentence, and paragraph units exist for the Move methods, but no heading related constant exists.
Similarly, the Range Expand method skips heading ranges and expands from a paragraph up to a whole section.
On the other hand, we can use the Range GoTo and similar methods to navigate between standard Word headings since there is a wdGoToHeading constant in the WdGoToItem enumeration. However, Range GoTo methods do not work with derived heading styles which I consider an essential feature.
Creating our own heading tools
As mentioned earlier, I like to create a working outline as a write a novel, and I use extensive organization in my novel note documents. It’s useful to create tools to manipulate headings and their associated content. Our job of automating our editing tasks for headings will be easier if we first create several functions as working tools which we can then easily use to help with various editing tasks.
Word macros and keyboard shortcuts
If we intend to assign keyboard shortcuts or custom buttons to a macro in Word, it cannot have any parameters or be a function. On the other hand, voice command scripts in Dragon Professional do not have this same limitation, but it is a separate paid application and a topic for another day.
A conundrum …
I’m torn on this set of functions and the resulting macros. They belong in a single article since the differences are too small to separate them. I’m also making a point about being smarter regarding how we organize and implement our macros using functions in our editing toolbox.
In this article, I like the simplicity of creating and using a single function to get the necessary heading content ranges, but that one function would be more complicated than it probably should be for a VBA tutorial. Unfortunately, using three different functions is also messy in its own way since there are more of them to track.
Hmmm … decisions, decisions.
Function skeletons
In the interest of maximum clarity [cough], we will create three related functions, one each for the start, end, or the full heading content, respectively. Stick with me since the result is nice, and you’ll be able to manipulate heading content faster. Once we’re done with the functions, the editing macros are almost trivial to create.
After the three working functions are created, we’ll specialize them to several specific macro tasks. Moreover, the functions can potentially be used later in other macros. The function skeletons are:
What parameters?
We could assume each function uses the starting Selection or insertion point already inside a heading, but when I’m writing a function, I prefer it to be general purpose. With this in mind, each function takes an intended “target” range variable called rTarget which is presumably positioned somewhere inside a heading.
Return type
Each function returns a range variable corresponding to the identified heading content. Depending on the function, the range is either the complete heading content or the heading content toward the beginning or end relative to the user’s starting position.
Remember we assign a return value in VBA by setting respective function name equal to the identified range.
Get the full heading content range
Let’s start with the first function GetHeadingContentRange and get the entire heading content range. This function mimics a previous macro we created, but today’s function returns the range rather than directly selecting the heading content.
We still need to overcome the difference between derived and standard heading paragraph styles. If you only use standard heading styles, we’ll include a separate version in the extended content. That function is a little cleaner, but I use derived heading styles extensively, so it’s a no go for me.
Difference between the Selection and a selection
When I use the capitalized “Selection” I’m referring to the Word object corresponding to the content spanned by the user’s selection. Only one Selection exists per document. The Selection is like a VBA range, but it contains extra properties and methods specific to manipulating it in the active document.
A lower case “selection” refers to any selected content in the document. A selection is often the currently spanned document content, but it could also be just an insertion point with no spanned text (but there are some VBA subtleties that we won’t get into today).
We will not consider non-contiguous or block selections. VBA gives us almost no tools to deal with them, and our current application has no use for them.
Manipulating the Selection
In general, it’s frowned upon to repeatedly manipulate the Selection in a macro or function because the changes are immediately visible on the screen, but in this case, it’s necessary if we want to work with derived heading styles.
We generally only want a macro to display the final result, but we cannot access the standard predefined Word bookmark named \HeadingLevel without the active document Selection being inside the target heading. Fortunately, the resulting macros are probably fast enough that we won’t notice it much in practice.
What if the target range doesn’t exist?
I don’t like detours, but we need one here before we get started.
We should verify the target range is assigned to something in the document. The simplest validation is to ensure the rTarget variable is not Nothing before continuing with the rest of the function. We’ve done this a few times in previous articles, so we’ll be more concise here.
Check for Nothing
A range (or any object) that is not yet assigned to something in the document has a literal value of “Nothing.” We check whether two objects are the same thing using the “Is” operator, so we can verify whether our target range Is Nothing using:
Putting this into a conditional statement, we have:
Exit the function if so
If the target range is not assigned to anything in the document yet, we need to return a Nothing value for the function to let the user know there was a problem.
Then we just exit the function.
Putting it together, we get:
Now we can continue with the rest of the function with more confidence that we have a valid target range.
Store starting range
Before we modify the Selection, we need to store the initial Selection range, so we can restore it at the end of the function. We first create a range variable named rInitial.
We need to Set the range because it is not a plain value like a number or plain text. I prefer to precede any range variables with an “r” just to be clear about what the variable represents in the document.
We set the rInitial range to Selection.Range because the Selection contains a specific range corresponding to the user’s spanned document content. The Selection is not just a range, but its Range is one of its more important properties.
Select target range (a necessary ick)
Since the function takes a target range parameter, presumably within the intended heading content, we need to select it. Normally this would be a no-no since any changes to the Selection are immediately displayed on the screen, but—
Arghhh … let’s just do it [pinches nose].
Get the whole heading
We define a working range named r just to keep the name short. This is the range we will manipulate for most of the function.
We access the pre-defined bookmark and store the bookmark range in our working range r.
This bookmark is automatically updated by Word based on the heading content around the current Selection. It works for standard Heading 1 through 9 styles as well as any derived styles. It’s a bit of a mystery why this bookmark is automatically updated by Word in real time, but we few other mechanisms in VBA allow us to directly manipulate derived heading styles.
Bookmarks are like document ranges, but they contain other information as well, so we need to reference its Range property in order to set it equal to our working range r.
Incidentally, we could instead use the ActiveDocument object rather than the Selection to access the \HeadingLevel bookmark, but there doesn’t seem to be any advantage of one approach over the other.
Exclude the first paragraph for heading content
Now we know the working range r spans the entire heading. This function returns just the heading content, so we need to exclude the heading paragraph. The first paragraph in the range is the heading paragraph, so the cleanest way to remove it is to reset the Start position of the working range r to the end of the first paragraph.
We can assign the positions directly because Start and End positions are just numbers corresponding to the number of characters from the beginning of the document.
There is a logical question here on how the End position of the first paragraph coincides with the Start position of the second paragraph (the beginning of the heading content), but that is how Word marks them. The first paragraph range extends through the paragraph mark at its logical end. Another way to think about it is the two locations exist at the same character position of the document which makes a little more sense.
Alternative way to exclude the first paragraph (do not use)
If the above just tweaks your brain enough that you’re looking at the monitor sideways, another way to accomplish the same thing is to use the Start position of the second paragraph directly.
Technically, we’re accessing the second index position of the Paragraphs collection (Collections in VBA begin counting at 1). We’re asking VBA to give us the second paragraph contained (even partially) in the heading range. This is more direct and makes more logical sense if you’re not used to Word VBA, but it’s mostly for your reading enrichment.
Huh?
Just keep reading if you’re about to insist.
Please don’t use it because it will cause an error when there is only one paragraph in the Paragraphs collection, meaning the heading only consists of a heading paragraph.
Why would someone have a heading with no content?
That’s a valid point, but it’s asking for trouble to implement a step that will crash the function for a reasonable special case even if it’s an odd one in practice.
I also find this a bit unsightly compared to the former version. One of the things I like about VBA is how it reads almost like English a lot of the time. For most editing tasks enhanced by VBA, it is uncommon to need to access the paragraph collection using the parenthetical index.
Set return value
Our working range now corresponds to the heading content, so set the function’s return value to our working range r using the function name.
Manipulating the Selection directly inside a function is generally frowned upon, but we had no choice if we wanted it to work with derived heading styles via Word’s predefined \HeadingLevel bookmark.
Final heading content range function
Collecting the steps together, our final function to get the entirety of the heading content is:
I don’t like how it selects the target range and then restores the original user selection. The back and forth is unseemly, but we have no choice.
Full heading version
We can simply leave out the line excluding the heading paragraph and have a version to return the full heading range. This will be a little more useful for some macros, so we'll go ahead and include it.
Heading content distinction
For completeness, the \HeadingLevel bookmark will also contain all content for any subheadings. This behavior contrasts with the Range GoTo methods which will navigate to the immediate next heading of any level. Fortunately, the current function is logically consistent with how many people would expect it to work but be careful if you use the Range GoTo methods (see standard headings version below).
Example function usage
We can call the function from any other macro. For our specific macros today, we want the range around the current Selection. We need to give (called “pass”) the function a target range that corresponds to the current Selection’s range.
For this isolated example, let’s store the result in another range variable rHeading.
In practice, I would probably have the calling macro disable and enable screen updates just to prevent any possible screen flicker from directly manipulating the Selection (see previous article).
Standard heading range version
If you prefer to work only with Word’s standard heading styles, this version is simpler. Without any explanation, the function is:
This one is nicer since we don’t need to select the target range, but unlike the previous function, this version only returns the body text immediately underneath the current heading paragraph. The resulting range will not include any subheading content. As advertised, it also does not work with any derived heading styles.
Get range to the start of the heading content
As a reminder, our function skeleton to get the heading content from the current document position back to the start of the heading is:
This may seem like a trivial variation, and it is overall, but it requires a tweak that is trickier than it may seem at first. We just have to get the details right.
Get the heading content range
The previous function already gets the heading content range, so we can start by just calling it in this new function.
Here we’re reusing the working range r, but the r inside that function is different than this r (the “scopes” are different, so VBA treats them as separate range variables meaning we can use the nice short name in both functions).
Validate working range
We should validate the result of the GetHeadingContentRange function which is stored in the working range r. The line of reasoning is the same as with the previous function, so the steps are:
We do not need to validate the target range also because that is checked in the GetHeadingContentRange function.
Restricting the heading content toward the start
We want to restrict our heading content range to any content before the current selection or insertion point position in the document. How do we do this?
We want the heading content toward the start, so we exclude any heading content that falls after the target range. We already have the full heading content range, so the simplest approach is to set the End position of the heading content range r to the End position of the target range. They will then both end at the same document position.
Remember End is a property of a range variable that is a literal position value based on the number of characters as counted from the beginning of the document, so we can assign it directly just like any other normal number.
Heading paragraph issue
We have a small problem if the initial target range is inside the heading paragraph. We’ve excluded the heading paragraph from the heading content range and stored the result in the working range r, but what if the End position of the target range is inside the heading paragraph?
The above assignment would move our working range backward into the heading paragraph.
Ughhh.
Remove the end of the heading content range
How do we check the position of the target range?
Remember, currently the Start position of the working range r is at the beginning of the heading content. We literally compare whether the End position of the target range is after the Start position of our working range. Which is bigger (later in the document)?
If this condition is True, we know the End position of the target range is somewhere inside the heading content (it falls after the heading paragraph). We can then adjust the End position of the working range r as we did above, but now the assignment is conditional.
Target range inside heading paragraph
What if the target range is inside the heading paragraph?
If the target range is inside the heading paragraph, then no heading content exists after the End of the target range, so we should return an empty working range. The easiest solution is to just collapse the working range r.
The default direction to collapse the range is toward the Start position. Collapsing the working range is consistent with r spanning no heading content but being positioned at its beginning. The range r is now empty as opposed to being set to Nothing. An empty range is one that has no spanned text where the Start and End positions are the same, but there are subtleties we will not address here.
Putting it together, we have:
Final get range function to the start of the heading content
The final function to select any heading content up to the current insertion point position is:
Get range to the end of the heading content
Our next function is the reverse of the previous. Here we want the heading content range from the user’s current position inside the heading toward the end of the heading content. That is, we exclude the beginning part of the heading content before the target range. The function skeleton is:
Get the heading content range
The initial part of this function is the same as the previous one which we repeat for completeness. We initially just get the heading content range around the target range using the earlier function.
We’re reusing the working range r since VBA considers it a different variable in this function.
Validate working range
We again validate the result of the GetHeadingContentRange function which we stored in the working range r.
We do not need to validate the target range also because that is checked in the GetHeadingContentRange function.
Restricting the heading content toward the end
Our working range r is currently the full heading content range. We want to restrict it to the content after the target range.
How do we do this?
The simplest approach is to set the Start of the working range r to the Start of the target range. That is, we’re removing the beginning portion of the heading content based on the location of the target range’s Start position.
Remember Start is a property of a range variable. Since it is just a position value, we can assign it directly like a regular number.
Heading paragraph issue
Again, we have a small problem if the initial target range is inside the heading paragraph. We only want to restrict the working range if the Start position of the target range is somewhere in the heading content. Otherwise, we leave the result as the entire heading content.
How do we check that condition?
We literally compare the Start positions of the working and target ranges. If the target range’s Start position is after the working range’s Start position, then it is somewhere inside the heading content, and we can restrict the function’s range result as above.
If this condition is True, then move the heading content Start position to the beginning of the target range. The conditional statement is:
Since the command is short, I prefer the more compact notation.
Otherwise, we do not change the working range leaving it to span the entire heading content.
Final get range function to the end of the heading content
The final function to select any heading content after the target range (toward the end) is:
Gotchas
What could possibly go wrong? Surely nothing—
Well … as with many “simple” macros, sometimes hidden traps are lurking just around the corner. It’s VBAs version of a horror movie (not that I watch them). We’re just waiting for the next jump scare.
We’re relatively safe in the first set of macros below since we’re just selecting content, but we should still be careful since part of the point of these functions is we can readily apply them to other macros that delete or otherwise manipulate text.
That’s when we might hear authors scream.
Delete my novel text? Ahhhh!
Initial Selection spans a heading
If the initial Selection spans more than one heading at the same outline level, the macro will not properly interpret the current heading content range. In this application, we could just collapse the initial target range which solves such issues in many macros, but I prefer not to sidestep the issue in these functions because our range adjustments are related to the Start and End positions of the target range. Collapsing the ranges makes those values the same.
I might compensate for this in my personal macros because I’m a perfectionist, but I don’t see it worth the extra macro steps for public content.
Does the bookmark exist?
Technically, we should validate whether the \HeadingLevel bookmark exists, but I have not had issues in my testing even in a document without any headings. In general though, bookmarks should be validated in macros (see for example this previous article on quick editing bookmarks) because it will cause an error if they don’t exist.
Validation checks … oh, my!
One might balk at the multiple validation checks (one in each function).
I understand the notion (I dislike inefficiency), but each function should stand on its own within reason since you might use it somewhere else on another day. If you create the function to be self-contained by always returning valid and reasonable results, you’re less likely to encounter problems later. Making too many unnecessary assumptions will get you in trouble.
Skipping validation checks will get you decent macros and functions 99.2431% of the time, but you’ll probably regret it someday since it will cost you hours trying to figure out what is wrong when something else happens that you didn’t expect. If you really wanted to be safe, while you’re creating the macros, you could even include some temporary error messages.
Just do the validations and avoid the annoying macro errors. You’ll thank me someday.
Applied Macros
Now let’s apply these functions to specific editing tools for our VBA toolbox. For the obvious cases, we have four practical macros for each function above:
Select the heading content
This function rephrases a simpler macro from a previous article just using the new function above. To get the current heading content range, we pass the initial Selection’s Range property to the GetHeadingContentRange function from earlier.
The old version of the macro wasn’t complicated, but this one is nice and simple in a different way.
Other macros
We can now trivially modify the SelectHeadingContent macro to work with Cut, Copy, and Delete. Literally just change the macro name and description text and then use the working range’s Cut, Copy, or Delete methods rather than Select.
And finally Delete. This one avoids using the clipboard which is nice at times.
And voilà, we have four tools instead of one all because we created a working function.
What do the functions do for us?
Using functions makes these macros easier to understand. I suppose we could have copied the bulk of the function steps over to each macro, but that would quickly become a mess and make any changes much more difficult. If we make any changes to the range function(s), these macros inherit the improvements automatically.
The situation is not a nice if you're using VBA in Dragon Professional scripting, each script is essentially independent, so we would need to copy functions over to each script (unless we load an external file every time) which is definitely not ideal.
No range result validation?
We could validate the range results from the range functions again. We’ve done this several times, but we're passing the Selection's Range property to our range function. If the Selection is invalid in the active document, Word has more serious problems than a macro validation will fix.
If you do prefer to include another validation check, it would look like:
That is, we only Select (or Copy, Cut, or Delete) the working range if we have a valid range result from the function., but these macros omit this validation for clarity.
Caution on heading macros and deleting content
In most cases, I like having all the macro variations available. I will not necessarily assign every variation to a keyboard shortcut, but I will probably include most of them in my Dragon Professional voice command scripts (a separate paid application).
With that said, I am cautious about allowing myself to delete whole headings. So much so that I have not implemented the Cut and Delete versions of the above macros to protect myself against an unfortunate mistake. I prefer to see that much novel content selected before I remove it, but I still thought it was useful to include them above since I have created similar groups of macros for other document elements (paragraphs, sentences, etc.) based on a common range function.
What about the Start and End variations?
In the interest of not making this article even longer than it already is, I’ll leave constructing most the other macro variations to the reader, but I would like to show a sample and tweak the Delete and Cut versions to show what we can do.
Select to the start or end of the heading content
We can adjust the earlier macros to apply to the Start or End of the heading content.
Most other variations require simple changes and are left to the reader.
Capitalize first word
For the Cut and Delete macros with the “To Start” variations, we’ve removed some text from the beginning of the heading content. It is convenient if the macro automatically capitalizes the new first word of a heading content paragraph (suppose we deleted text up to somewhere in the middle of a paragraph).
Any range variable has a Case property which we can reference as r.Case. Using a table of standard word case constants, we can capitalize the new first word.
Case is a numerical property (which is a little odd to me, but that is how Word VBA does it), so we just literally assign it with an equals = sign.
After the Delete or Cut, the working range r is empty (doesn’t span any text logically speaking), but we can still change the case of the word immediately to its right. There is also a wdTitleSentence constant, but the distinction only matters if the range spans multiple words. For this case (no pun intended), they do the same thing.
If you’ve included the above validation check for the working range r, change the case of the word inside the same if statement. The macros below omit this validation.
Fortunately, the capitalization does nothing if the new first word is already capitalized.
Delete and Cut with Start variations
Here are macros that Cut or Delete heading content at the beginning of the heading.
Again, the other variations require simple changes and are left to the reader, but we don’t need to worry about capitalization adjustments for the “To End” variations nor any Copy or Select variations.