I’ve spent quite a bit of time today with a cool feature – integrating MS Word directly into Help Builder as an edit control. By this I mean, using Word as an embedded control into my application and using it for basic editing and more importantly for its spell checking support.
The normal editing environment in Help Builder is entirely text based and using Word too leaves the actual editing as text based with all mark up handled through Help Builder’s menus, rather than through Word.
Here’s what this looks like in Help Builder:
Although the editing doesn’t take advantage of Words layout capabilities or even its markup it does provide a smoother text typing environment and of course inline spell checking.
This feature is optional and not the default edit mode – you basically have to select this option from the Edit toolbar when you are in a specific control. As soon as you move off the field, the editor reverts to a default textbox. You could say that Word is used in this scenario merely as an overlay on the stock textbox.
Of course, the customer must have Word installed in order for this to work at all, and as this interface uses the Web Browser control to host a Word document, must have security settings to allow editing Word documents locally.
How it works
Embedding Word into a FoxPro (or any other ActiveX container) is not easily done. Although Word is an Activedocument host, it doesn’t provide a direct interface to embed itself into a host application.
You can make this happen however, by using the Microsoft Web Browser ActiveX control and hosting a Word document inside of it. You can simply navigate the WebBrowser to a Word Document and Word will load into the browser control in place and expose the Word object model via the Document property.
If you’re not familiar with the Web Browser control you can look here for more information:
The following function loads a Word document into the browser control hosted on a form (or in this case a Container control in Visual FoxPro):
LPARAMETERS lcDocument, lcText
MESSAGEBOX("Can't find document: " + lcDocument,48,"Word Editing")
this.cinternaldocument = lcDocument
THIS.oBrowser.Navigate( lcDocument )
IF this.oBrowser.ReadyState # 4
WAIT WINDOW TIMEOUT .2
LOCAL loDoc as Word.Document, loApp as Word.Application
loDoc = this.oBrowser.Document
THIS.oDoc = loDoc
this.oWord = loDoc.Application
*** Put document into 'Web View' mode
loWord.ActiveWindow.View = 6
*** Match document fonts to current font
lnCount = ALINES(laWords,this.cfontinfo,",")
IF lnCount = 3
loDoc.Styles.Item("Normal").Font.Name = this.oEdit.Fontname
loDoc.Styles.Item("Normal").Font.Size = this.oEdit.Fontsize
*** Manage display of toolbars
loDoc.CommandBars("Reviewing").visible = .f.
loDoc.CommandBars("Standard").Visible = .t.
*** Load up the text into the Word doc
this.oBrowser.Document.Content.Text = lcText
*** Try to select the same selection in the Word doc
this.oWord.Selection.Start = this.oEdit.SelStart
this.oWord.Selection.End = this.oEdit.SelStart + this.oEdit.SelLength
THIS.oBrowser.Visible = .t.
This code is part of a class that essentially handles two way editing and data exchange with a textbox. The control (a Container) receives an EditBox and the container then overlays this Editbox with the Web Browser control hosting Word. When focus is lost the the control writes the output back into the textbox, matching the selections and cursor locations etc.
The code above requires that you pass a document. Word loaded into the browser needs a Word document to start with, even if you’re only interested in the text typed into Word (as I am in Help Builder). This means no matter what, you need to load some sort of document and I have an empty template document that I use for this purpose. I copy this document to a temporary location and then load the document with this method. I can optionally pass in a second parameter which is the text that gets loaded into the editing document. lcText gets set at the very end.
Note the code to load the document: It navigates the browser to the temporary document, then waits for the document to become ready. This is tricky and you need to make sure that you use DOEVENTS extensively while polling for the Browser’s ReadyState property to get to 4. The WaitForReadyState() method provides this functionality and is outlined in the article mentioned earlier.
Once the document is loaded you can get a reference to the active document which is an instance of the Word.Document COM interface. From there you can Word.Application with Document.Application, which gives you access to the entire Word document model. The code that follows customizes the look and feel of Word a bit to match what I need in Help Builder.
Specifically I want to run in Web View which is most similar to an EditBox control. You also want to hide some toolbars – the Reviewing bar always pops up by default, but this toolbar is not used for editing. You can hide all toolbars if you choose, but be aware that resizing the browser can cause some of the toolbars to pop back up…
Finally I go and set the text of the control and match it to the text in the Edit box. I also pick up the selection of the EditBox and map it over into the Word document so the doc pops up in the same cursor position.
Once that’s done you’re ready to start editing text…
To do data exchange you can now call the SaveText() method manually:
IF TYPE("THIS.oDoc") = "O"
this.Value = THIS.oDoc.Content.Text
IF !ISNULL(THIS.oEdit )
this.oEdit.Value = this.Value
this.oEdit.SelStart = this.SelStart
this.oEdit.SelLength = this.SelLength
this.Value = ""
The control exposes a value property which contains the value from the Word document. At any point you can call SaveText to retrieve the Word documents content and also update the selection points in the edit control.
This behavior can also be Auto-Triggered when the control looses focus. By default in Help Builder the control is set up to ExitOnLostFocus() so that when you click on another control or the form the control releases and updates the underlying edit control.
*** Switch back to EditControl view
*** Otherwise just save the text
To make life easier when switching Views there’s SetViewMode() method that handles swapping the EditBox and Word interfaces:
IF lnViewMode = 2
loBrowser = this.oBrowser
*** Resize into the container control
loBrowser.Left = 0
loBrowser.Top = 0
loBrowser.Height = this.Height
loBrowser.Width = this.Width
loBrowser.Anchor = 15
*** Force a resize to occur
THIS.Width = this.Width
this.oEdit.Visible = .f. && Hide the EditBox
loBrowser.Visible = .t. && Make the browser visible
this.Visible = .t.
IF TYPE("THISFORM.nWordEditMode") = "N"
THISFORM.nWordEditMode = 1 && Into Word Mode
IF TYPE("THISFORM.nWordEditMode") = "N"
THISFORM.nWordEditMode = 0 && No Word/Text Mode
this.Visible = .f.
this.oEdit.Visible = .t.
Notice that this code actually adds and deletes an instance of the browser. It does this because the Web Browser control is finicky in some ways and I had some problems from keeping the UI working properly when the Web Browser control was hidden. From time to time it would simply pop up ontop of the text box, or would cause the textbox to not be editable. The workaround was to remove the control completely.
Removing the browser or exiting the document is also a bit tricky. The problem is that Word hosted in the browser uses the underlying document for editing. Since you’ve essentially made changes to the document when you try to move off the document or close /remove the browser, Word wants to prompt you to save. So rather than the prompt, we can fake out Word by saving our document first, then navigating to a blank page, before actually removing the browser. The process looks like this:
IF TYPE("this.oBrowser") = "O"
*** Must save so we get no dialog
*** Must navigate off
THIS.oDoc = .null.
THIS.oWord = .NULL.
IF TYPE("this.oBrowser") = "O"
THIS.oEdit.visible = .t.
THIS.oEdit = .null. && Clear the reference
LOCAL llError, lnX
llError = .T.
DO while llError AND lnX < 10
llError = .f.
llError = .t.
WAIT WINDOW "" TIMEOUT .3
lnX = lnX + 1
This code makes very sure that all object reference are released. This is important or else Word may not completely shut down.
The control also has a property for lDeleteDocument which when true deletes the document that was created when done editing. The idea being that you want to have a temporary file on disk for editing using a unique name (in case you have more than a single instance of the control on a form editing documents at the same time). If true Destroy code deletes the temporary file.
Although this code is full of a few odd ‘hacks’ to make the browser and Word behave properly I find that this works actually really well. The process of managing this ‘hot swapping’ of controls in the UI is a bit of work, but quite doable. In fact, Help Builder already supports this overlay mechanism for the HTML Edit control and this interface works of the same concepts.
Help Builder had similar Word integration in previous versions for spell checking. Previously the Spell checker would bring up Word.Application as a COM object externally and then manipulate the document the same way. While that’s much easier than this integration into a form, the in-form approach is much more intuitive and provides access to all the of the editing features that Help Builder provides such as image capture/embedding and easy cross linking. This only makes sense in a form based interface and wouldn’t really work in an external interface.
While the solution I show above is a bit implementation specific and focused on exchanging data with an EditBox you can use the same logic for direct access or for simply embedding and editing/managing saving a Word document in your own applications. Check it out…