Delphi/MSWord Automation FAQ (Views: 30)
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 |