2011. június 10., péntek

Delphi/MSWord Automation FAQ

Problem/Question/Abstract:

This document provides answers to some basic OLE Automation questions regarding Delphi (3 or 4) and Microsoft Word (8.0). The concepts outlined here can also be applied to many other MS applications (Excel, Internet Explorer etc) as well as any other application that supports OLE Automation.

Answer:

Setting up Delphi to work with Word

In order for Delphi to access methods and properties exposed by Word (using OLE Automation early binding) the Word type library must be installed. Type libraries provide the definitions for all exposed methods and properties of an Automation Server in a standardized format that can be used by any compliant programming application including Delphi. To use Word's type library in Delphi select the "Import Type Library" from the "Project" menu and choose the file msword8.olb located in Microsoft Office's "Office" directory. This will create the file "Word_TLB.pas" which is the object pascal translation of the type library. The files Office_TLB.pas and VBIDE_TLB.pas will also be created since the Word type library references these type libraries. These files should be saved in Delphi's "Imports" directory. Now simply include Word_TLB in the uses list of any unit that will be accessing Word properties or methods.

Finding help on Word's interfaces and methods

All exposed functionality for Office applications is documented in the vba*.hlp files located in Microsoft Office's "Office" directory. For help on Word objects refer to the help file vbawrd8.hlp. This file is not installed by default during Office installation so you may have to get it from the Office installation program.

How to open Word using OLE Automation

The CoApplication class defined in the type library represents the implementation of the Word Application interface. Call CoApplication.Create to create an instance of Word. This method will return a pointer to an interface of type _Application. The _Application interface provides a "Documents" interface which provides 2 methods to access documents: Add and Open.

Both these methods return a pointer to a _Document interface. As well these methods take parameters that are of type OLEVariant. Many parameters passed to Word methods are defined as "optional". Optional parameters must be included in calls to methods but can be defined as Unassigned to indicate that they are not being used. Delphi 4 provides a variable which can be used for optional parameters that are not being used called EmptyParam.

Sample Code

uses
Word_TLB;

procedure StartWord(var WordApp: _Application; var WordDoc: _Document);
var
SaveChanges: OleVariant;
begin
try
WordApp := CoApplication.Create;
WordDoc := WordApp.Documents.Add(EmptyParam, EmptyParam);
WordApp.Visible := True;
except
if Assigned(WordApp) then
begin
SaveChanges := wdDoNotSaveChanges;
WordApp.Quit(SaveChanges, EmptyParam, EmptyParam);
end;
end;

How to connect to a running copy of Word

To connect to a running instance of Word use the Delphi command GetActiveOleObject. This will return an IDispatch variable which points to the Word Application. You can then query the return object using QueryInterface to get the pointer to the _Application object. GetActiveOleObject will raise an exception if an instance of the object does not exist in the Running Object Table (ROT) so make sure to wrap the call in a try..except block.

Sample Code

uses
Word_TLB;

procedure StartWord(var WordApp: _Application);
var
SaveChanges: OleVariant;
begin
try
GetActiveOleObject('Word.Application').QueryInterface(_Application, WordApp);
except
WordApp := nil;
end;

if Unassigned(WordApp) then
begin
try
WordApp := CoApplication.Create;
WordApp.Visible := True;
except
if Assigned(WordApp) then
begin
SaveChanges := wdDoNotSaveChanges;
WordApp.Quit(SaveChanges, EmptyParam, EmptyParam);
end;
end;
end;
end;

Getting data from Word

The Word Document object supports the IDataObject Interface. To get data from Word (RTF, text, structured storage etc) the IDataObject must be used. To get a pointer to the IDataObject Interface use QueryInterface. Word documents support the standard formats CF_TEXT and CF_METAFILEPICT as well as a number of other specific formats including RTF and structured storage. For the standard formats the constant values can be used for the value of cfFormat, but for the other formats the Document must be queried using the function EnumFormatEtc. This function will return a list of supported formats. The required format from this list is then passed to the GetData function of the IDataObject interface. It is important to note that the value of cfFormat for the proprietary formats (RTF etc.) is not constant between machines so it must always be found using EnumFormatEtc and not hard coded. For more information on IDataObject and its methods refer to the Win32 programming help files (included with Delphi 4, C++Builder, Visual C++ etc.).

Sample Code

uses
Word_TLB;

function GetRTFFormat(DataObject: IDataObject; var RTFFormat: TFormatEtc): Boolean;
var
Formats: IEnumFORMATETC;
TempFormat: TFormatEtc;
cfRTF: LongWord;
Found: Boolean;
begin
try
OleCheck(DataObject.EnumFormatEtc(DATADIR_GET, Formats));
cfRTF := RegisterClipboardFormat('Rich Text Format');
Found := False;
while (not Found) and (Formats.Next(1, TempFormat, nil) = S_OK) do
if (TempFormat.cfFormat = cfRTF) then
begin
RTFFormat := TempFormat;
Found := True;
end;
Result := Found;
except
Result := False;
end;
end;

procedure GetRTF(WordDoc: _Document);
var
DataObject: IDataObject;
RTFFormat: TFormatEtc;
ReturnData: TStgMedium;
Buffer: PChar;
begin
if Assigned(WordDoc) then
begin
try
WordDoc.QueryInterface(IDataObject, DataObject);
if GetRTFFormat(DataObject, RTFFormat) then
begin
OleCheck(DataObject.GetData(RTFFormat, ReturnData));
// RTF is passed through global memory
Buffer := GlobalLock(ReturnData.hglobal);

{ Buffer is a pointer to the RTF text
Insert code here to handle the RTF text (ie. save it, display it etc.) }
GlobalUnlock(ReturnData.hglobal);
end;
except
ShowMessage('Error while getting RTF');
end;
end;
end;

Event Sinking with Word

There are 2 ways that event sinking can be performed on Word:

1.   Using the IAdviseSink interface

To use the IAdviseSink interface you must first write an object that implements this standard interface. This object is then passed to the DAdvise method of a Word Document's IDataObject interface or to the Advise method of a Word Document's IOleObject interface. Refer to the help MS help on IAdviseSink for more information on this interface.

2.   Using ConnectionPoints

Word provides the following event sources that can be sinked to:

ApplicationEvents:

procedure Startup; dispid 1;
procedure Quit; dispid 2;
procedure DocumentChange; dispid 3;

DocumentEvents:

procedure New; dispid 4;
procedure Open; dispid 5;
procedure Close; dispid 6;

OCXEvents:

procedure GotFocus; dispid -2147417888;
procedure LostFocus; dispid -2147417887;

To start a connection with Word you must get the IConnectionPointContainer for the Word application or document (depending what events you want to sink to). Next query the IConnectionPointContainer for the IConnectionPoint that you wish to use (ApplicationEvents, DocumentEvents or OCXEvents in this case). Once you have the IConnectionPoint use the Advise method to establish the connection.

There appears to be some limitations with Word's implementation of connection points. When a document is closed in Word, without closing Word itself, Word sends a DocumentEvents.Close message and then an ApplicationEvents.DocumentChange message. Then when Word is closed nothing is sent. On the other hand if Word is closed with an open document then it sends a DocumentEvents.Close message and an ApplicationEvents.Quit message. Another problem is that Word will send the DocumentEvents.Close message when the user "closes" the document but before the "Do you wish to save changes?" dialog is shown. So if the user then selects cancel the document is never closed but the DocumentEvents.Close message was sent.

Sample Code (StartingConnection)

uses
Word_TLB, activex, comobj, ConnectionObject

// ConnectionObject is the unit containing TWordConnection

procedure StartWordConnection(WordApp: _Application;
WordDoc: _Document;
var WordSink: TWordConnection);
var
PointContainer: IConnectionPointContainer;
Point: IConnectionPoint;
begin
try
{ TWordConnection is the COM object which receives the
notifications from Word. Make sure to free WordSink when
you are done with it. }
WordSink := TWordConnection.Create;
WordSink.WordApp := WordApp;
WordSink.WordDoc := WordDoc;

// Sink with a Word application
OleCheck(WordApp.QueryInterface(IConnectionPointContainer, PointContainer));
if Assigned(PointContainer) then
begin
OleCheck(PointContainer.FindConnectionPoint(ApplicationEvents, Point));
if Assigned(Point) then
Point.Advise((WordSink as IUnknown), WordSink.AppCookie);
end;

// Sink with a Word document
OleCheck(WordDoc.QueryInterface(IConnectionPointContainer, PointContainer));
if Assigned(PointContainer) then
begin
OleCheck(PointContainer.FindConnectionPoint(DocumentEvents, Point));
if Assigned(Point) then
Point.Advise((WordSink as IUnknown), WordSink.DocCookie);
end;
except
on E: Exception do
ShowMessage(E.Message);
end;
end;

Sample Code (Connection Object)

unit ConnectionObject;

interface

uses
Word_TLB;

type
TWordConnection = class(TObject, IUnknown, IDispatch)
protected

{ IUnknown }
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;

{ IDispatch }
function GetIDsOfNames(const IID: TGUID; Names: Pointer;
NameCount, LocaleID: Integer;
DispIDs: Pointer): HResult; stdcall;
function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; stdcall;
function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
Flags: Word; var Params;
VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;

public
WordApp: _Application;
WordDoc: _Document;
AppCookie, DocCookie: Integer;
end;

implementation

uses
Windows, ActiveX, Main;

procedure LogComment(comment: string);
begin
Form1.Memo1.Lines.Add(comment);
end;

{ IUnknown Methods }

function TWordConnection._AddRef: Integer;
begin
Result := 2;
end;

function TWordConnection._Release: Integer;
begin
Result := 1;
end;

function TWordConnection.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
Result := E_NOINTERFACE;
Pointer(Obj) := nil;
if GetInterface(IID, Obj) then
Result := S_OK;
if (not Succeeded(Result)) then
if IsEqualIID(IID, DocumentEvents) or IsEqualIID(IID, ApplicationEvents) then
if GetInterface(IDispatch, Obj) then
Result := S_OK;
end;

{ IDispatch Methods }

function TWordConnection.GetIDsOfNames(const IID: TGUID; Names: Pointer;
NameCount, LocaleID: Integer;
DispIDs: Pointer): HResult;
begin
Result := E_NOTIMPL;
end;

function TWordConnection.GetTypeInfo(Index, LocaleID: Integer;
out TypeInfo): HResult;
begin
Pointer(TypeInfo) := nil;
Result := E_NOTIMPL;
end;

function TWordConnection.GetTypeInfoCount(out Count: Integer): HResult;
begin
Count := 0;
Result := E_NOTIMPL;
end;

function TWordConnection.Invoke(DispID: Integer; const IID: TGUID;
LocaleID: Integer; Flags: Word;
var Params; VarResult, ExcepInfo,
ArgErr: Pointer): HResult;
begin
// This is the entry point for Word event sinking
Result := S_OK;
case DispID of
1: ; // Startup
2: ; // Quit
3: ; // Document change
4: ; // New document
5: ; // Open document
6: ; // Close document
else
Result := E_INVALIDARG;
end;
end;

end.

Call Delphi from Word (VBA)

Make your Delphi application an OLE Automation server (TAutoObject). File..New..ActiveX..Automation Object. Define your interface(s) and write the methods that you wish to call from Word.
In VBA add your Delphi exe to the project. Tools..References. You should now be able to use the VBA Object Browser to (F2) to browse your Delphi functions.
Code a VBA procedure to call Delphi.

Sample Code

Sub foo
' AutoServer is the name of the class
' in the object browser
Dim MyServer as AutoServer

System.Cursor = wdCursorWait

set MyServer = new AutoServer
Call MyServer.DelphiFoo(p1, p2)

System.Cursor = wdCursorNormal
end Sub



Nincsenek megjegyzések:

Megjegyzés küldése