Font of all headings in a Word Document - vba

I am trying to retrieve the font name and size of all the headings from a word document. Any idea how to get it?

The basic structure will be something like below:
Public Sub ShowFontAndSize()
Dim singleLine As Paragraph
Dim lineText As String
For Each singleLine In ActiveDocument.Paragraphs
Debug.Print singleLine.Range.Font.Name
Debug.Print singleLine.Range.Font.Size
Next singleLine
End Sub
The catch will be that this won't sense if there are different fonts and sizes on the same line. If that's a possibility, you will need to add another loop with For Each singleCharacter In singleLine.Range.Characters inside of the paragraphs loop.
Edit: A trickier problem is what to do with this data once you've collected it. Building up an array seems like the natural fit, but VBA arrays are borderline useless, since basic methods like .append() require you to redim the whole array. See http://www.cpearson.com/excel/VBAArrays.htm for more info if you would like to go down that road.

The most straight forward solution is to open the document in Word and access the object model. This is traditionally done using VBA, but you can also use .NET (e.g. C# og VB.NET) by using VSTO (Visual Studio Tools for Office). Personally I find C#/VB.NET much better languages than VBA.
Once you have access to the object model you will have to enumerate paragraphs in the document. When you find a heading (perhaps defined by the style) you will have to figure out the formatting of the heading.

This is what I got from a brief skim of the MSDN page on "HeadingStyles":
MsgBox ActiveDocument.HeadingStyles(1).Style

Related

VBA Macro no longer works in office 365

I have a word macro working fine in Office2013.
The macro reduces the height of the last page in a document to remove trailing blank space. Later I combine serveral of these documents (converted to XPS) to one doc - without blank areas due to empty spaces on the last pages.
Sub SetCurPageHeight(nHeight, nPage)
nHeight = PointsToCentimeters(nHeight)
nHeight = nHeight + 0.5
Dim strHeight As String
strHeight = CStr(nHeight)
strHeight = Replace(strHeight, ",", ".")
WordBasic.PageSetupPaper PaperSize:=9, TopMargin:="0", _
PageHeight:=strHeight, Orientation:=1, FirstPage:=0, _
OtherPages:=0, _
ApplyPropsTo:=1, FacingPages:=0, _
SectionStart:=nPage, _
SectionType:=1
End Sub
With Office365 the macro no longer works as expected. When I use it ALL pages are set to the given height - not only the last one.
I can't even find any documentation for "PageSetupPaper". And also no replacement for this functionality.
I thinmk there was a "page setup dialog" which had a "only this page" option - from that I recoreded the macro and adapted it for my needs.
Is there any documentation for PageSetupPaper - or has anyone a hint how to solve the problem with other code Office365 VBA.
You need to re-write the macro in a pure VBA without using WordBasic in the code. Read more about that in the Converting WordBasic Macros to Visual Basic article.
Also you may consider using the macro recorder to find the required properties and methods to get the job while you are doing the steps manually and recording them.
Perhaps the simplest approach if using Word's standard justification method is to use something like:
Sub Document_Minimise()
ActiveDocument.Compatibility(wdWPJustification) = True
End Sub
This applies WordPerfect justification to justified paragraphs in the document, compressing the text more than Word's own justification method. That alone may be sufficient to reduce the page count.

Saving Custom Document Properties in a Loop

I'm trying to save the values of data that have been input into my form. There are a total of about 50 different fields to save across 5 different agents, so I loaded the data into arrays.
I've tried saving the fields in a loop, but it doesn't seem to work in a loop, only if each field has a separate line, which is a lot of code and messy. The Ag1Name, Ag2Name and Ag3Name are the names of my textboxes that the user enters to populate the form.
Sub LoadAndSaveData()
NumberofAgents = 3
Dim AgentName(3) as String
AgentName(1) = Ag1Name.Value
AgentName(2) = Ag2Name.Value
AgentName(3) = Ag3Name.Value
For Count = 1 To NumberOfAgents
With ActiveDocument.CustomDocumentProperties
.Add Name:="AgentName" & Count, LinkToContent:=False, Value:=AgentName(Count), Type:=msoPropertyTypeString
End With
Next Count
End Sub
The data doesn't get saved to the Custom Document Properties when the code is set up in a loop like the above. Since there are so many values to save and all the data is already in arrays, I would much prefer to use a loop rather than write out a separate line of code for all ~50 of the values. It does seem to work when each field is saved in a separate line of code.
I think this would probably get what you want. You don't really need to count the document properties first, only increment with the ones you want to update. Hopefully the only document properties you want contain the name AgentName in it.
ReDim AgentName(0) As String
Dim P As Long
For Each c In ThisDocument.CustomDocumentProperties
If InStr(1, c.Name, "AgentName", vbTextCompare) > 0 Then
ReDim Preserve AgentName(P)
AgentName(P) = c.Value
P = P + 1
End If
Next c
As a guest I cannot post a comment here, but the code you gave works OK here.
However, there is a problem with creating legacy custom document properties programmatically, because doing that does not mark the document as "changed". When you close the document, Word does not necessarily save it and you lose the Properties and their values.
However, if you actually open up the Custom Document Property dialog, Word does then mark the document as "changed" and the Properties are saved.
So it is possible that the difference between your two scenarios is not the code, but that in one scenario you have actually opened the dialog box to check the values before closing the document and in the other you have not.
If that is the case, here, I was able to change this behaviour by adding the line
ActiveDocument.Saved = False
after setting the property values.
If you do not actually need the values to be Document Properties, it might be better either to use Document Variables, which are slightly easier to use since you can add them and modify them with exactly the same code, or perhaps by storing them in A Custom XML Part, which is harder work but can be useful if you need to extract the values somewhere where Word is not available.
You can make this even easier by looping the controls on the UserForm, testing whether the control name contains "Ag" and, if it does, create the Custom Document Property with the control's value - all in one step.
For example, the following code sample loops the controls in the UserForm. It tests whether the controls Name starts with "Ag". If it does, the CustomDocumentProperty is added with that control's value.
Sub LoadAndSaveData()
Dim ctl As MSForms.control
Dim controlName As String
For Each ctl In Me.Controls
controlName = ctl.Name
If Left(controlName, 2) = "Ag" Then
With ActiveDocument.CustomDocumentProperties
.Add Name:=controlName, LinkToContent:=False, value:=ctl.value, Type:=msoPropertyTypeString
End With
End If
Next
End Sub
I feel a little stupid... I just realized that the reason that the code wasn't working was that the variable NumberofAgents was not being calculated correctly elsewhere in my code. I've got it working now. Thanks for your thoughts!

VBA on MSWord: Is there an event for speak ended for SAPI.SpVoice object?

I'm using VBA on Microsoft Word.
I've create the SAPI.SpVoice object:
Set speech = CreateObject("SAPI.SpVoice")
And I'm using it to read the selected paragraph:
Selection.Next(wdParagraph).Select
speech.Speak Selection.Text, 3
It works great. Now, when "speaking" is finished, I want the program to automatically move to the next paragraph and read it. i.e. to run the above two lines of code, again.
When I did something similar in C# I used the SpeechSynthesizer.SpeakCompleted event to invoke the "Move to the next paragraph and read it" function. I'm looking for the equivalent in VBA.
Any ideas?
Found the solution. Apparently, the solution itself is simple but it didn't work since I had the wrong reference in VBA.
In VBA > Tools > Reference there were two appearances of "Microsoft Speech Object Library".
One was referencing to C:\Windows\System32\Speech_OneCore\common\sapi_onecore.dll
The other was referencing to C:\Windows\System32\Speech\Common\sapi.dll
The 2nd one is the right one.
After fixing this, I've just used the simple integrated event:
Private Sub speech_EndStream(ByVal StreamNumber As Long, ByVal StreamPosition As Variant)
'Do stuff
End Sub

VB.Net equivalent for (CustomizationContext) in VBA

I'm busy with some word automation and have run into an issue whereby a context menu within a document has items in, that I wish to remove.
Once the document is open, through vba I can remove these items by running the following code;
[VBA]
Dim oContextMenu As CommandBar
Dim oContextMenuItem As CommandBarControl
'Make changes to the ActiceDocument only (this is needed to make any changes to this document).
CustomizationContext = ActiveDocument
For Each oContextMenu In ActiveDocument.CommandBars
If oContextMenu.Type = MsoBarType.msoBarTypePopup Then 'Loop through all the context menus of type (msoBarTypePopup)
For Each oContextMenuItem In oContextMenu.Controls
If (InStr(oContextMenuItem.Caption, "Smokeball")) Then
oContextMenuItem.Delete
End If
Next
End If
Next
If I execute this code and check the document, all contextMenu sub items that contain the text "smokeball" are removed.
When I try move this code to my VB.NET solution (I have no choice of language, so VB it is), I get errors on the CustomizationContext = ActiveDocument line (this line has to be there for it to affect the current document).
The error I get is CustomizationContext' is not a by reference property.
Does anyone know how to get just that ONE line equivalent for vb.net?
Thanks in advance.
EDIT: In case you need to see the vb.net sub:
Private Sub RemoveUnwantedContextMenuItems()
Dim oContextMenu As CommandBar
Dim oContextMenuItem As CommandBarControl
'Make changes to the ActiceDocument only (this is needed to make any changes to this document).
WordApplication.CustomizationContext = WordApplication.ActiveDocument 'This is the error.
For Each oContextMenu In WordApplication.CommandBars
If oContextMenu.Type = MsoBarType.msoBarTypePopup Then 'Loop through all the context menus of type (msoBarTypePopup)
For Each oContextMenuItem In oContextMenu.Controls
If (InStr(oContextMenuItem.Caption, "Smokeball")) Then
oContextMenuItem.Delete()
End If
Next
End If
Next
End Sub
PS - I have also already tried using the .AttachedTemplate as well as .Normal / .NormalTemplate
Jules pointed me in the right direction with his sample code.
After lots of playing around I noticed that somewhere in the solution, the [TYPE] of WordApplication was getting changed to a dynamic type of sorts, hence, it couldn't use CustomizationContext.
My solution was this:
I changed this line;
WordApplication.CustomizationContext = WordApplication.ActiveDocument
To this:
CType(WordApplication, Microsoft.Office.Interop.Word.Application).CustomizationContext = WordApplication.ActiveDocument
Forcing the types to be correct.
Simple solution but took some time.
Thanks to Jules for pointing me in the right direction.
(Points should go to you).

.Select, .Activesheet, .Activecell etc...

For this question, I refer to the post below to clarify myself:
Why is my conditional format offset when added by VBA?
In many, many posts I see these days, OP's are silently allowed to use .Activate, .Select, .Offset, etc... while they are an open door to potential bugs (most often caused by the end users).
The code is sometimes even supported.
My question: Is there one valid situation where you would use any of these statements without direct alternatives being available that catch typical bugs resulting from these stmts?
I'm referring to dynamic solutions that in my opinion are a must when developing for Excel.
Personally, in more than 6 years I can't remember a single case where I needed it; it seems always to be one of the the worst options available. In my previous company, it was a silent rule never to use it and it only made my VBA life (and that of the end user) better.
Why I create this question is because I think that it is worthful to make newcomers into VBA aware of the risks they take when using these statements (by experience proven risks when the End Users do something unexpected - in the end they don't have any affection with VBA) and to propose direct alternatives (I won't state I always did that before myself, but I feel in my gut that there is something wrong with just offering quick solutions on already bug monsters).
I believe that when silently allowed (which it automatically enhances in this case), starting VBA developers will create a growing amount of tools the wrong way (and thus also newcomers will inherit the behaviour - which they will also learn from Stack Overflow since Google returns the results they look for (!)).
If the developer is not aware why he "can" use a "select" and in which situations it is a potential bug, (s)he should never use it imho. Personally I might use the select stmt in the immediate window to do some quick checks on dynamic range definition (bug mode), but not in written code.
The result makes VBA in the end even more unpopular than it is already; the language will be made the victim in case trouble appear (yet it is imho still the "best" programming support available for the Excel and Access applications). I've seen this happen too many times in a large company where VBA is always "shit".
This is only my own honest experience.
It is not a question of being right or wrong; I am interested in hearing your point of view on the question.
I agree about Select and Activate, but not ActiveWorkbook, ActiveSheet, and ActiveCell (I agree that they are abused, but not that they should be avoided, per se). There are definitely legitimate uses for those. I have a program that automates a "fill series" that does so from the ActiveCell. My program can't predict what cells will be used; it's up the user to select it. That's part of the user interface.
However, there are three situations where I have had to use Select (now four that I read about zoom, but I don't ever use it).
Conditional Formatting. There is a work around using Application.ConvertFormula, but it's worse than just storing the selection, selecting the right cell, doing the deed, and reselecting the previous selection.
Data Validation. Same reason.
Shapes. I wish I could remember the details, but it's been too long since I've worked with Shapes. There was something I couldn't do without selecting the shape first.
Ridding code of Select and Activate is a noble fight.
There are a few methods in Excel that require Activate or ActiveSheet/ActiveWorkbook etc as I've been caught with a gotchas on occasion. The only one I can remember at the moment is the zoom property. Zoom affects only the sheet that's currently active in the window so to zoom all sheets you would need something like
Sub SetZoom()
Dim ws As Worksheet
Application.screenupdating = false
For Each ws In Worksheets
ws.Select
ActiveWindow.Zoom = 80
Next ws
Application.screenupdating = true
End Sub
You can use .Select to determine what a user's view is after running code - for example if you create a new workbook in your code, without using Activate or Select your user may not know this happens.
I frequently end a long operation creating a new workbook or other largescale data manipulations with
FinalViewWorkbook.FinalViewSheet.Range("A1").Select
Just to inform the end user about something - "oh, this created a new workbook of reports!" etc.
I think it is important in this matter to distinguish some:
Active-something: Only use this if it is absolutely necessary to know what the user is handling right now. In my experience, this is usually Data Validation or Active Sheet Detection (e.g. "Update the Sheet where the user just pressed a button").
Selection: Somewhat the same as Active, only use readingly. Userful either for Data Validation, or for gimmicks like "Interpret the cell value as path and open it in a new Explorer Window".
Select, Activate: Imho different from Selection, as it actually changes the selected Cell, Sheet etc. Never ever use this to read or write data, since it enables a user to mess up your program by just clicking. Users love to click. Only use this to Zoom (see answer by #user3357963) or clean up a view after your code has finished working (see answer by #enderland). (I'm not sure, but I think handling the PageView also requires ActiveSheet).
Select, Activate the 2nd: If you are new to VBA and are learning via Macro Recorder, you will find a lot of code generated like this:
First Range("A5").Select, then Selection.Value="NewValue". Join this to Range("A5").Value="NewValue".
Offset: Personally, I don't have a problem using .Offset() - I never encountered problems with this command. Instead, I think it's a handy way of saying "The cell next to this" without having to go through "This cell's sheet at this cell's row and column+1" every time.
In many, many posts I see these days, OP's are silently allowed to use .Activate, .Select, .Offset, etc...
I agree with this. Even though it's easier to just give the necessary answer to make a piece of code work, the use of ActiveCell.Value and the like should be discouraged. This will be much easier if there's a well explained Thread to link to, as this here is hopefully becoming :-)
From my perspective, with few exceptions, the only time you should use Select is as a user input, and only then after careful consideration of alternative design/UI requirements.
For example, I'd say it's generally not advisable to rely on Selection to let user define a Range object when this method keeps execution within the code:
Dim myRange as Range
Set myRange = Application.InputBox("Select your range", Type:=8)
However, if you need to prompt users to select a particular shape or object on the worksheet, then maybe it's better to let them make a Selection (however, this can open up a Pandora's Box of problems without good error-handling and logic to prevent undesired user actions...).
Here is an example of one such exception that I have in PowerPoint. I have some RibbonUI XML and VBA that adds buttons to the Shapes right-click context menu in PowerPoint, and adds similar buttons to the Ribbon itself. These are seamless UI that give the end-user a more "native" experience with the application -- users want to be able to right-click the chart and then run some macro procedures against that selected chart or table, etc. They don't want to press a button to open up a user form and scroll through a listbox of generic shape names or GUIDs.
The procedure code needs to examine the Selection in order to handle it properly so I can use something like below, where
Sub UpdateOrEditSelection(update As Boolean)
'This procedure invoked when user edits/updates a chart.
Dim uid As Variant
Dim sel As Selection
Dim s As Integer
Dim chartsToUpdate As Object
Dim multipleShapes As Boolean
Dim sld As Slide
Set sel = ppPres.Windows(1).Selection
If update Then
Set chartsToUpdate = CreateObject("Scripting.Dictionary")
Select Case sel.Type
Case ppSelectionShapes
For s = 1 To sel.ShapeRange.count
uid = sel.ShapeRange(s).Name
'....
'...
'..
'.
Next
Case ppSelectionSlides
For Each sld In sel.SlideRange
For s = 1 To sld.Shapes.count
uid = sld.Shapes(s).Name
'....
'...
'..
'.
Next
Next
Case ppSelectionText
s = 1
If sel.ShapeRange(s).HasTable Or sel.ShapeRange(s).HasChart Then
uid = sel.ShapeRange(s).Name
'....
'...
'..
'.
End If
End Select
'....
'...
'..
'.
Where does it come from?
The Macro Recorder. Essentially, this feature records every literal user input: scrolling, selecting, viewing, activating, default properties, etc., to the point of overkill. While this is sometimes helpful, it does encourage bad code written by people who don't know that it's bad, but I will not belabor that point which has been made here:
How to avoid using Select in Excel VBA macros
What is better, conceptually?
Program to the objects directly. If you're merely using VBA to mimic keystrokes and mouseclicks, you're doing it wrong.
Exceptions:
I've found when applying formatting to series data in charts, where Select is occasionally necessary. This seems IMO to be a bug with Excel and not a design feature.
Other applications (because VBA is not only Excel):
Word is a different animal, which relies a lot more on Selection object
In PowerPoint there are some sort of operations that can only be performed when the application and slide/shape are visible or otherwise in view. While you don't usually need to "select" anything, it does require more cumbersome code.
I found this snippet in my App:
Set tb = cht.Shapes.AddTextbox(msoTextOrientationHorizontal, ptLeft, tBoxTop, ptWidth, ptHeight)
tb.Select '<--- KEEP THIS LINE OTHERWISE TEXTBOX ALIGNMENT WILL NOT WORK ## ## ##
And this:
'PPT requires selecting the slide in order to export an image preview/jpg
sld.Select
ppPres.Windows(1).View.GotoSlide sld.SlideIndex
sld.Shapes(1).Chart.Export imgPath, ppShapeFormatJPG
And this, dealing with individual Point objects:
pt.Select
pt.format.Line.Visible = msoTrue
pt.format.Line.Visible = msoFalse
pt.MarkerSize = pt.MarkerSize + 2
This is not an exhaustive list, just some examples of exceptions that I found. While these were from PowerPoint, the charts in PowerPoint use the same object model as Excel so I would not be surprised if some of these also need to be hacked in Excel, and Word, too.
Outlook: I don't do much with Outlook, it is a lot like Word and actually uses the Word object model in the Inspector, but what little I do with Outlook does rely on things like ActiveInspector, etc.
Neither Word or PowerPoint have a "macro recorder" anymore (actually, I think Word might but it's so damn impotent as to be useless) and by the time most people do any development in other applications, they've figured most of this out already.