Author: softmosis.ca
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
1 uses
2 Word_TLB;
3
4 procedure StartWord(var WordApp: _Application; var WordDoc: _Document);
5 var
6 SaveChanges: OleVariant;
7 begin
8 try
9 WordApp := CoApplication.Create;
10 WordDoc := WordApp.Documents.Add(EmptyParam, EmptyParam);
11 WordApp.Visible := True;
12 except
13 if Assigned(WordApp) then
14 begin
15 SaveChanges := wdDoNotSaveChanges;
16 WordApp.Quit(SaveChanges, EmptyParam, EmptyParam);
17 end;
18 end;
19
20 //How to connect to a running copy of Word
21
22 //To connect to a running instance of Word use the Delphi command
23 GetActiveOleObject. This will return an IDispatch variable which points to the Word
24 Application. You can then query the return object using QueryInterface to get the
25 pointer to the _Application object. GetActiveOleObject will raise an exception if
26 an instance of the object does not exist in the Running object Table (ROT) so make
27 sure to wrap the call in a try..except block.
28
29 Sample Code
30
31 uses
32 Word_TLB;
33
34 procedure StartWord(var WordApp: _Application);
35 var
36 SaveChanges: OleVariant;
37 begin
38 try
39 GetActiveOleObject('Word.Application').QueryInterface(_Application, WordApp);
40 except
41 WordApp := nil;
42 end;
43
44 if Unassigned(WordApp) then
45 begin
46 try
47 WordApp := CoApplication.Create;
48 WordApp.Visible := True;
49 except
50 if Assigned(WordApp) then
51 begin
52 SaveChanges := wdDoNotSaveChanges;
53 WordApp.Quit(SaveChanges, EmptyParam, EmptyParam);
54 end;
55 end;
56 end;
57 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
58 uses
59 Word_TLB;
60
61 function GetRTFFormat(DataObject: IDataObject; var RTFFormat: TFormatEtc): Boolean;
62 var
63 Formats: IEnumFORMATETC;
64 TempFormat: TFormatEtc;
65 cfRTF: LongWord;
66 Found: Boolean;
67 begin
68 try
69 OleCheck(DataObject.EnumFormatEtc(DATADIR_GET, Formats));
70 cfRTF := RegisterClipboardFormat('Rich Text Format');
71 Found := False;
72 while (not Found) and (Formats.Next(1, TempFormat, nil) = S_OK) do
73 if (TempFormat.cfFormat = cfRTF) then
74 begin
75 RTFFormat := TempFormat;
76 Found := True;
77 end;
78 Result := Found;
79 except
80 Result := False;
81 end;
82 end;
83
84 procedure GetRTF(WordDoc: _Document);
85 var
86 DataObject: IDataObject;
87 RTFFormat: TFormatEtc;
88 ReturnData: TStgMedium;
89 Buffer: PChar;
90 begin
91 if Assigned(WordDoc) then
92 begin
93 try
94 WordDoc.QueryInterface(IDataObject, DataObject);
95 if GetRTFFormat(DataObject, RTFFormat) then
96 begin
97 OleCheck(DataObject.GetData(RTFFormat, ReturnData));
98 // RTF is passed through global memory
99 Buffer := GlobalLock(ReturnData.hglobal);
100
101 { Buffer is a pointer to the RTF text
102 Insert code here to handle the RTF text (ie. save it, display it etc.) }
103 GlobalUnlock(ReturnData.hglobal);
104 end;
105 except
106 ShowMessage('Error while getting RTF');
107 end;
108 end;
109 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)
110 uses
111 Word_TLB, activex, comobj, ConnectionObject
112
113 // ConnectionObject is the unit containing TWordConnection
114
115 procedure StartWordConnection(WordApp: _Application;
116 WordDoc: _Document;
117 var WordSink: TWordConnection);
118 var
119 PointContainer: IConnectionPointContainer;
120 Point: IConnectionPoint;
121 begin
122 try
123 { TWordConnection is the COM object which receives the
124 notifications from Word. Make sure to free WordSink when
125 you are done with it. }
126 WordSink := TWordConnection.Create;
127 WordSink.WordApp := WordApp;
128 WordSink.WordDoc := WordDoc;
129
130 // Sink with a Word application
131 OleCheck(WordApp.QueryInterface(IConnectionPointContainer, PointContainer));
132 if Assigned(PointContainer) then
133 begin
134 OleCheck(PointContainer.FindConnectionPoint(ApplicationEvents, Point));
135 if Assigned(Point) then
136 Point.Advise((WordSink as IUnknown), WordSink.AppCookie);
137 end;
138
139 // Sink with a Word document
140 OleCheck(WordDoc.QueryInterface(IConnectionPointContainer, PointContainer));
141 if Assigned(PointContainer) then
142 begin
143 OleCheck(PointContainer.FindConnectionPoint(DocumentEvents, Point));
144 if Assigned(Point) then
145 Point.Advise((WordSink as IUnknown), WordSink.DocCookie);
146 end;
147 except
148 on E: Exception do
149 ShowMessage(E.message);
150 end;
151 end;
Sample Code (Connection Object)
152 unit ConnectionObject;
153
154 interface
155
156 uses
157 Word_TLB;
158
159 type
160 TWordConnection = class(TObject, IUnknown, IDispatch)
161 protected
162
163 { IUnknown }
164 function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
165 function _AddRef: Integer; stdcall;
166 function _Release: Integer; stdcall;
167
168 { IDispatch }
169 function GetIDsOfNames(const IID: TGUID; Names: Pointer;
170 NameCount, LocaleID: Integer;
171 DispIDs: Pointer): HResult; stdcall;
172 function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; stdcall;
173 function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
174 function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
175 Flags: Word; var Params;
176 VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;
177
178 public
179 WordApp: _Application;
180 WordDoc: _Document;
181 AppCookie, DocCookie: Integer;
182 end;
183
184 implementation
185
186 uses
187 Windows, ActiveX, Main;
188
189 procedure LogComment(comment: string);
190 begin
191 Form1.Memo1.Lines.Add(comment);
192 end;
193
194 { IUnknown Methods }
195
196 function TWordConnection._AddRef: Integer;
197 begin
198 Result := 2;
199 end;
200
201 function TWordConnection._Release: Integer;
202 begin
203 Result := 1;
204 end;
205
206 function TWordConnection.QueryInterface(const IID: TGUID; out Obj): HResult;
207 begin
208 Result := E_NOINTERFACE;
209 Pointer(Obj) := nil;
210 if GetInterface(IID, Obj) then
211 Result := S_OK;
212 if (not Succeeded(Result)) then
213 if IsEqualIID(IID, DocumentEvents) or IsEqualIID(IID, ApplicationEvents) then
214 if GetInterface(IDispatch, Obj) then
215 Result := S_OK;
216 end;
217
218 { IDispatch Methods }
219
220 function TWordConnection.GetIDsOfNames(const IID: TGUID; Names: Pointer;
221 NameCount, LocaleID: Integer;
222 DispIDs: Pointer): HResult;
223 begin
224 Result := E_NOTIMPL;
225 end;
226
227 function TWordConnection.GetTypeInfo(Index, LocaleID: Integer;
228 out TypeInfo): HResult;
229 begin
230 Pointer(TypeInfo) := nil;
231 Result := E_NOTIMPL;
232 end;
233
234 function TWordConnection.GetTypeInfoCount(out Count: Integer): HResult;
235 begin
236 Count := 0;
237 Result := E_NOTIMPL;
238 end;
239
240 function TWordConnection.Invoke(DispID: Integer; const IID: TGUID;
241 LocaleID: Integer; Flags: Word;
242 var Params; VarResult, ExcepInfo,
243 ArgErr: Pointer): HResult;
244 begin
245 // This is the entry point for Word event sinking
246 Result := S_OK;
247 case DispID of
248 1: ; // Startup
249 2: ; // Quit
250 3: ; // Document change
251 4: ; // New document
252 5: ; // Open document
253 6: ; // Close document
254 else
255 Result := E_INVALIDARG;
256 end;
257 end;
258
259 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
|