Advanced Visual Basic - Project 6

My Editor - OLE Automation

Creating a Simple Editor that uses the Microsoft« Word Spell checker

A very powerful feature of Visual Basic is the CreateObject function. With CreateObject you can create OLE Automation objects within your Visual Basic programs. An OLE Automation object can access the functionality of other applications or programming tools through OLE Automation interfaces. In this project you will create a simple text editor which, through OLE automation, allows the User to spell check their text with Microsoft Word’s spell checker. This is possible because Microsoft Word is an OLE Server application. An OLE Server is an application which defines classes that other applications can manipulate using standard Visual Basic syntax. The classes defined in an OLE server can have their own properties and methods. In this case you will be creating an OLE Client application which creates a Word Basic object. Word Basic is a powerful Macro Language built into Microsoft Word.

Oh, I almost forgot to mention, you’ll also be using a new control in this application as well. You can see it in the example above at the bottom of the screen—A Status Bar.

Load Visual Basic and select New Project under the File menu (make it a Standard project). Press the F4 key to view the Form’s properties—if they are not already displayed.

Add the following controls to the blank frmEdit form:

Use the Menu Editor to add a menu with the following structure:

Level 1
Caption
Level 2
Caption
Suggested
Name
&File   mnuFile
  &Open mnuOpen
  &Close mnuClose
  Save &As mnuSaveAs
  &Save mnuSave
  mnuBrk (a single hyphen)
  E&xit mnuExit
&Tools   mnuTools
  &Spell Check mnuSpell

If it’s been awhile since you last used the Menu Editor, be careful to set only the Caption and Name properties of each menu item—leave all other settings for each item at their defaults. You can change the level and order of the menu items with the arrow buttons on the Menu Editor:

Now add these controls to the blank frmEdit form:

1 CommonDialog (Use this control for the Open and Save As menu items)

1 Textbox (Multiline=True, Font=Arial 10pt., Scrollbars=2-Vertical)

1 Status Bar (if the Status Bar control is not available in the Control Toolbox, right click on the Control Toolbox and select Components from the context menu. Put a check mark next to the Microsoft Windows Common Controls #.# item and click OK. #.# means the latest version number in the list (6.0 or higher). Do not choose the -2 #.#)

After placing the Status Bar on the form, view it’s properties and click on the ellipsis () button of the Custom property to access the Statusbar Control Properties dialog. Click on the Panels tab and use Insert Panel and the options on the Style dropdown list box to create 5 panels. Use the illustration at the beginning of the document and add Date, Time, Caps, and Insert displays to the 4 panels on the right. Leave the first panel on the left blank.

Here is the code that spell checks the text in your Textbox (txtEditor) control. This code goes in your mnuSpell_Click event procedure (the lines are numbered and described below the code. Don't type the line numbers when you type this code):

  1. Dim WordObj As Object
  2. Set WordObj = CreateObject("Word.Basic")
  3. Clipboard.Clear
  4. Clipboard.SetText txtEditor.Text, 1
  5. WordObj.FileNew
  6. WordObj.EditPaste
  7. WordObj.ToolsSpelling
  8. WordObj.EditSelectAll
  9. WordObj.EditCopy
  10. WordObj.FileClose 2
  11. WordObj.FileQuit
  12. Set WordObj = Nothing
  13. txtEditor.Text = Clipboard.GetText()
  1. Here’s where you dimension your Word Object variable. Object variables are stored as 32-bit (4-byte) addresses that refer to OLE Automation objects within an OLE Server application. A variable dimensioned as an Object type is one that can subsequently be assigned (using the Set statement) to refer to any object produced by an OLE Server application.
  2. This is how you declare a Word.Basic object. When this command is executed, Microsoft Word is actually launched and minimized. After this declaration, you can use the WordObj variable to execute other Microsoft Word commands. This is possible because Word Basic is an OLE class object embedded in Microsoft Word. That’s what makes Microsoft Word an OLE Server. Microsoft Word is comprised of many OLE objects. So is Microsoft Excel. Microsoft Word and Excel share the same Spell Checking object. In this project you are going to access that Spell Checker via Microsoft Word by using its Word Basic macro language.
  3. Clipboard.Clear clears the contents of the Clipboard. The Clipboard object in Visual Basic doesn’t need to be dimensioned or declared ahead of time. It gives you access to the system Clipboard. Since you’re about to copy the contents of your Textbox (txtEditor) to the clipboard, it’s a good idea to clear it first.
  4. Clipboard.SetText txtEditor.Text, 1. This command copies the text from your Textbox (txtEditor) to the Clipboard. Get help on the Clipboard object’s SetText method to find out what the parameter 1 does.

5-11) The next 6 commands are all Word Basic commands:

  1. WordObj.FileNew opens a new blank document in Microsoft Word in the default format (Normal) (The Word application remains minimized).
  2. WordObj.EditPaste pastes the contents of the system clipboard—which is the text from the Textbox (txtEditor)—onto the new Word document. This is exactly like dropping down the Edit menu in Word and selecting the Paste item (The Word application still remains minimized).
  3. WordObj.ToolsSpelling invokes the spell checker. This is like dropping down the Tools menu in Word and selecting the Spelling item. (The Word application still remains minimized). At this point if there are any misspelled words, the spell checker’s correction dialog appears with suggestions so the User can make corrections. When spell checking is complete, control returns to your Visual Basic program and the next command is executed.
  4. WordObj.EditSelectAll selects all the text in the Word document. At this point Microsoft Word can no longer function minimized. The User sees Word suddenly appear on their screen (usually in a small window). However, it’s just there for a moment as the text is selected and the next 2 commands are executed.
  5. WordObj.EditCopy copies the selected text to the system clipboard.
  6. WordObj.FileClose 2 closes the file, the parameter 2 tells Word not to save it.
  7. WordObj.FileQuit quits the Microsoft Word application.
  1. Set WordObj = Nothing deletes the WordObj object. The Nothing keyword in Visual Basic is used to disassociate an object variable from any actual object.
  2. txtEditor.Text = Clipboard.GetText() copies the contents of the System Clipboard—the spell checked and corrected text—to the your Textbox (txtEditor).

Try to get each of the menu items functioning on your own—I’ve given you the code for the mnuSpell menu item in step 2 above. Keep the following suggestions in mind while you work:

Hints for completing each menu item follow. Please try to use these hints sparingly. And don’t forget to add your own personality to this project. When you’re finished, be sure to implement the required enhancement at the end.

Hints

Open (mnuOpen_Click)

Remember, the CommonDialog control doesn’t actually open a file for you. It just provides the interface for the user to select a file to open. These 4 lines of code should do it (don’t forget to dimension the sFileName string variable):

‘This code goes in the General Declarations section
Dim sFileName As String

‘This code goes in the mnuOpen_Click event procedure
'Set the Dialog title
CommonDialog1.DialogTitle = "Enter Name Of File To Open"
'Set file filter to TXT file types
CommonDialog1.Filter = "Txt Files (*.txt)  |  *.txt"
'Display the Open file dialog box
CommonDialog1.ShowOpen
'Extract the file name the user selected
sFileName = CommonDialog1.filename

Opening and reading the file is another matter. With this next line of code you need to make sure that sFileName isn’t an empty string (which is what it could be if the User didn’t select a file and clicked the OK button on the open file dialog box. We will address actually capturing the Cancel Error generated by the Cancel button of a CommonDialog control later in this project):

If sFileName = "" Then Exit Sub

If sFileName is an empty string, this code exits the subroutine immediately. If sFileName is not an empty string then you need to open the file and read the first 50 lines of it into the txtEditor Textbox (don’t forget to dimension sText, sLine, and iLineCount. You don’t need to include the extensive comments I’ve added to this code):

‘This code is a continuation of the code that goes in the mnuOpen_Click
'
      event procedure

'Open the file for input
Open sFileName For Input As #1
‘Make sure sText starts out Null, and iLineCount 0 (zero)
sText = ""
iLineCount = 0

'Read the file to the EOF or the 1st 50 lines, whichever comes first
Do While Not EOF(1) And iLineCount < 50
'Read a line of text from the file into sLine
Line Input #1, sLine
'Append sLine to sText and increment iLineCount. Here sLine is appended to
sText along with a Carriage Return (13) and a Linefeed (10) character. The
Line Input command reads lines from a file, but fails to preserve Carriage
‘Return and Linefeed characters, so you have to add them back manually. When
‘this loop is finished, sText will contain all of the lines that were read.
sText = sText & sLine & Chr(13) & Chr(10)
‘Instead of Chr(13) & Chr(10), you could use VB’s vbCrLf constant like this:
‘sText = sText & sLine & vbCrLf
iLineCount = iLineCount + 1
Loop
'Close the file (don’t forget to do this!)
Close #1
'Copy sText to the txtEditor Textbox
txtEditor.Text = sText

That takes care of the code to get the Open menu item working.

Save As (mnuSaveAs_Click)

The Save As function should allow the User to assign a name to the file. The CommonDialog’s ShowSave method makes this easy, like so:

‘This code goes in the mnuSaveAs_Click event procedure
Dim sNewName As String
'Set the Dialog title
CommonDialog1.DialogTitle = "Enter Name To Save File As"
‘Set the Filter for TXT files only
CommonDialog1.Filter = "Text File (*.txt)  |  *.txt"
'Show the SaveAs dialog
CommonDialog1.ShowSave
‘Grab the file name they create and put it into sNewName
sNewName = CommonDialog1.filename

Like you did in the Open function, you must make sure that sNewName isn’t null as a result of the User hitting the Save As Dialog’s Cancel button. Only then do you open the file and write the contents of the txtEditor Textbox to it:

'Make sure sNewName isn't an empty string
If sNewName <> "" Then
      
'sNewName includes the drive and path to the new file
      Open sNewName For Output As #1
      
‘Write the contents of the Textbox to the file
      Print #1, txtEditor.Text
      
'Close the file (don’t forget to do this!)
      Close #1
      
'Make sure the Save option works without prompting for a file name
      sFileName = sNewName
End If

That takes care of the code to get the Save As menu item working.

Save (mnuSave_Click)

This is implemented like the Save As function except that the file should already have a name and you just need to write the contents of the txtEditor Textbox to it. What if the file doesn’t already have a name? Better check before trying to open a file for writing with null for a name:

‘This code goes in the mnuSave_Click event procedure
If sFileName = "" Then
      
‘If the file has no name, default to the Save As function so the User can
      '        assign one
      mnuSaveAs_Click
Else
      Open sFileName For Output As #1
      Print #1, txtEditor.Text
      Close #1
End If

That takes care of the code to get the Save menu item working.

Putting the Filename in the Status Bar

The easiest part of this process is actually putting the name in the left panel of the Status Bar. Each panel on the Status Bar is an element in an array of panels. The far left panel is element 1 (just to keep you confused, this array begins with element 1 not 0 like most arrays in Visual Basic). To the right of panel element 1 is panel element 2 and so on. Here’s the command to assign a filename to the far left panel (panel element 1) of the Status Bar (sbStatus):

sbStatus.Panels(1).Text = CommonDialog1.FileTitle

Notice how we're using the CommonDialog's FileTitle property and not the filename property to assign the filename to the Status bar. That's because the CommonDialog’s filename property includes not only the file’s name, but the complete path to where the file is located. Here’s an example of what CommonDialog1.filename might contain:

c:\windows\textdocs\test1.txt

The FileTitle property conveniently includes only the filename without the path, saving us the trouble of extracting the file name. That completes this project.

Trapping the CommonDialog's Cancel button

By default, when you use a CommonDialog control to display Save or Open file dialog boxes, you can't detect when the User clicks the dialog's Cancel button. 

The CommonDialog's CancelError Property

All CommonDialog controls come with a CancelError property which is specifically designed to help you detect when the User clicks the Cancel button of a CommonDialog generated dialog box.  When a CommonDialog's CancelError property is set to True,  a critcal error is generated whenever the User clicks the Cancel button of a CommonDialog generated dialog box.  Normally this would cause your program to crash, but we can trap the error to prevent that from happening.  Take a look at the basic code required to trap a program crashing (critical) error:

Private Sub mnuOpen_Click()
     On Error Goto ErrorHandler
     ...
     ...  Code that may cause the critical error goes here
    
...
     Exit Sub
ErrorHandler:
     ...  Error handling code would go here
End Sub

Just 3 new lines of code are required to implement an Error Trap:

  1. On Error Goto ErrorHandler   —  The "On Error Goto <Tag>" command gets inserted at the beginning of the event procedure (before the code that may cause the error).  If a critical error happens, this line of code stops the normal execution of code and jumps to the line of code following the tag (ErrorHandler:).
  2. Exit Sub  —   This line of code must be inserted above the error handler tag so that if no error occurs it exits the procedure before executing the error handler code.
  3. ErrorHandler:   —  This is the jump tag (specified in the On Error Goto line).   It can be any word, but it should be descriptive, followed by a colon (:).

Here's how the above error trapping code would look inserted into your mnuOpen_Click event procedure:

Private Sub mnuOpen_Click()
     Dim sText As String
     Dim sLine As String
     Dim iLineCount As Integer
α On Error Goto ErrorHandler
     CommonDialog1.DialogTitle = "Enter File Name To Open"
     CommonDialog1.Filter = "Text File (*.txt) | *.txt"
     CommonDialog1.InitDir = "A:\"
     CommonDialog1.ShowOpen
     sFileName = CommonDialog1.filename
     CommonDialog1.filename = ""

     If sFileName <> "" Then
          Open sFileName For Input As #1
          sText = ""
          iLineCount = 0

          Do While Not EOF(1) and iLineCount < 50
               Line Input #1, sLine
               sText = sText & sLine & Chr(13) & Chr(10)
               iLineCount = iLineCount + 1
          Loop

          Close #1
          txtEditor.Text = sText
     End If

α Exit Sub
α ErrorHandler: 
     'No actual error code is required in this situation, since if the User
     '    decides to cancel opening a file, no real error has occured

End Sub

Normally, we would be using error trapping code like this to catch unexpected errors (like the error caused when a CDbl conversion function tries to convert a string of non-numeric characters into a number—an error that results when a User enters text into a number field).  However, in this situation, we are actually forcing the CommonDialog control to generate a critical error by setting it's CancelError flag to True (this is the ONLY way to detect the Cancel button on a CommonDialog generated dialog box).  We are using the error trap to re-route the execution of the program in the event of an error.  

You can use error trapping code like this to prevent procedures that are prone to cause intermittent errors from crashing your program.  For those really hard to track down errors, It's a good idea to display an informative message box so that you know where the error occurred after your program is compiled, like this:

Private Sub <Procedure Name>
     On Error Goto ErrorHandler
     ...
     ...  Code that may cause the critical error goes here
    
...
     Exit Sub
ErrorHandler:
     'The Number and Description properties of the Err object 
     '      can be used to display useful information about the error
     MsgBox  Err.Number & ": " & Err.Description,  _
                       vbExclamation, "Error"
End Sub

Required Enhancement

Get the Close (mnuClose) menu item working. It should do the following:

More Spell Checking Options

Spell Checking Selected Text

A Textbox control has a SelLength property which is always equal to the length, in characters, of any selected text in the textbox.  If SelLength is zero, there is no selected text, if it's bigger than zero then there is.  A Textbox also has a SelText property which is always equal to the text that is selected.  We can use the SelLength property of txtEditor to help us decide whether we want to spell check the entire document (If txtEditor.SelLength = 0), or spell check the text that's selected by the user (If txtEditor.SelLength > 0).  Take a look at the following modification of our original code, which either copies the selected text (txtEditor.SelText) to the clipboard (when txtEditor.SelLength > 0) or the entire document (when txtEditor.SelLength = 0). 

Dim bSelected As Boolean
Dim WordObj As Object 
Set WordObj = CreateObject("Word.Basic")
Clipboard.Clear
'If no text is selected, Spell-Check ALL text in the textbox
If txtEditor.SelLength = 0 Then
    Clipboard.SetText  txtEditor.Text, 1 
    bSelected = False
'Else only Spell-Check the selected text
Else
    Clipboard.SetText  txtEditor.SelText
    bSelected = True
End If
WordObj.FileNew 
WordObj.EditPaste 
WordObj.ToolsSpelling 
WordObj.EditSelectAll 
WordObj.EditCopy 
WordObj.FileClose 2 
WordObj.FileQuit 
Set WordObj = Nothing 
'If the entire document was spell checked, assign
'     the contents of the clipboard to the Text
'     property. Else overwrite the selected text with
'     the contents of the clipboard.
If bSelected = False Then 
    txtEditor.Text = Clipboard.GetText() 
Else
    txtEditor.SelText = Clipboard.GetText() 
End If

An Alternate Method of Spell Checking

We are not limited to using the Word.Basic object library to access the exposed functionality of Microsoft Word. The following code uses the Microsoft Word 9.0 Object Library to do the job. To use this code you must select References under the Project drop down menu and put a checkmark in front of Microsoft Word 9.0 Object Library (if 9.0 is not available, choose the highest version number you can find). Then add this code to a new menu item's click event procedure (i.e. mnuSpell2):

  1. Dim WordObj As Word.Application
    Dim i As Integer, sTmp As String
  2. Set WordObj = New Word.Application
  3. WordObj.Documents.Add
  4. WordObj.Selection.TypeText txtEditor.Text
  5. WordObj.Documents.Item(WordObj.Documents.Count).CheckSpelling
  6. txtEditor.Text = WordObj.Documents.Item(WordObj.Documents.Count).Content
  7. txtEditor.Text = Left(txtEditor.Text, Len(txtEditor.Text) - 1)
  8. WordObj.Quit wdDoNotSaveChanges
  9. Set WordObj = Nothing
    'Word replaces the Carriage Return (13) and a Linefeed (10) characters
    '    in our document with a single Carriage Return (13).  This For-Next loop
    '    is required to replace every single Carriage Return with a Carriage Return 
    '    LindFeed combination (vbCrLf) to get our document to display properly.
  10. For i = 1 to Len(txtEditor.Text)
        
    'When a character is Carriage Return (13), replace is with vbCrLf
         If Asc(Mid(txtEditor.Text, i, 1))  =  13 Then
               sTmp = sTmp  &  vbCrLf
         Else 
               sTmp = sTmp  &  Mid(txtEditor.Text, i, 1)
         End If
    Next i
    'Put the corrected text back in the txtEditor textbox
  11. txtEditor.Text  =  sTmp

We're doing a couple of things in the code above in a different way from what we did with the Word.Basic reference that we had previously used. For example, let's take a look at the first 2 lines of code above:

Dim WordObj As Word.Application
Set WordObj = New Word.Application


We could have done this instead:

Dim WordObj As Object
Set WordObj = CreateObject("Word.Application")


This above method--which uses CreateObject like our Word.Basic version earlier--doesn't require us to explicitly set a reference to the Microsoft Word 9.0 Object Library ahead of time.  Both ways achieve the same result, but I wanted you to see a different way of doing it (There is actually a difference between both methods at a lower level: By dimensioning WordObj as an Object reference, the compiler doesn't know at compile time what type of object WordObj is supposed to reference.  That is why we must use the CreateObject function to set our reference to Word.Application.  On the other hand, by dimensioning WordObj as a Word.Application reference directly, CreateObject is not required and we can set our reference to Word.Application with the New command.  While both methods achieve the same goal, the new way I've shown you (which dimensions WordObj as Word.Application directly), binds WordObj to the Word.Applcation library at compile time, whereas the original way we did it (dimensioning WordObj as an Object and using CreateObject to set the reference at run-time), binds WordObj to Word.Application at Run time (which makes the program load slightly slower).  That's the only real difference between the two methods.

Now let's take a look at some more code from above (lines 3 through 9):

  1. WordObj.Documents.Add
  2. WordObj.Selection.TypeText  txtEditor.Text
  3. WordObj.Documents.Item(WordObj.Documents.Count).CheckSpelling
  4. txtEditor.Text = WordObj.Documents.Item(WordObj.Documents.Count).Content
  5. txtEditor.Text = Left(txtEditor.Text,  Len(txtEditor.Text) - 1)
  6. WordObj.Quit  wdDoNotSaveChanges
  7. Set WordObj = Nothing

As you can see, we are not using commands that correspond directly to menu selections in Word (like Word.Basic objects do). In the code above we use several objects that are members of the Application class to do the job. These include the Documents and Selection classes. You should also notice that we don't need the Clip Board to move the text from the txtEditor textbox to the Word document. Instead we pass the Text property of the txtEditor textbox to the TypeText method of the Selection class (line 4 above)--this places the text directly into a new Word document. Then we copy the spelling-corrected text from Word back into the txtEditor textbox (line 6 above)--via the Item method of the Documents class. Also notice this line of code from above:

txtEditor.Text = Left(txtEditor.Text, Len(txtEditor.Text) - 1)

As a final step, the above line of code removes an End of File character that gets added to the end of the text when it's copied back from Word (this line may not be necessary, depending upon the operating system.  You need to test to see).  To see a list of ALL the Classes, Properties, and Methods that are available through the Microsoft Word 9.0 Object Library, all you need to do--after you set the Reference to the  Microsoft Word 9.0 Object Library--is open the Object Browser (press the F2 key) and choose the Word library from the Library drop down list in the upper left corner of the Object Browser. This is just the tip of a VERY big iceberg that  you could literally spend days exploring. Have fun.

Counting Spelling Errors

Before actually going to all the trouble of spell-checking a document, here is a method of counting and displaying the number of spelling errors in a document (Put this code into the Click event procedure of a new Count Spelling Errors menu item):

  1. Dim WordObj As Word.Application
  2. Dim iErrCount As Integer
  3. Dim i As Integer
  4. Set WordObj = New Word.Application
  5. WordObj.Documents.Add
  6. WordObj.Selection.TypeText  txtEditor.Text
  7. iErrCount = WordObj.ActiveDocument.SpellingErrors.Count
  8. If iErrCount > 0 Then
  9.       MsgBox "This document contains: " & iErrCount & " spelling errors!"
  10. Else
  11.       MsgBox "No spelling errors found."
  12. End If
  13. WordObj.Quit  wdDoNotSaveChanges
  14. Set WordObj = Nothing

In the above code (in line 7) the SpellingErrors object of the ActiveDocument class is actually a collection of the spelling errors that were found, so we can use its Count property to easily extract the number of spelling errors.

Because the SpellingErrors object is actually a collection of the words that are spelled incorrectly, we can also display each misspelled word with the addition of this code (lines 10 thru 12 below) to the code above:

  1. iErrCount = WordObj.ActiveDocument.SpellingErrors.Count
  2. If iErrCount > 0 Then
  3.       MsgBox "This document contains: " & iErrCount & " spelling errors!"
  4.       For i = 1 To iErrCount
  5.             MsgBox "Error #" & i & " is " &  _
                              WordObj.ActiveDocument.SpellingErrors(i)
  6.       Next i
  7. Else

These are just some of the powerful things you can do with Object Linking and Embedding.