In this chapter, you'll learn some practical examples of enhancing applications using COM servers, focusing primarily on Microsoft Office.
Whether you love it or hate it, you have to give Microsoft credit. It has set a vision and it goes for it full tilt. In this case, Microsoft has had a vision of developing applications using components (hence the term Component Object Model) and it bought into it bigtime. Microsoft Office has become a model for creating applications designed not only for end-user use but also for use by developers.
Office presents a huge array of functionality. From the WinWord document to the Excel spreadsheet to the Outlook cornucopia of functionality, Office offers developers a huge suite of ready-to-use functionality at no extra cost.
The one downside to using Microsoft Office applications as COM servers lies in learning their object models. Object model, in this context, refers to the objects that these applications expose as COM servers, what their contents are (object members, properties, and methods), and when to use what.
This chapter is not designed to teach you how to use applications such as Word and Excel as COM servers. Rather, it will show you some strategies for continuing the learning process and show you examples that you can use in your applications immediately.
If you're using something like Word or Excel as a COM server, it can be a little hairy to learn what the application does and how it's done. If you need to get something done in Word or Excel, and you are unsure how to do it, there is a simple trick that many people use to get themselves over the hump.
Basically, perform the operation you need to accomplish (such as printing a document) in the application (as any normal user would do it) while recording a macro for it. Then look at the generated macro. For the most part, you will be able to copy and paste that macro into Visual FoxPro with few modifications.
For example, suppose that you need to generate a report in Excel. When generating a report, you need to enter information, format cell contents, size columns, and so on. Here's a simple way to learn how to do this without a huge learning curve.
Start Excel and turn on the macro recorder by selecting Tools, Macro, Record New Macro. The dialog that pops up enables you to name the macro (see Figure 21.1).
Figure 21.1 : The Excel Record Macro dialog appears when you initiate a new macro.
While you're recording the macro, the Macro Recording toolbar is visible (see Figure 21.2). Manually perform the operations that you want to automate, such as entering information, formatting the cells and columns, and so on. When you are done, and have accomplished at least one instance of every operation, stop the recorder.
Figure 21.2 : The Macro Recording toolbar is visible while you record a macro in Excel.
The next step is simple: Press Alt+F8 and you will see a list of all macros in the document, including the one you just created. Highlight the name of the macro you just created and click Edit. The code generated by the macro recorder appears in the Visual Basic Editor (see Figure 21.3). If you look at the code, you will see that it is remarkably similar to the code you would write in Visual FoxPro. In fact, it is so similar, you will not have to do much in the way of changes to get it to work in Visual FoxPro.
There is one problem, though. When Word and Excel record macros, the generated code uses a spate of defined constants without providing definitions for those constants. Fortunately, the constants have not been hidden from you. All constants used by these applications are documented in the application's type library. Some languages, such as Visual Basic, do not need anything other than the type library to understand these constants. Visual FoxPro cannot use the type library directly. Fear not, however, if you have the full Visual Studio, you can create an .H file for Visual FoxPro with a minimum of bother that will take care of the situation for you.
In the next section, I'll show the trick. For now, accept the fact that I have a header file called Excel.h that has all the constants in it.
The code generated by the macro recorder is shown in Listing 21.1.
Sub Macro1()
'
' Macro1 Macro
' Macro recorded 8/23/98 by Menachem Bazian, CPA
'
ActiveCell.FormulaR1C1 = "Account Number"
Range("B4").Select
ActiveCell.FormulaR1C1 = "Description"
Range("C4").Select
ActiveCell.FormulaR1C1 = "Balance"
Range("A5").Select
ActiveCell.FormulaR1C1 = "'100"
Range("A6").Select
ActiveCell.FormulaR1C1 = "'200"
Range("A7").Select
ActiveCell.FormulaR1C1 = "'300"
Range("B5").Select
ActiveCell.FormulaR1C1 = "This is the account description"
Range("B6").Select
Columns("B:B").EntireColumn.AutoFit
ActiveCell.FormulaR1C1 = "This is another account description."
Range("B7").Select
ActiveCell.FormulaR1C1 = "This is a third account description."
Range("C5").Select
ActiveCell.FormulaR1C1 = "100"
Range("C6").Select
ActiveCell.FormulaR1C1 = "200"
Range("C7").Select
ActiveCell.FormulaR1C1 = "300"
Range("C8").Select
Columns("B:B").EntireColumn.AutoFit
Range("C5").Select
ActiveCell.FormulaR1C1 = "1245.99"
Range("C6").Select
ActiveCell.FormulaR1C1 = "14290.26"
Range("C7").Select
ActiveCell.FormulaR1C1 = "-500.98"
Range("B9").Select
ActiveCell.FormulaR1C1 = "TOTAL"
Range("C9").Select
ActiveCell.FormulaR1C1 = "=SUM(R[-4]C:R[-1]C)"
Columns("C:C").Select
Selection.NumberFormat = "#,##0.00_);[Red](#,##0.00)"
Columns("A:A").EntireColumn.AutoFit
Rows("4:4").Select
Selection.Font.Bold = True
Range("B9:C9").Select
Selection.Font.Bold = True
Range("C7").Select
Selection.Borders(xlDiagonalDown).LineStyle = xlNone
Selection.Borders(xlDiagonalUp).LineStyle = xlNone
Selection.Borders(xlEdgeLeft).LineStyle = xlNone
Selection.Borders(xlEdgeTop).LineStyle = xlNone
With Selection.Borders(xlEdgeBottom)
.LineStyle = xlContinuous
.Weight = xlThin
.ColorIndex = xlAutomatic
End With
Selection.Borders(xlEdgeRight).LineStyle = xlNone
Range("C9").Select
Selection.Borders(xlDiagonalDown).LineStyle = xlNone
Selection.Borders(xlDiagonalUp).LineStyle = xlNone
Selection.Borders(xlEdgeLeft).LineStyle = xlNone
Selection.Borders(xlEdgeTop).LineStyle = xlNone
With Selection.Borders(xlEdgeBottom)
.LineStyle = xlDouble
.Weight = xlThick
.ColorIndex = xlAutomatic
End With
Selection.Borders(xlEdgeRight).LineStyle = xlNone
Range("A4").Select
Columns("A:A").EntireColumn.AutoFit
End Sub
The trick to getting this code to work in Visual FoxPro is simple. First, you have to include the header file. Also, you must define two constants for .T. and .F. (I like to define them separately; I'll explain why in the next section.)
The next step is to work on the minor differences in syntax. Mainly, you need to add periods before each collection. For example,
With Selection.Borders(xlEdgeBottom)
becomes
With .Selection.Borders(xlEdgeBottom)
Another obvious difference is the End With command, which is two words in Visual Basic for Applications and one word in Visual FoxPro.
When you're done, the final code should appear as shown in Listing 21.2.
* Program....: EXCEL1.PRG
* Version....: 1.0
* Author.....: Menachem Bazian, CPA
* Date.......: August 23, 1998
* Project....: Using Visual FoxPro 6 Special Edition
* Notice.....: Copyright (c) 1998 Menachem Bazian, CPA, All Rights Reserved.
* Compiler...: Visual FoxPro 06.00.8141.00 for Windows
* Abstract...:
* Changes....:
#INCLUDE excel.h
#DEFINE True .T.
#DEFINE False .F.
WITH oExcel
.WorkBooks.Add
.Sheets(1).Select
.Range("A4").Select
.ActiveCell.FormulaR1C1 = "Account Number"
.Range("B4").Select
.ActiveCell.FormulaR1C1 = "Description"
.Range("C4").Select
.ActiveCell.FormulaR1C1 = "Balance"
.Range("A5").Select
.ActiveCell.FormulaR1C1 = "'100"
.Range("A6").Select
.ActiveCell.FormulaR1C1 = "'200"
.Range("A7").Select
.ActiveCell.FormulaR1C1 = "'300"
.Range("B5").Select
.ActiveCell.FormulaR1C1 = "This is the account description"
.Range("B6").Select
.Columns("B:B").EntireColumn.AutoFit
.ActiveCell.FormulaR1C1 = "This is another account description."
.Range("B7").Select
.ActiveCell.FormulaR1C1 = "This is a third account description."
.Range("C5").Select
.ActiveCell.FormulaR1C1 = "100"
.Range("C6").Select
.ActiveCell.FormulaR1C1 = "200"
.Range("C7").Select
.ActiveCell.FormulaR1C1 = "300"
.Range("C8").Select
.Columns("B:B").EntireColumn.AutoFit
.Range("C5").Select
.ActiveCell.FormulaR1C1 = "1245.99"
.Range("C6").Select
.ActiveCell.FormulaR1C1 = "14290.26"
.Range("C7").Select
.ActiveCell.FormulaR1C1 = "-500.98"
.Range("B9").Select
.ActiveCell.FormulaR1C1 = "TOTAL"
.Range("C9").Select
.ActiveCell.FormulaR1C1 = "=SUM(R[-4]C:R[-1]C)"
.Columns("C:C").Select
.Selection.NumberFormat = "#,##0.00_);[Red](#,##0.00)"
.Columns("A:A").EntireColumn.AutoFit
.Rows("4:4").Select
.Selection.Font.Bold = True
.Range("B9:C9").Select
.Selection.Font.Bold = True
.Range("C7").Select
.Selection.Borders(xlDiagonalDown).LineStyle = xlNone
.Selection.Borders(xlDiagonalUp).LineStyle = xlNone
.Selection.Borders(xlEdgeLeft).LineStyle = xlNone
.Selection.Borders(xlEdgeTop).LineStyle = xlNone
WITH .Selection.Borders(xlEdgeBottom)
.LineStyle = xlContinuous
.Weight = xlThin
.ColorIndex = xlAutomatic
ENDWITH
.Selection.Borders(xlEdgeRight).LineStyle = xlNone
.Range("C9").Select
.Selection.Borders(xlDiagonalDown).LineStyle = xlNone
.Selection.Borders(xlDiagonalUp).LineStyle = xlNone
.Selection.Borders(xlEdgeLeft).LineStyle = xlNone
.Selection.Borders(xlEdgeTop).LineStyle = xlNone
WITH .Selection.Borders(xlEdgeBottom)
.LineStyle = xlDouble
.Weight = xlThick
.ColorIndex = xlAutomatic
ENDWITH
.Selection.Borders(xlEdgeRight).LineStyle = xlNone
.Range("A4").Select
.Columns("A:A").EntireColumn.AutoFit
.Visible = .T.
ENDWITH
If you look closely at the code, the only modifications are:
To quantify the entire process, generating the macro and changing the code took a few short minutes. After I have the generated code, I can examine it to determine how certain things were done and then apply these methods to the entire report.
You'll see an example of a report I did in this manner in a little bit. Before I go into that, though, there is one open issue, which is addressed in the following section.
As said earlier, the constants in the generated code are not understandable to Visual FoxPro with a header file. The question is, how can I get that header file?
Before I go any further, let me state that the issue is one that can affect much more than Microsoft Office applications. Any application that you learn in this manner can have similar issues. So, the trick of generating an .H file for a COM server might prove useful beyond the Office suite.
The trick works like this: First you have to export the type library to a text file. You do this with Visual Studio's Object Viewer. After opening the Object Viewer (see Figure 21.4), use it to open the type library by choosing File, View TypeLib. Object Viewer requests the name of the file to open; for Excel, the file is named Excel8.olb and can be found in the ...Microsoft Office\Office\ folder.
After you select the file to view, the ITypeLib Viewer displays the type library (see Figure 21.5).
Figure 21.5 : This viewer displays a type library.
You see a text representation of the type library in the right pane of the viewer. Place your cursor in the viewer and select all the text with Ctrl+A and then press Ctrl+C to copy the selected text to the Clipboard. Open a text file using Visual FoxPro or Notepad and paste the text there. Save it. After you do this, run the program MakeHFile to parse it out and make a header file.
The code for MakeHFile is presented in Listing 21.3.
*-- Program....: MAKEHFILE.PRG
*-- Version....: 1.0
*-- Author.....: Menachem Bazian, CPA
*-- Date.......: August 23, 1998
*-- Project....: Using Visual FoxPro 6 Special Edition
*-- Notice.....: Copyright (c) 1998 Menachem Bazian, CPA, All Rights Reserved.
*-- Compiler...: Visual FoxPro 06.00.8141.00 for Windows
*-- Abstract...:
*-- Changes....:
*-- Creates a header file for the constants in a type library.
*-- The type library contents have been
*-- exported with the OLE viewer prior to entering this program.
*--
*-- Parameters:
*--
*-- tcInFile - The name of the file to parse out.
*-- tcOutFile - The name of the .h file to create.
*--
*-- Note: Only the first parameter is required. If tcOutFile is not
*-- provided, the name of the input file is used as the name of
*-- the .h file. For example, Excel8.prh would output Excel8.h
LPARAMETERS tcInFile, tcOutFile
LOCAL lnInFile, lnOutFile, lcText, lcOutText, llOutput
*-- If the name of the file was not provided or it doesn't exist,
*-- error out.
IF PCOUNT() = 0 OR !FILE(tcInFile)
MESSAGEBOX("Input file must be specified!", 16, "MakeHFile")
RETURN
ENDIF
*-- If only the input file was provided, generate a .h file name
IF PCOUNT() = 1
lnDotPos = RAT('.', tcInFile)
tcOutFile = LEFT(tcInFile, lnDotPos - 1) + ".h"
ENDIF
CLOSE ALL
lnInFile = FOPEN(tcInFile)
lnOutFile = FCREATE(tcOutFile)
lcText = ""
lcOutText = ""
llOutPut = .F.
DO WHILE !fEof(lnInFile)
lcText = FGETS(lnInFile)
*-- At the close curly braces, the constants are done.
IF "}" $ lcText
llOutput = .F.
ENDIF
IF llOutPut
lcOutText = ALLTRIM(lcText)
lcOutText = STRTRAN(lcOuttext, '=', ' ')
lcOutText = "#DEFINE " + ALLTRIM(lcOutText)
IF RIGHT(lcOutText, 1) = ','
lcOutText = SUBST(lcOutText, 1, LEN(lcOutText) - 1)
ENDIF
=FPUTS(lnOutFile, ALLTRIM(lcOutText))
ENDIF
*-- Look for Typedef Enum { in the file. That's the beginning
*-- of the constants we need to export to the .h File
IF "ENUM {" $ UPPER(lcText)
llOutput = .T.
ENDIF
ENDDO
CLOSE ALL
MakeHFile is called with one or two parameters. The first parameter, which is required, is the name of the file to parse. As a naming convention, I give the exported type libraries an extension of .PRH (for pre-h file), but you can name them whatever you like. The second parameter, which is optional, gives a name to the output file (the header file to be generated). If the second parameter is not provided, the name of the input file is used and a new extension, .H, is added.
The only thing that MakeHFile does not do is deal with the representation of True and False. Visual Basic represents the value "true" as True, and Visual FoxPro uses .T.. The same goes for False and .F.. As a general rule, you can create a second header file with general definitions like this (such as FoxPro.h) and include it there. In the example just shown, I specifically defined the constants in the code.
It would be very useful to take a quick look at the code generated by the macro recorded in Excel. Excel has a hierarchical object model. In this case, you start with a workbook, which is a collection of sheets. Sheets are a collection of cells. You can reference workbooks and sheets with an array. For example, Sheets(1) is the first sheet in the display order (that is, if you look at the tabs of the sheets in the workbook, it is the left-most tab).
You can add workbooks and sheets with the .ADD method each collection has. You can set properties of the sheets and workbooks. For example, to change the name of the sheet, just change its name property.
The good news about learning to use Excel, as well as Word and Outlook, is that syntax is rather consistent. Both the Workbooks and Sheets collections use the .ADD method to add a member to their collection. In Outlook, you add an appointment to the calendar collection with-you guessed it-.ADD.
The bad news about all this is that it can get a little hairy trying to work all this stuff out. The Help documentation that comes with Excel is fairly good, but it takes time to get used to working with the collections. Word is the same. However, I find it very useful to browse the objects I am using in the Visual Basic Object Viewer (see Figure 21.6).
Figure 21.6 : The Visual Basic Object Browser provides an organized view.
Notice that Excel starts with the application class. So, if I want to see what the members of the application class are, I scroll to Application in the list on the left and I can see all of its members, methods, and properties.
Using the Object Browser in conjunction with the generated code and the help files gives you a good way to learn how to use Excel and Word as COM servers.
At this point you have seen some generated code, taken a brief look at the Excel object model, and have solved the problem of the constants. It's time to put your newfound skills to use and generate a report.
Excel is perfect for columnar reports. It especially shines when the report is one that the user might want to do ad-hoc analysis on (I did this for a client once and it saved them many hours of retyping information).
For the example here, I created a simple report based on the Customer table in the testdata sample database that ships with Visual FoxPro. The report lists the customer name and their total sales. The cells are formatted and the report is sorted by total sales (descending).
Take a look at the code for CustRpt.PRG as shown in Listing 21.4. Notice the comments in CustRpt.PRG, as they document the thoughts behind why certain things are done in a certain way.
*-- Program....: CUSTRPT.PRG
*-- Version....: 1.0
*-- Author.....: Menachem Bazian, CPA
*-- Date.......: August 23, 1998
*-- Project....: Using Visual FoxPro 6 Special Edition
*-- Notice.....: Copyright (c) 1998 Menachem Bazian, CPA, All Rights Reserved.
*-- Compiler...: Visual FoxPro 06.00.8141.00 for Windows
*-- Abstract...:
*-- Changes....:
*-- This program basically dumps all customers and their maximum order
*-- amount into a formatted Excel report.
*-- Include all the Excel stuff.
#INCLUDE Excel.h
#DEFINE False .F.
#DEFINE True .T.
*-- Default font and size I want. I #DEFINE it to make it
*-- easier to change later on.
#DEFINE RPT_FONTNAME "Comic Sans MS"
#DEFINE RPT_FONTSIZE 12
*-- First, open the database and table and run the report
CLOSE DATA ALL
*-- Note, this program uses the path from my development machine.
*-- yours may differ.
WAIT WINDOW NOWAIT "Selecting rows from Customer table..."
OPEN DATA ("D:\PROGRAM FILES\DEVSTUDIO\VFP\samples\data\testdata")
USE customer
SELECT * ;
FROM customer ;
ORDER BY MaxOrdAmt DESCENDING ;
INTO CURSOR Output
*-- Now, get the instance for Excel.
LOCAL loExcel, lcOldError, lcRange, lnSheets, lnCounter
WAIT WINDOW NOWAIT "Starting Excel..."
lcOldError = ON("ERROR")
ON ERROR loExcel = .NULL.
loExcel = GetObject(, "Excel.Application")
ON ERROR &lcOldError
IF ISNULL(loExcel)
loExcel = CreateObject( "Excel.Application" )
ENDIF
*-- At this point, I have an instance to Excel.
*-- I am *assuming* the existence of Excel. Not
*-- necessarily a valid assumption, but acceptable for
*-- the purposes of this sample program.
WITH loExcel
*-- When you start Excel as a COM server, you have no workbooks.
*-- So, I need to add one.
.Workbooks.Add
*-- One critical thing to do is make sure that the COM server
*-- doesn't put up a dialog. DisplayAlerts is a property
*-- roughly equivalent to Visual FoxPro's SET SAFETY. By setting
*-- it to False, attempting to close the workbook without
*-- saving it will not generate an error.
.DisplayAlerts = False
*-- By default, creating a workbook starts it with several
*-- sheets. I want to be neat, so I am deleting all but the
*-- one sheet I need.
lnSheets = .Sheets.Count
FOR lnCounter = 1 TO lnSheets - 1
.Sheets(1).Delete
ENDFOR
*-- Next step is to rename the sheet. Again, for neatness sake.
*-- The SELECT is probably not necessary but I like to be a bit
*-- paranoid with this just in case.
WITH .Sheets(1)
.Select
.Name = "Testdata Customers"
ENDWITH
*-- And, get rid of the grid lines... I don't like them in a
*-- formatted report
.ActiveWindow.DisplayGridlines = False
*-- OK, now that we have the housekeeping stuff done, we can
*-- get down to business. First step is to build the header of
*-- the report. That includes the title of the report and
*-- and the date/time of the report.
*--
*-- Note that there are two ways to reference a cell in Excel.
*-- The Cells collection can be used in which case you specify
*-- the row and column of the cell as numbers. Cells(1,1) refers
*-- to A1. Cells(2,1) is A2.
*--
*-- The Range() collection does the same thing except I can reference
*-- a cell by its "English" name.
*--
*-- Of the Two, I prefer the Range() method when I am going for a
*-- particular cell because I think of the cells that way. Later on,
*-- I use the Cells() collection in the loop because the numeric
*-- parameters are perfect for that kind of cell populating exercise.
*--
*-- There is one other benefit to the Range() collection. You can
*-- work on a range of cells at the same time. For example,
*-- I can format a whole range of cells in one operation.
*-- I have an example of this later on in the program.
WAIT WINDOW NOWAIT "Building Header Rows"
WITH .Range("A1")
.Value = "Testdata Customer Report"
WITH .Font
.Bold = .T.
.Size = 14
.Underline = xlUnderlineStyleSingle
ENDWITH
ENDWITH
*-- Center A1 over columns A and B
With .Range("A1:B1")
.HorizontalAlignment = xlCenter
.VerticalAlignment = xlBottom
.WrapText = False
.Orientation = 0
.ShrinkToFit = False
.MergeCells = False
.Merge
EndWith
WITH .Range("A3")
.Value = "=Now()"
.NumberFormat = "m/d/yy h:mm AM/PM"
.HorizontalAlignment = xlLeft
ENDWITH
*-- Now do the column headers
*--
*-- Just for fun, we'll color in the column headers.
.Range("A5").Value = "Customer Name"
.Range("B5").Value = "Maximum Order Amt"
lcRange = "A5:B5"
WITH .Range(lcRange)
.Font.Bold = .T.
.Font.Size = RPT_FONTSIZE
.Font.Name = RPT_FONTNAME
.HorizontalAlignment = xlCenter
WITH .Borders(xlEdgeBottom)
.Weight = xlMedium
.LineStyle = xlContinuous
ENDWITH
WITH .Interior
.ColorIndex = 42
.Pattern = xlSolid
ENDWITH
ENDWITH
*-- Now, scan through the XTAB table and put all
*-- the information in the spreadsheet
WAIT WINDOW NOWAIT "Populating cells:"
SELECT Output
GO TOP
*-- Populate the report
*--
*-- Note the use of Cells() in this case instead of Range().
lnRow = 7
SCAN
WAIT WINDOW NOWAIT "Populating cells: Record " + ALLTRIM(STR(RECNO()))
+ ;
" of " + ALLTRIM(STR(RECCOUNT()))
*-- Read the record into the cells
.Cells(lnRow, 1).Value = output.Company
.Cells(lnRow, 2).Value = output.MaxOrdAmt
lnRow = lnRow + 1
ENDSCAN
*-- OK, the body of the report is complete. Now, let's get the totals
*-- in there.
.Cells(lnRow + 2, 1).Value = "Totals"
WITH .Cells(lnRow + 2, 2)
.Value = "=SUM(B7:B" + ALLT(STR(lnRow-1)) + ")"
WITH .Borders(xlEdgeBottom)
.Weight = xlMedium
.LineStyle = xlDouble
ENDWITH
ENDWITH
*-- Format the body of the report.
lcRange = "A7:B" + ALLTRIM(STR(lnRow+2))
WITH .Range(lcRange)
.Font.Size = RPT_FONTSIZE
.Font.Name = RPT_FONTNAME
ENDWITH
WITH .Range(ALLT(STR(lnRow+2)) + ":" + ALLT(STR(lnRow+2)))
.Font.Bold = .T.
ENDWITH
WITH .Range("B7:B" + ALLT(STR(lnRow+2)))
.NumberFormat = "$#,##0.00_);[Red]($#,##0.00)"
.Font.Name = RPT_FONTNAME
.Font.Size = RPT_FONTSIZE
ENDWITH
*-- Now some column formatting here. Note the use of the Columns()
*-- Collection.
*-- Here's another example of how wierd Excel can be. In order to
*-- have a column automatically size itself properly, you can:
.Columns("A:B").EntireColumn.AutoFit
*-- And that's it
ENDWITH
WAIT CLEAR
=MessageBox("Done")
*-- Setting the visible property to .T. will cause Excel to become
*-- visible and become the foremost application.
loExcel.Visible = .T.
Release loExcel
CLOSE DATA ALL
RETURN
Notice the code at the top of CustRpt.PRG where Excel is started. The whole reason for the rigmarole with first trying GetObject() and then CreateObject() (if GetObject() fails) is to prevent Excel from running multiple times on my machine.
In truth, you will probably want to try doing this with other servers as well. Therefore, it behooves us to abstract that code into a generic routine. GetCOMInstance.PRG is a program that does just that and is shown in Listing 21.5. Review the code and, once again, pay attention to the comments as they tell the story on how to use this handy little utility.
*-- Program....: GETCOMINSTANCE.PRG
*-- Version....: 1.0
*-- Author.....: Menachem Bazian, CPA
*-- Date.......: August 23, 1998
*-- Project....: Using Visual FoxPro 6 Special Edition
*-- Notice.....: Copyright (c) 1998 Menachem Bazian, CPA, All Rights Reserved.
*-- Compiler...: Visual FoxPro 06.00.8141.00 for Windows
*-- Abstract...:
*-- Changes....:
*-- This UDF will try to get an instance of a COM server without
*-- running the application again.
LPARAMETERS tcCOMServerClass, tcDocumentFileName
*-- The parameter tcCOMServerClass is the name of the COM server class to
*-- instantiate. For example, "excel.application". This program
*-- specifically sets OLEOBJECT on (otherwise, why would we
*-- be calling this program?"
*--
*-- If the COM server cannot be instantiated, this program returns NULL.
*-- Step 1 - Make sure we got a parameter.
ASSERT TYPE("tcCOMServerClass") = "C" ;
MESSAGE "You must provide a character parameter to GetCOMInstance."
IF PCOUNT() = 0 OR TYPE("tcCOMServerClass") # "C"
RETURN .NULL.
ENDIF
*-- If we get this far, we can go ahead and attempt to instantiate the
*-- COM server with GetObject(). Trying to instantiate with GETObject
*-- will try to get an instance from an existing instance of the
*-- application.
*--
*-- Note also that this program will instantiate with GetObject on a file
*-- name. tcDocumentFileName has to be passed and the file has to exist.
LOCAL loCOMInstance, lcOldError
lcOldError = ON("ERROR")
*-- See if the document file name has been passed through. If so, try
*-- to create a reference to that file. If not, just create a regular
*-- instance.
*--
*-- Note that only the GetObject and Createobject lines of code are
*-- wrapped in the changes to ON ERROR. That's because the special
*-- error handler is only applicable to those lines of code.
IF PCOUNT() = 2 AND ;
TYPE("tcDocumentFileName") = "C" AND ;
FILE(tcDocumentFileName)
ON ERROR loCOMInstance = .NULL.
loCOMInstance = GetObject(tcDocumentFileName, tcCOMServerClass)
ON ERROR &lcOldError
ELSE
*-- In this case, no documentfile name has been passed through.
*-- Just create a standard COM instance
ON ERROR loCOMInstance = .NULL.
loCOMInstance = GetObject(, tcCOMServerClass)
ON ERROR &lcOldError
IF ISNULL(loCOMInstance)
ON ERROR loCOMInstance = .NULL.
loCOMInstance = CreateObject(tcCOMServerClass)
ON ERROR &lcOldError
ENDIF
ENDIF
RETURN loCOMInstance
So much for Excel. You could write an entire book on Excel but time and space are limited here, so I will leave the rest up to you. Let's move on to an example of Word. You'll notice some marked differences here, but it should be similar enough that you can use the knowledge you have acquired in Excel to help you here.
By the way, I am a big fan of learning by example. In other words, I think the best way to learn how to work with a COM object is to make a task for yourself and work it through. By the time you have a task figured out, you will be well on your way to learning how to make the most out of the object.
In keeping with this philosophy, let's figure out something you can do with Word, dive right into it, and see where it leads you.
The law firm of Boyd, Dewey, Cheatem, and Howe has called you in as a Visual FoxPro and COM expert. They have an accounting system that was written in Visual FoxPro and they want you to write a report. Not any ordinary report, of course. BDCH wants to run a report of all the clients that owe them money and then have the system automatically generate letters to go to the customers. They provide you with a sample Word document named Boyd.doc and leave you to your own devices (see Figure 21.7).
Figure 21.7 : The Boyd.doc document displayed in Microsoft Word.
Not being too well informed about how to automatically generate these letters in Word, but having read Using Visual FoxPro 6 Special Edition, I have a perfect plan. I will type in the sample letter as a macro and see what is generated.
The macro that is generated is shown in Listing 21.6.
Sub Macro3()
'
' Macro3 Macro
' Macro recorded 08/23/98 by Menachem Bazian, CPA
'
Documents.Add Template:="Normal", NewTemplate:=False
Windows.Arrange
Selection.TypeText Text:="Boyd, Dewey, Cheatem, and Howe"
Selection.HomeKey Unit:=wdLine, Extend:=wdExtend
Selection.Font.Bold = wdToggle
Selection.ParagraphFormat.Alignment = wdAlignParagraphCenter
Selection.EndKey Unit:=wdLine
Selection.TypeParagraph
Selection.TypeText Text:="Attorneys at Out Law"
Selection.TypeParagraph
Selection.TypeText Text:="111 Sewer Avenue"
Selection.TypeParagraph
Selection.TypeText Text:="Ratville City, NJ 0700"
Selection.TypeParagraph
Selection.Font.Bold = wdToggle
Selection.ParagraphFormat.Alignment = wdAlignParagraphLeft
Selection.TypeParagraph
Selection.InsertDateTime DateTimeFormat:="MMMM d, yyyy", InsertAsField:= _
False
Windows("Boyd.doc").Activate
ActiveWindow.ActivePane.SmallScroll Down:=7
Windows("Document7").Activate
Selection.MoveDown Unit:=wdLine, Count:=1
Selection.TypeParagraph
Selection.TypeParagraph
Selection.TypeText Text:="Joe PayShlepper"
Selection.TypeParagraph
Selection.TypeText Text:="521 DeadBroke Avenue"
Selection.TypeParagraph
Selection.TypeText Text:="Ratville City, NJ 07001"
Selection.TypeParagraph
Selection.TypeParagraph
Selection.TypeText Text:="Dear Joe,"
Selection.TypeParagraph
Selection.TypeParagraph
Selection.TypeText Text:= _
"Our records show that you owe us a lot of money. Here's the "
Selection.TypeText Text:= _
"breakdown you deadbeat. Pay up or we will have our friendj G the "
Selection.TypeText Text:="legbreaker"
Selection.MoveLeft Unit:=wdCharacter, Count:=15
Selection.MoveLeft Unit:=wdCharacter, Count:=3, Extend:=wdExtend
Selection.Delete Unit:=wdCharacter, Count:=3
Selection.EndKey Unit:=wdLine
Selection.TypeText Text:=" visit you and break your kneecaps."
Selection.TypeParagraph
Selection.TypeParagraph
Windows("Boyd.doc").Activate
ActiveWindow.ActivePane.SmallScroll Down:=8
Windows("Document7").Activate
Selection.TypeText Text:="Get the idea?"
Selection.TypeParagraph
Selection.TypeParagraph
Selection.TypeText Text:="Very sincerely yours,"
Selection.TypeParagraph
Selection.TypeParagraph
Selection.TypeText Text:="Boyd, Dewey, Cheatem, and Howe"
Selection.TypeParagraph
Selection.TypeParagraph
ActiveDocument.Tables.Add Range:=Selection.Range, NumRows:=2, NumColumns:= _
3
Selection.TypeText Text:="Invoice #"
Selection.MoveRight Unit:=wdCell
Selection.TypeText Text:="Invoice Date"
Selection.MoveRight Unit:=wdCell
Selection.TypeText Text:="Amount Due"
Selection.MoveRight Unit:=wdCell
Windows("Boyd.doc").Activate
ActiveWindow.ActivePane.SmallScroll Down:=6
Windows("Document7").Activate
Selection.TypeText Text:="100"
Selection.MoveRight Unit:=wdCell
Selection.TypeText Text:="1/1/98"
Selection.MoveRight Unit:=wdCell
Selection.TypeText Text:="100"
Selection.MoveRight Unit:=wdCell
Selection.TypeText Text:="110"
Selection.MoveRight Unit:=wdCell
Selection.TypeText Text:="2/1/98"
Selection.MoveRight Unit:=wdCell
Selection.TypeText Text:="400"
Selection.MoveRight Unit:=wdCell
Selection.TypeText Text:="135"
Selection.MoveRight Unit:=wdCell
Selection.TypeText Text:="3/1/98"
Selection.MoveRight Unit:=wdCell
Selection.TypeText Text:="900"
Selection.MoveRight Unit:=wdCell
Selection.Font.Bold = wdToggle
Selection.TypeText Text:="Total Due"
Selection.MoveRight Unit:=wdCell
Selection.MoveRight Unit:=wdCell
Selection.Paste
Selection.MoveUp Unit:=wdLine, Count:=1
Selection.HomeKey Unit:=wdLine
Selection.MoveDown Unit:=wdLine, Count:=3, Extend:=wdExtend
Selection.MoveLeft Unit:=wdCharacter, Count:=1, Extend:=wdExtend
Selection.Delete Unit:=wdCharacter, Count:=1
Selection.Font.Bold = wdToggle
Selection.TypeText Text:="1,400"
Selection.MoveUp Unit:=wdLine, Count:=3
Selection.HomeKey Unit:=wdLine
Selection.MoveDown Unit:=wdLine, Count:=3, Extend:=wdExtend
Selection.ParagraphFormat.Alignment = wdAlignParagraphRight
End Sub
One of the immediate problems in looking at this macro code is that it is in a different format than the Excel code. For instance, take the following line of code:
Selection.TypeText Text:="Total Due"
What in blazes is that? Well, here's where the help file comes in. A quick trip to the WordBasic help file shows that there are two syntaxes (in other words, you can call the same method or function in two different ways). In one, you can use named arguments as you see above. However, you can also specify the arguments as standard parameters. So, I could easily use the following syntax:
Selection.TypeText("Total Due")
What's the difference? Well, when you use named arguments, you do not have to worry about the order of parameters. It's no big deal, but it does mean that you have more work to do in getting this macro to work than you did in Excel.
| NOTE |
For the record, the commands I have used from generated code seem to include the names arguments in the order they need to appear as parameters. I don't count on that, though, and always check. |
The next obvious thing that comes out is the selection object. In Excel, where it's easy to discern one piece of data from another, you have a cell object. Selection is more appropriate to a word processor and deals with the selected text. If you have no selected text, it deals with the location of the cursor.
Now you can start taking the macro apart and turning it into COM instructions for WinWord.
Before you do that, though, I need to confess something to you. If you look at the generated code, you will see oddities. Take this code, for example:
Selection.TypeText Text:= _
"breakdown you deadbeat. Pay up or we will have our friendj G the "
Selection.TypeText Text:="legbreaker"
Selection.MoveLeft Unit:=wdCharacter, Count:=15
Selection.MoveLeft Unit:=wdCharacter, Count:=3, Extend:=wdExtend
Selection.Delete Unit:=wdCharacter, Count:=3
Selection.EndKey Unit:=wdLine
Selection.TypeText Text:=" visit you and break your kneecaps."
This is a case of a typo being created and corrected. See the phrase friendj G? The original letter just said friend, so I had to go back and fix the accidental typing. I could have edited the macro prior to including it in this chapter, but I wanted you to see this. A secretary, I'm not. Although I type fast, I make a number of mistakes (this is one author who is grateful for spell check). If you're like me, plenty of this stuff happens when you're recording macros, and you'll need to correct it later, so you might as well get used to the idea up front.
After working with the help file to get the alternative syntax and modifying the code in the macro to make it legible to Visual FoxPro, I came up with the code shown in Listing 21.7.
*-- Program....: WORD1.PRG
*-- Version....: 1.0
*-- Author.....: Menachem Bazian, CPA
*-- Date.......: August 23, 1998
*-- Project....: Using Visual FoxPro 6 Special Edition
*-- Notice.....: Copyright (c) 1998 Menachem Bazian, CPA, All Rights Reserved.
*-- Compiler...: Visual FoxPro 06.00.8141.00 for Windows
*-- Abstract...:
*-- Changes....:
*-- Word macro modified for Visual FoxPro
#INCLUDE msWord.h
#DEFINE True .T.
#DEFINE False .F.
*-- Step 1 -- get a reference to MS Word
LOCAL loWord, loTable, lnRow, lnColumn
loWord = GetCOMInstance("word.application")
loWord.Visible = .t.
*-- Now, build the letter
WITH loWord
.Documents.Add("Normal", False)
WITH .Selection
.TypeText("Boyd, Dewey, Cheatem, and Howe")
.HomeKey(wdLine,wdExtend)
.Font.Bold = wdToggle
.ParagraphFormat.Alignment = wdAlignParagraphCenter
.EndKey(wdLine)
.TypeParagraph
.TypeText("Attorneys at Out Law")
.TypeParagraph
.TypeText("111 Sewer Avenue")
.TypeParagraph
.TypeText("Ratville City, NJ 0700")
.TypeParagraph
.Font.Bold = wdToggle
.ParagraphFormat.Alignment = wdAlignParagraphLeft
.TypeParagraph
.InsertDateTime("MMMM d, yyyy", False)
* .MoveDown(wdLine, 1)
.TypeParagraph
.TypeParagraph
.TypeText("Joe PayShlepper")
.TypeParagraph
.TypeText("521 DeadBroke Avenue")
.TypeParagraph
.TypeText("Ratville City, NJ 07001")
.TypeParagraph
.TypeParagraph
.TypeText("Dear Joe,")
.TypeParagraph
.TypeParagraph
*-- Up until this point, all I have really done is reformat the code
*-- from the macro (with deleting some extraneous commands that we
*-- didn't need,such as commands generated by my switching windows
*-- while originally generating the macro). Now, I have our first real
*-- modification to the macro code. Instead of many TypeText calls for
*-- the one paragraph,I changed it to one call.
*--
*-- As a general rule, the more calls you can eliminate and combine,
*-- the faster your execution speed will be.
lcText = "Our records show that you owe us a lot of money. Here's the
" + ;
"breakdown you deadbeat. Pay up or we will have our friend the" +
;
" legbreaker visit you and break your kneecaps."
.TypeText(lcText)
.TypeParagraph
.TypeParagraph
.TypeText("Get the idea?")
.TypeParagraph
.TypeParagraph
.TypeText("Very sincerely yours,")
.TypeParagraph
.TypeParagraph
.TypeText("Boyd, Dewey, Cheatem, and Howe")
.TypeParagraph
.TypeParagraph
ENDWITH
*-- Now to the next major modification. In the original macro, the table
*-- was created by inserting a 2 X 2 table and then "typing" the text.
*-- A table, however, is an object. Using the Table object, you can more
*-- directly populate and manipulate the table.
*--
*-- Why do I care about the table object? I did not, after all, bother to
*-- look for a more elegant way to do the body of the letter. Well, the
*-- is obvious when you look at the code. By using the table object, I can
*-- the code to populate the table generic. When this program is modified
*-- again to make it fully generic (and get the information from the
*-- firm's customer and transaction tables), the infrastructure exists.
*--
*-- Oh, by the way. Look at the code to work with the cells in the table.
*-- Doesn't it seem similar to Excel?
.ActiveDocument.Tables.Add(.Selection.Range, 5, 3)
loTable = .ActiveDocument.Tables(1)
DECLARE laTableData[5,3]
laTableData[1,1] = "Invoice #"
laTableData[1,2] = "Invoice Date"
laTableData[1,3] = "Amount Due"
laTableData[2,1] = "100"
laTableData[2,2] = "1/1/98"
laTableData[2,3] = "100"
laTableData[3,1] = "110"
laTableData[3,2] = "2/1/98"
laTableData[3,3] = "400"
laTableData[4,1] = "135"
laTableData[4,2] = "3/1/98"
laTableData[4,3] = "900"
laTableData[5,1] = "Total Amount Due"
laTableData[5,2] = ""
laTableData[5,3] = "1,400"
WITH loTable
FOR lnRow = 1 TO ALEN(laTableData, 1)
FOR lnColumn = 1 TO ALEN(laTableData, 2)
.Cell(lnRow,lnColumn).Range.InsertAfter(laTableData[lnRow,
lnColumn])
ENDFOR
ENDFOR
*-- Our table is populated here. Now, all we have to do is autoformat
*-- it. In case you were wondering.... I cheated... I autoformatted a
*-- table, captured it in a macro, and then converted the macro code
*-- here.
loTable.AutoFormat(wdTableFormatClassic4, ;
True, ;
True, ;
True, ;
True, ;
True, ;
True, ;
False, ;
False, ;
True)
ENDWITH
*-- You need a page break between letters...
.Selection.InsertBreak(wdPageBreak)
ENDWITH
*-- And that, as they say, is that.
RETURN
A closer look at the code shows that only one section has been radically modified from the original code generated by WinWord. That's the section that generates the table. The changes, and the reasons for them, are documented in the code.
You're still not done, but the rest is the proverbial piece of cake. You now have a program that will generate one letter for a specific client, but you need to make it more generic. What can be so difficult about that? After you have this program working, the last step is a simple matter of pulling the information out of the company's tables, replacing the explicit text with fields or variables, and that should do it.
To illustrate this, look at word2.prg, which assumes that you have two tables, Customers and Invoices. These two tables are used to generate the letters for the clients.
The structures of Customers and Invoices and their contents are shown in Listing 21.8.
Structure for table: CUSTOMERS.DBF
Number of data records: 3
Date of last update: 08/24/98
Code Page: 1252
Field Field Name Type Width Dec Index Collate Nulls
1 CCUSTNO Character 10 No
2 CORGNAME Character 35 No
3 CCONTACT Character 35 No
4 CSALUT Character 10 No
5 CADD1 Character 30 No
6 CADD2 Character 30 No
7 CCITY Character 15 No
8 CSTATE Character 2 No
9 CZIP Character 10 No
** Total ** 178
Record #: 1
Ccustno 1
Corgname PayShlepper Enterprises
Ccontact Joe PayShlepper
Csalut Joe
Cadd1 521 DeadBroke Avenue
Cadd2
Ccity RatVille City
Cstate NJ
Czip 07001
Ccustno 2
Corgname Owealot Industries
Ccontact G. I. Owealot
Csalut G. I.
Cadd1 100 Owealot Way
Cadd2 Penthouse Suite
Ccity New York
Cstate NY
Czip 10111
Ccustno 3
Corgname PayNot Garments
Ccontact D. Ed Beat
Csalut Ed
Cadd1 1022 WontPay Drive
Cadd2
Ccity Moscow
Cstate NY
Czip 10000
Structure for table: INVOICES.DBF
Number of data records: 9
Date of last update: 08/24/98
Code Page: 1252
Field Field Name Type Width Dec Index Collate Nulls
1 CCUSTNO Character 10 Asc Machine No
2 CINVNO Character 10 No
3 DINVDATE Date 8 No
4 NAMOUNT Numeric 10 2 No
** Total ** 39
Record #: 1
Ccustno 1
Cinvno 100
Dinvdate 01/01/98
Namount 100.00
Ccustno 1
Cinvno 110
Dinvdate 02/01/98
Namount 400.00
Ccustno 1
Cinvno 135
Dinvdate 03/01/98
Namount 900.00
Ccustno 2
Cinvno 101
Dinvdate 01/01/98
Namount 1600.00
Ccustno 2
Cinvno 115
Dinvdate 02/15/98
Namount 2235.77
Ccustno 2
Cinvno 146
Dinvdate 03/01/98
Namount 2200.00
Ccustno 2
Cinvno 165
Dinvdate 04/01/98
Namount 500.00
Ccustno 2
Cinvno 199
Dinvdate 05/01/98
Namount 2722.00
Ccustno 3
Cinvno 111
Dinvdate 02/01/98
Namount 2233.98
Listing 21.9 contains the code for Word2.prg.
*-- Program....: WORD2.PRG
*-- Version....: 1.0
*-- Author.....: Menachem Bazian, CPA
*-- Date.......: August 24, 1998
*-- Project....: Using Visual FoxPro 6 Special Edition
*-- Notice.....: Copyright (c) 1998 Menachem Bazian, CPA, All Rights Reserved.
*-- Compiler...: Visual FoxPro 06.00.8141.00 for Windows
*-- Abstract...:
*-- Changes....:
*-- This is Word1.PRG modified to get the data from the firm's tables
*-- For all intents and purposes, the strategy of Word1.prg remains intact.
*-- However, some key modifications are present in this program:
*--
*-- 1. The functionality for building a paragraph has been moved into its
*-- own procedure.
*--
*-- 2. The mechanism for generating the letter has been moved into its own
*-- procedure.
*--
*-- 3. Certain additional functionality is required because we are dealing with
*-- data. The two key modifications is moving the addition of the page break
*-- to the beginning of the letter for a customer (if this is not the first
*-- customer letter) and passing the record number so we can get access to
*-- the right table object for a customer.
*--
*-- Finally, most of the comments from Word1.PRG that related to the upgrade of
*-- the code from VBA to VFP has been removed.
*-- Word macro modified for Visual FoxPro
#INCLUDE msWord.h
#DEFINE True .T.
#DEFINE False .F.
*-- Open the tables. We are assuming, for the purposes of this example,
*-- that the tables are the result of processing and represents the information
*-- be presented in the letters (i.e., I will not do any processing on the
*-- data other than what is needed to populate the Word letters).
*-- Step 1 -- get a reference to MS Word
LOCAL loWord, loTable, lnRow, lnColumn
loWord = GetCOMInstance("word.application")
loWord.Visible = .t.
loWord.Documents.Add
loWord.DisplayAlerts = .F.
*-- In order to make the code a little easier to modify and work with,
*-- I am moving the code that actually generates the letter to a procedure.
*--
*-- I will be sendind the procedure an object with the customer information
*-- and an array with the invoice information. As you will see, it will make
*-- some of the letter processing even easier.
CLOSE DATA ALL
USE customers
USE invoices IN 0
SELECT customers
SCAN
SCATTER NAME oCust
SELECT invoices.cInvNo, ;
DTOC(invoices.dInvDate), ;
invoices.nAmount ;
FROM invoices ;
WHERE invoices.cCustNo = oCust.cCustNo ;
ORDER BY 1 ;
INTO ARRAY laInvoices
DO genletter WITH loWord, oCust, laInvoices, RECNO()
ENDSCAN
*------------------------------------------------------
* Procedure...: GenLetter
* Called by...: Word2
*
* Abstract....: Actually generates the word letter
*
* Parameters..: toWord - A reference to the word COM server.
* : toCust - The customer information
* : taInvoices - Array of open invoices for this customer
* : tnRecno - Customer record number
*
* Notes.......:
*------------------------------------------------------
PROCEDURE genletter(toWord, toCust, taInvoices, tnRecno)
*-- Now, build the letter
WITH toWord
WITH .Selection
IF tnRecno > 1
*-- You need a page break between letters... Only add the
*-- page break starting with the second letter.
.InsertBreak(wdPageBreak)
ENDIF
*-- Note that all text typing and paragraph mark functionality
*-- has been moved to AddParagraph. This saves on the code
*-- duplication.
.Font.Bold = .T.
.ParagraphFormat.Alignment = wdAlignParagraphCenter
AddParagraph(toWord, "Boyd, Dewey, Cheatem, and Howe")
AddParagraph(toWord, "Attorneys at Out Law")
AddParagraph(toWord, "111 Sewer Avenue")
AddParagraph(toWord, "Ratville City, NJ 0700")
.Font.Bold = .F.
.ParagraphFormat.Alignment = wdAlignParagraphLeft
AddParagraph(toWord)
.InsertDateTime("MMMM d, yyyy", False)
AddParagraph(toWord,"",2)
*-- Insert the customer information
IF !EMPTY(toCust.cContact)
AddParagraph(toWord, ALLTRIM(toCust.cContact))
ENDIF
IF !EMPTY(toCust.cOrgName)
AddParagraph(toWord, ALLTRIM(toCust.cOrgName))
ENDIF
IF !EMPTY(toCust.cAdd1)
AddParagraph(toWord, ALLTRIM(toCust.cAdd1))
ENDIF
IF !EMPTY(toCust.cAdd2)
AddParagraph(toWord, ALLTRIM(toCust.cAdd2))
ENDIF
LOCAL lcCSZ
lcCSZ = ALLTRIM(toCust.cCity) + ", " + ;
toCust.cState + " " + ALLT(toCust.cZIP)
IF !EMPTY(lcCSZ)
AddParagraph(toWord, ALLTRIM(lcCSZ))
ENDIF
AddParagraph(toWord)
AddParagraph(toWord, "Dear " + ALLTRIM(toCust.cSalut) + ",", 2)
lcText = "Our records show that you owe us a lot of money. Here's
the " + ;
"breakdown you deadbeat. Pay up or we will have our friend
the" + ;
" legbreaker visit you and break your kneecaps."
AddParagraph(toWord, lcText, 2)
AddParagraph(toWord, "Get the idea?", 2)
AddParagraph(toWord, "Very sincerely yours,", 2)
AddParagraph(toWord, "Boyd, Dewey, Cheatem, and Howe", 2)
ENDWITH
*-- Note how the table size is now based on the size of taInvoices...
LOCAL lnRows, lnCols, lnCounter, lnCount2, lnRow, loTable, lnTotal
lnRows = ALEN(taInvoices, 1)
lnCols = ALEN(taInvoices, 2)
.ActiveDocument.Tables.Add(.Selection.Range, lnRows + 2, lnCols)
*-- Each customer has one table... The record number of the customer
*-- will get us the right table in this case.
loTable = .ActiveDocument.Tables(tnRecno)
DECLARE laTableData[lnRows + 2, lnCols]
laTableData[1,1] = "Invoice #"
laTableData[1,2] = "Invoice Date"
laTableData[1,3] = "Amount Due"
FOR lnCounter = 1 TO lnRows
FOR lnCount2 = 1 TO lnCols
laTableData[lnCounter + 1, lnCount2] = taInvoices[lnCounter,
lnCOunt2]
ENDFOR
ENDFOR
*-- Now the totals line
laTableData[lnRows + 2, 1] = "Total Amount Due"
laTableData[lnRows + 2, 2] = ""
*-- Do a quick sum here
lnTotal = 0
FOR lnCOunter = 1 TO ALEN(taInvoices, 1)
lnTotal = taInvoices[lnCounter, 3] + lnTotal
ENDFOR
laTableData[lnRows + 2, 3] = lnTotal
WITH loTable
FOR lnRow = 1 TO ALEN(laTableData, 1)
FOR lnColumn = 1 TO ALEN(laTableData, 2)
*-- Processing is slightly different for the third
*-- column. If we have a number, we want the number
*-- formatted properly.
*--
*-- Also, we want it right aligned.
IF lnColumn = 3
IF TYPE("laTableData[lnRow, lnColumn]") = "N"
.Cell(lnRow,lnColumn).Range.InsertAfter( ;
TRANSFORM(laTableData[lnRow, lnColumn],
"999,999.99"))
ELSE
.Cell(lnRow,lnColumn).Range.InsertAfter( ;
laTableData[lnRow, lnColumn])
ENDIF
.Cell(lnRow,lnColumn).Range.ParagraphFormat.Alignment
= ;
wdAlignParagraphRight
ELSE
.Cell(lnRow,lnColumn).Range.InsertAfter( ;
laTableData[lnRow, lnColumn])
ENDIF
ENDFOR
ENDFOR
*-- Our table is populated here. Now, all we have to do is
*-- autoformat it. In case you were wondering.... I cheated... I
*-- autoformatted a table, captured it in a macro and then
*-- converted the macro code here.
.AutoFormat(wdTableFormatClassic4, ;
True, ;
True, ;
True, ;
True, ;
True, ;
True, ;
False, ;
False, ;
True)
ENDWITH
*-- Move to the end of the document.
.Selection.EndKey(wdStory)
ENDWITH
*-- And that, as they say, is that.
RETURN
ENDPROC &&* GenLetter
*------------------------------------------------------
* Procedure...: AddParagraph
* Called by...: GenLetter
*
* Abstract....: Adds a paragraph
*
* Parameters..:
*
* Notes.......:
*------------------------------------------------------
PROCEDURE AddParagraph(toWord, tcText, tnParagraphs)
LOCAL lnCounter
WITH toWord.Selection
IF PCOUNT() >= 2
.TypeText(tcText)
ENDIF
IF PCOUNT() < 3 OR TYPE("tnParagraphs") # "N"
tnParagraphs = 1
ENDIF
FOR lnCounter = 1 TO tnParagraphs
.TypeParagraph
ENDFOR
ENDWITH
RETURN
ENDPROC &&* AddParagraph
I am sure that there are those who might look at the way in which these letters were generated and opine that there are better ways to do the task. Subjects like templates, running macros within Word, and more might come up.
The point behind this material was not to show you exactly how to use Word in your applications, but to take you through the learning process. The process whereby you learn how to use Word and Excel is more important than anything that can be documented within one chapter of a book.
© Copyright, Sams Publishing. All rights reserved.