Delphi and Word Part II

By: Charles Calvert

Abstract: This article shows how to use interfaces to access Word, Excell and the Internet Explorer from Delphi

(a)17

Chapter 17

Interfaces and Internet Explorer

Comparing Variants and Interfaces

Files Needed in This Chapter

Working with Word and Excel

Working with IE and TWebBrowser

Placing an ActiveX Control Within a Browser

This chapter is about how Delphi can interact with some of the key programs and controls on a typically well-equipped Windows 98 or Windows NT system. While reading the chapter, you will hear a lot about accessing automation objects via interfaces. In a sense, this chapter is a continuation of the preceding chapter, except that this time the focus is on calling objects via custom interfaces rather than calling them via a Variant and IDispatch.

The text begins with a brief overview of COM and interfaces. I already covered COM basics earlier in the book, so I am just going to add a few more comments specific to working with interfaces and automation objects.

After getting the theoretical issues out of the way, I'll show an example from the preceding chapter that is rewritten to use interfaces rather than Variants. After that, I'll show an example of manipulating an ActiveX control via an interface, and finally, I'll divulge the core subject matter of the chapter with several fairly involved examples of using interfaces to manipulate Internet Explorer. As a final bonus, I'll throw in a simple example showing how to use the Wang Image editor control.

The section on the Explorer shows how to use a Web browser as an ActiveX control that can be hosted in a Delphi application. Because IE itself can host ActiveX controls inside a Web page, the chapter can feature some fairly interesting code in which Delphi is seen manipulating both Internet Explorer and a series of controls hosted by the Explorer.

By the end of the chapter, you should have a fairly good sense of how to use interfaces to control objects found on your system or imported to your system. This field of endeavor represents an extremely important aspect of contemporary programming and one that is likely to become increasingly valuable as the body of available COM-based work grows.

(c)Comparing Variants and Interfaces

As you know, when you use a Variant to access an automation class, what is really happening is that Delphi is calling the methods of your objects via an interface called IDispatch.

When using IDispatch to access a method, a programmer either explicitly or implicitly calls GetIDsOfNames, which returns the ID of the method to be called. For instance, if you want to access a method called GetVisible, then you need to find out the ID of that method. To get the ID, you call IDispatch.GetIDsOfNames. After you have the ID, then you call IDispatch.Invoke, passing in the ID. (To see exactly how Delphi calls GetIDsOfNames and Invoke, open COMObj.pas. This file is located in the ..SourceRTLSys directory provided with all versions of Delphi that ship with the source to the RTL.)

When you use Variants, each time you want to call a method, you have to first call GetIDsOfNames and then call Invoke. That's one trip over to the host application, one trip back, and a third trip over again to actually invoke the procedure. That's a lot of overhead to make a simple call. Remember that each trip between applications can be very lengthy when compared to making a call to a method or function inside your address space.

A further problem with the Variant method of calling an automation procedure is that you can't type-check the call at design time. Delphi knows only that it needs to pass some information over to the server using IDispatch. If it can do that, then it gives your app a clean bill of health. It has no way of checking whether you are calling a real function or whether you are passing valid parameters.

(d)Interfaces to the Rescue

So now you are familiar with the problem that needs to be solved. The possible solutions come in two flavors. One is called an interface, and the second is called a dispinterface.

Let's take a look at one interface and one dispinterface:

ISeriesCollection = interface(IDispatch)

['{0002086C-0001-0000-C000-000000000046}']

function Get_Application(out Retval: Application): HResult; stdcall;

function Get_Creator(out Retval: XlCreator): HResult; stdcall;

function Get_Parent(out Retval: IDispatch): HResult; stdcall;

function Add(Source: OleVariant; Rowcol: XlRowCol;

SeriesLabels, CategoryLabels, Replace: OleVariant;

out Retval: Series): HResult; stdcall;

function Get_Count(out Retval: Integer): HResult; stdcall;

function Extend(Source, Rowcol,

CategoryLabels: OleVariant): HResult; stdcall;

function Item(Index: OleVariant; out Retval: Series): HResult; stdcall;

function _NewEnum(out Retval: IUnknown): HResult; stdcall;

function Paste(Rowcol: XlRowCol; SeriesLabels, CategoryLabels, Replace,

NewSeries: OleVariant): HResult; stdcall;

function NewSeries(out Retval: Series): HResult; stdcall;

end;

 

SeriesCollection = dispinterface

['{0002086C-0000-0000-C000-000000000046}']

property Application: Application readonly dispid 148;

property Creator: XlCreator readonly dispid 149;

property Parent: IDispatch readonly dispid 150;

function Add(Source: OleVariant; Rowcol: XlRowCol;

SeriesLabels, CategoryLabels, Replace: OleVariant): Series; dispid 181;

property Count: Integer readonly dispid 118;

procedure Extend(Source, Rowcol, CategoryLabels: OleVariant); dispid 227;

function Item(Index: OleVariant): Series; dispid 170;

function _NewEnum: IUnknown; dispid -4;

procedure Paste(Rowcol: XlRowCol; SeriesLabels, CategoryLabels, Replace,

NewSeries: OleVariant); dispid 211;

function NewSeries: Series; dispid 1117;

end;

As you can see, these two interfaces to a single valid Excel object are nearly identical. But they do have some differences. In particular, note that SeriesCollection supports properties, whereas ISeriesCollection does not.

The problem at hand, as you know, is that the Variant method of calling a method is slow, and it does not allow for type checking. Creating an interface for a COM object helps to solve both problems. First, it gives the Delphi compiler some way to type-check your code. Here is the interface for an object. The implementation is still over on the Excel side, but at least the compiler now has some way of knowing the structure of the object in question and the parameters that its methods take.

Second, if you look closely at the dispinterface, you will see that each function, procedure or property is followed by something called a Dispatch ID. This is the number that is normally retrieved by a call to GetIDsOfNames. If you have a dispinterface, then you can call IDispatch Invoke directly without having to first call GetIDsOfNames. Dispatch IDs save two of the three trips that have to be made each time you call a procedure or function. Dispinterfaces are a huge improvement over the technology involved in a simple automation call against a Variant.

Chapter 13, "Interfaces and the Basics of COM," ends by discussing how an interface allows you to call the VTable of an object directly. If you use VTables, you have a proxy for the object on the client side. Calling the methods of the object is simply a matter of letting the compiler directly dispatch the call without ever having to call Invoke or without having to marshal information back and forth. In other words, Delphi can call the methods of the object using the same speedy technology it uses when calling a standard Delphi object. The actual call still needs to be marshaled between the client and the server, so it is not nearly as fast a regular object call, but all the overhead associated with IDispatch is gone.

Many COM objects have something called a dual interface. A dual interface means that the object supports both IDispatch, and a standard VTable interface. Go back up and look at the declaration of ISeriesCollection. As you can see, it descends directly from IDispatch. In other words, it supports both IDispatch and ISeriesCollection. This means that an application can call its methods using either an interface or a Variant. It has a dual interface! As you have seen in earlier chapters, many of the COM objects you create in Delphi descend from IDispatch and thus support dual interfaces.

(d)Working with Type Libraries

As you know, a type library is a binary file, usually with a .tlb or .olb extension, that contains binary information describing an interface. Quite often, these binary files are appended onto an executable, and sometimes they are kept separate. Wherever a type library is stored, you or the system needs to find it and open it if you want to get to the description of an interface.

Select Project, Import Type Library from the Delphi menu to read a type library and automatically convert the binary information found therein into Object Pascal. For instance, the ISeriesCollection and SeriesCollection interfaces shown previously are from one of these automatically generated files.

Why, you might ask, does Delphi generate an interface for both ISeriesCollection and SeriesCollection? Wouldn't Delphi users always want to use a real interface such as ISeriesCollection rather than a dispinterface? The answer is simply that Delphi creates both interfaces because it can do so. It has enough information to create both interfaces, and so it does create them. In many cases, Delphi only creates dispinterfaces because that is all that Microsoft provides.

This is the end of the section of this chapter dedicated to theoretical issues.

(c)Files Needed in This Chapter

You will need the following programs if you want to run all the examples in this chapter:

[lb] Internet Explorer 4.0 or higher

[lb] Microsoft Word 97 (I'm using Service Release 1)

[lb] Microsoft Excel 97 (I'm using Service Release 1)

If you don't have Word or Excel, you will miss out on part of this chapter, but you can still understand all the main points. My original plan was to write this chapter entirely on Word and Excel, but I decided that I should instead focus on free products that everyone can own, such as Internet Explorer and the controls mentioned in the next few paragraphs of this section.

You need to have the following free controls on your system:

[lb] The TWebBrowser control (comes automatically with IE4). I explain how to import the control in more depth later in the chapter, but if you already understand the mechanisms involed, you can just select Component | Import ActiveX Control from the Delphi menu and import the Microsoft Internet Controls.

[lb] The DirectAnimation control (comes with some copies of IE4). Follow the same general pattern you followed getting the TWebBrowser control but search for the DirectAnimation Library.

[lb] The Wang Image control (comes with all copies of Windows 95 and NT). Import as with DirectAnimation but search on Wang Edit Control. (This control may not ship with all copies of Windows 98 or NT 5.0.)

[lb] The ActiveMovie control comes with some copies of IE 4.0 or can be downloaded from www.microsoft.com. Import the Microsoft Active Movie Control just as you have the other controls in this section.

As you import each of these files, notice that new PAS files are installed in your imports directory.

You need to import the following type libraries. By default, they wind up in the Delphi4Imports directory, which also, by default, should be on your path. You may have to explicitly compile some of these files from the command line using dcc32:

[lb] Excel_TLB.pas: Choose Project, Import Type Library from the Delphi menu and find Excel8.olb in the c:program filesmicrosoft officeoffice directory.

[lb] Word_TLB.pas: Import MSWord8.olb from the same place as the Excel8.olb file.

[lb] Office_tlb: You can find the Microsoft Office entry preregistered in the files you see when you choose Project, Import Type Library from the Delphi menu. Select this file and import its type library.

[lb] VBIDE_TLB.pas: This file is generated automatically when you create Office_TLB.pas.

[lb] MSHTML_TLB.pas: Import this important type library from the MSHTML.dll file in the WindowsSystem or WinntSystem32 directories.

[lb] ShdDocVw_TLB.pas: This file is generated automatically when you import the WebBrowser control.

[lb] DirectAnimation_TLB.pas: This file is generated automatically when you import the DirectAnimation control.

You will find copies of all the headers mentioned here in the MSTypeLibraryHeaders directory on the CD that comes with this book. However, generating all the headers yourself is a good idea, in part because it helps you get used to the technology involved.

(c)Working with Word and Excel

It's time now to take a look at some programming using interfaces. The following program, shown in Listings 17.1 and 17.2, is a translation of the Excel4 program from the preceding chapter, altered so that it uses interfaces rather than variants. The program is called Excel4I, pronounced Excel Four Eye, where the I stands for interface.

Listing 17.1[em]The Excel4I Program Gives Interfaces a Good Workout

//////////////////////////////////////

// Purpose: Use Word and Excel from Delphi

// Project: Excel4I.dpr

// Copyright (c) 1998 by Charlie Calvert

//

unit Main;

 

{------------------------------------------------------------------------------

Creating data and a chart in Excel and copying both to Word.

This example is like Excel4, only it uses interfaces instead

of variants.

------------------------------------------------------------------------------}

interface

 

uses

Windows, Messages, SysUtils,

Classes, Graphics, Controls,

Forms, Dialogs, StdCtrls,

Office_Tlb, Excel_TLB, Word_TLB;

 

type

TForm1 = class(TForm)

RunBtn: TButton;

SendMailBtn: TButton;

procedure RunBtnClick(Sender: TObject);

procedure FormDestroy(Sender: TObject);

procedure SendMailBtnClick(Sender: TObject);

private

XLApp: Excel_TLB.Application_;

WordApp: Word_TLB.Application_;

public

procedure HandleData(WorkSheet: _WorkSheet);

procedure ChartData(var WorkSheets: Sheets);

procedure CopyData;

procedure CopyChartToWord;

procedure CopyCellsToWord;

end;

 

var

Form1: TForm1;

 

implementation

 

uses

ComObj, ActiveX;

 

{$R *.DFM}

 

{------------------------------------------------------------------------------

In Delphi 4 this function is no longer necessary, but

I keep it here for Delphi3 programmers who want to see

how to create a Variant that can be passed as an empty,

or inert, parameter.

------------------------------------------------------------------------------}

function CreateEmptyParam: OleVariant;

begin

TVarData(EmptyParam).VType := VT_ERROR;

TVarData(EmptyParam).VError := DISP_E_PARAMNOTFOUND;

end;

 

procedure TForm1.RunBtnClick(Sender: TObject);

var

WorkSheet: _Worksheet;

WorkBks: WorkBooks;

WorkSheets: Sheets;

Workbk: WorkBook;

begin

XLApp := Excel_TLB.CoApplication_.Create;

//CreateOleObject('Excel.Application') as ExcelTlb.Application;

XLApp.Visible[0] := True;

WorkBks := XLApp.WorkBooks as WorkBooks;

Workbks.Add(XLWBatWorksheet, 0);

WorkBk := WorkBks.Item[1];

WorkSheets := Workbk.WorkSheets;

WorkSheet := WorkSheets.Get_Item(1) as _WorkSheet;

Worksheet.Name := 'Delphi Data';

HandleData(WorkSheet);

ChartData(WorkSheets);

CopyData;

SendMailBtn.Enabled := True;

end;

 

procedure TForm1.HandleData(WorkSheet: _WorkSheet);

var

i: Integer;

begin

for i := 1 to 10 do

WorkSheet.Cells.Item[i, 1] := i;

end;

 

{------------------------------------------------------------------------------

In this method I try to make the following call:

AChart := WorkSheets.Add(EmptyParam, EmptyParam, 1, xlChart, 0) as Chart;

And I get back this apparently undocumented error: $800A03EC. I can't

resolve this one, so I create the object off a Variant:

 

AChart := XLApp.WorkBooks.Item[1].Sheets.Add(EmptyParam,

EmptyParam, 1, xlChart, 0) as Chart;

 

------------------------------------------------------------------------------}

procedure TForm1.ChartData(var WorkSheets: Sheets);

var

ARange: Excel_TLB.Range;

AWorksheet: Worksheet;

AChart: Chart;

Index: OleVariant;

aSeries: Series;

ASeriesCollection: SeriesCollection;

begin

AWorkSheet := WorkSheets.Item['Delphi Data'] as Worksheet;

ARange := AWorksheet.Range['A1', 'A10'];

 

AChart := XLApp.WorkBooks.Item[1].Sheets.Add(EmptyParam,

EmptyParam, 1, xlChart, 0) as Chart;

Index := 1;

ASeries := AChart.SeriesCollection(Index, 0) as Series;

ASeries.Values := ARange;

AChart.ChartType := xl3DPie;

ASeries.HasDataLabels := True;

 

AChart := XLApp.Workbooks.Item[1].Sheets.Add(NULL,

NULL,1,xlChart,0) as Chart;

ASeries := AChart.SeriesCollection(Index, 0) as Series;

ASeries.Values := ARange;

ASeriesCollection := AChart.SeriesCollection(EmptyParam,

0) as SeriesCollection;

ASeriesCollection.NewSeries;

Index := 2;

ASeries := AChart.SeriesCollection(Index, 0) as Series;

ASeries.Values := ARange;

ASeriesCollection.NewSeries;

Index := 3;

ASeries := AChart.SeriesCollection(Index, 0) as Series;

ASeries.Values := VarArrayOf([1,2,3,4,5, 6,7,8,9,10]);

AChart.ChartType := xl3DColumn;

end;

 

{------------------------------------------------------------------------------

I could not copy the chart to the Clipboard using interfaces. The

following line works, but it copies the chart to a new location in

Excel, not to the clipboard:

 

AChart.Copy(EmptyParam, EmptyParam, 0);

 

So I copied the chart using Variants, as shown here:

 

V := AChart.Application_;

V.Selection.Copy;

------------------------------------------------------------------------------}

procedure TForm1.CopyData;

var

ASheets: Sheets;

AChart: Chart;

V: Variant;

AWorksheet: Worksheet;

ARange: Excel_TLB.Range;

begin

SetFocus;

 

ASheets := XLApp.Sheets as Sheets;

 

AWorksheet := ASheets.Item['Delphi Data'] as Worksheet;

AWorksheet.Activate(0);

ARange := AWorksheet.Range['A1', 'A10'];

ARange.Select;

ARange := AWorksheet.UsedRange[0];

ARange.Copy(EmptyParam);

 

CopyCellsToWord;

 

AChart := ASheets.Item['Chart1'] as Chart;

AChart.Activate(0);

AChart.Select(EmptyParam, 0);

V := AChart.Application_;

V.Selection.Copy;

CopyChartToWord;

end;

 

procedure TForm1.CopyChartToWord;

var

ARange, ParRange: Range;

StartRange, EndRange: OleVariant;

NumPars, i: Integer;

Index: OleVariant;

DataType: OleVariant;

Docs: Documents;

Doc: Document;

Pars: Paragraphs;

Par: Paragraph;

begin

Index := 1;

Docs := WordApp.Documents;

Doc := Docs.Item(Index);

Pars := Doc.Paragraphs;

NumPars := Pars.Count;

Par := Pars.Item(NumPars);

ParRange := Par.Range;

StartRange := ParRange.Start;

EndRange := ParRange.End_;

ARange := Doc.Range(StartRange, EndRange) as Range;

ARange.Text := 'This is a graph of the column: ';

 

for i := 1 to 3 do

Pars.Add(EmptyParam);

 

Par := Pars.Item(NumPars + 2);

ParRange := Par.Range;

StartRange := ParRange.Start;

EndRange := ParRange.End_;

ARange := Doc.Range(StartRange, EndRange);

 

DataType := wdPasteOleObject;

ARange.PasteSpecial(EmptyParam, EmptyParam, EmptyParam, EmptyParam,

DataType, EmptyParam, EmptyParam);

end;

 

{------------------------------------------------------------------------------

In some cases, you may gain performance with interfaces.

For instance, the following line:

Pars.Item(3).Range.Start;

Would probably execute just as fast as:

Par := Pars.Item(3);

TempRange := Par.Range;

AStart := TempRange.Start;

------------------------------------------------------------------------------}

procedure TForm1.CopyCellsToWord;

var

ARange, TempRange : Range;

i: Integer;

AStart, Template, OpenAsTemplate: OleVariant;

Docs: Documents;

Doc: Document;

Pars: Paragraphs;

Par: Paragraph;

begin

WordApp := CoApplication_.Create;

WordApp.Visible := True;

 

Template := 'Normal';

OpenAsTemplate := False;

 

Docs := WordApp.Documents;

Doc := Docs.Add(Template, OpenAsTemplate);

ARange := Doc.Range(EmptyParam, EmptyParam);

ARange.Text := 'This is a column from a spreadsheet: ';

 

Pars := Doc.Paragraphs;

for i := 1 to 3 do

Pars.Add(EmptyParam);

 

Par := Pars.Item(3);

TempRange := Par.Range;

AStart := TempRange.Start;

ARange := Doc.Range(AStart, EmptyParam);

ARange.Paste;

 

for i := 1 to 3 do

Pars.Add(EmptyParam);

end;

 

procedure TForm1.FormDestroy(Sender: TObject);

var

Index: OleVariant;

SaveChanges: OleVariant;

Docs: Documents;

Doc: Document;

begin

Index := 1;

SaveChanges := wdDoNotSaveChanges;

if not VarIsEmpty(XLApp) then begin

XLApp.DisplayAlerts[0] := False; // Discard unsaved files....

XLApp.Quit;

end;

 

if not VarIsEmpty(WordApp)then begin

Docs := WordApp.Documents;

Doc := Docs.Item(Index);

Doc.Close(SaveChanges, EmptyParam, EmptyParam);

WordApp.Quit(EmptyParam, EmptyParam, EmptyParam);

end;

end;

 

procedure TForm1.SendMailBtnClick(Sender: TObject);

var

Index: OleVariant;

SaveFile: OleVariant;

begin

SaveFile := 'c:foo.doc';

WordApp.Documents.Item(Index).SaveAs(SaveFile, EmptyParam, EmptyParam,

EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam,

EmptyParam, EmptyParam, EmptyParam);

WordApp.Options.SendMailAttach := True;

WordApp.Documents.Item(Index).SendMail;

end;

 

initialization

 

end.

Listing 17.2[em]The Project Source File for the Excel1I Application, Showing Files from the Imports Directory

program Excel4I;

 

uses

Forms,

Main in 'Main.pas' {Form1},

Excel_TLB in 'Excel_TLB.pas',

Office_TLB in 'Office_TLB.pas',

VBIDE_TLB in 'VBIDE_TLB.pas',

Word_TLB in 'Word_TLB.pas';

 

{$R *.RES}

 

begin

Forms.Application.Initialize;

Forms.Application.CreateForm(TForm1, Form1);

Forms.Application.Run;

end.

This program does all the same things as the Excel4 program from the preceding chapter. In particular, it opens copies of both Word and Excel and then creates a spreadsheet and some graphs in Excel. Finally, it copies both the spreadsheet data and one of the graphs over to Word and gives you the option of emailing the result across the network.

Excel4I is a straight port of the IDispatch-based Excel4 program to an interface-based program. I simply took routines crafted to use Variants and rewrote them to use interfaces. As a rule, this port was very successful: however, I still used IDispatch in one place because I had trouble making the code work correctly using interfaces.


Server Response from: ETNASC04