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.