Paul Klemond
{ewc navigate.dll, ewbutton, /Bcodeview /T"Click to open or copy files in the CLIDEMO project." /C"samples_2}
{ewc navigate.dll, ewbutton, /Bcodeview /T"Click to open or copy files in the SRVRDEMO project." /C"samples_2}
In an industry brimming over with acronyms, a new one is showing up more and more in product reviews and on software packaging. Object Linking and Embedding (OLE) gives users a powerful, simple way to share data among MicrosoftÒ WindowsÔ-based applications. OLE is implemented as a set of libraries that is part of the Windows1 version 3.1 operating system, but these libraries also work with applications written for Windows 3.0. In fact, many popular applications already support OLE or are being retrofitted to support it: LotusÒ 1-2-3Ò for Windows 1.0, Microsoft Excel version 3.0, Lotus Ami Professional 2.0, Microsoft Word for Windows 2.0, MicrografxÒ Charisma 2.1, and many more.
Unfortunately, OLE can also give Windows programmers reason to consider a career change: as Charles Petzold has said, OLE "is undoubtedly the largest and most complex single enhancement to Windows 3.0." I wasn’t aware of OLE’s complexity myself when I set out to implement support for it in the spreadsheet and chart modules of Microsoft Works for Windows.
Before explaining OLE programming, I’m going to illustrate how OLE works in common Windows applications.
Pretend you’re preparing a document to explain the design of some code you’re about to write. You’re using the updated Write applet included with Windows 3.1. Write now supports OLE.
You’ve written a few paragraphs of your design document, and now you’d like to describe some data structures that you sketched out on paper. A diagram would be great, so you run a drawing program, say Windows PaintbrushÔ. Using Paintbrush, you create a diagram of your new data structure and then copy it to the Clipboard.
Back in Write, you paste the diagram into your document. You go back and close Paintbrush and return to your document in the word processor. Without realizing it, you’ve just created an OLE object, and embedded it into your Write document. But what does this mean?
Before OLE, pasting something into a target document meant inserting a static vestige from the source document--say, some cells from a spreadsheet. What happened if you wanted to update the pasted-in information? If you saved the cells in a file someplace and could find them, you’d edit it and paste a new copy into your document. If you didn’t save them, you had to start over from scratch.
With OLE’s embedded objects, everything you need is kept right in the target document. In the design document example, just double-clicking on the embedded picture automatically invokes Paintbrush, already loaded with your picture and ready for editing.
Because you pasted an OLE diagram object into your document, Write received the diagram’s native data (the binary information that would normally be saved into a file). With that native data, Write has received some information about the drawing package that created it. Write associated this information with the diagram’s bitmap you see in your document. Write chose the bitmap format to be the "presentation format" for the object.
In the example above, Write is referred to as an OLE client. It is capable of receiving, displaying, and storing OLE objects from all sorts of other applications, like Paintbrush. You may see a client’s document referred to as an object container, a container document, or just a container. Paintbrush is an OLE server: it creates and edits OLE objects. Every server has its own unique class of OLE object, and every class is supported by exactly one server. Every class has a unique classname. This is because each server knows how to edit its own object class’s native data formats. In the future, there may be standard classes for objects that multiple sources can edit, like bitmaps. For now, every class must be unique.
There’s another way to embed an object, a way that doesn’t require you to go to the Program Manager and start the server application yourself. OLE clients such as Write have a new Insert Object menu pick. Selecting this brings up a dialog box listing the names of all OLE object classes for which servers have been installed on the machine (see Figure 1). As you can see, there’s a variety of OLE objects.
Figure 1 The Insert Object pick lets you insert an OLE object.
Say you want to put another object into your design document. Using this method, you would choose the Paintbrush Picture object class from the list of classes in Write’s Insert Object dialog box. The Paintbrush applet, an OLE server, appears (see Figure 2).
Figure 2 Closing the server and returning to the OLE client.
After you create your new object, you close the Paintbrush server, either by double-clicking on its system menu or by selecting the Exit & Return to Filename item from the File menu (see Figure 2). Write now shows the embedded object in your document (see Figure 3). Double-clicking on this object brings up Paintbrush again, allowing you to edit the embedded object.
Figure 3 Write reappears, showing the embedded object in the document.
Your design document is almost finished, but you decide to include a table with estimate data from your schedule. Let’s say your schedule data is kept in a Microsoft Excel spreadsheet, perhaps as part of a larger document containing the data for all the members working on your project. Microsoft Excel is an OLE server, so this is going to be easy. You run Microsoft Excel and open the spreadsheet file containing the data for your project. Next, you select the table containing the relevant information and copy it to the Clipboard. You then click on Write.
Since the data in Microsoft Excel might change, you would have to manually update it if you simply embedded it into your design document. Here’s where the "L" in OLE comes in: linking is an alternative to embedding. To create a linked object, select the Paste Special item from Write’s Edit menu. This brings up a dialog box (see Figure 4), with a list containing all of the formats available on the Clipboard. It is possible to create both linked and embedded objects from this dialog box. To create a linked object, select the classname in this list box (Excel Worksheet Object in this case), and press the Paste Link button. (To create an embedded object, you would select the Paste button after selecting the classname.)
Figure 4 Using the Paste Special menu pick to create a linked object.
Instead of the Microsoft Excel data itself, this linked object consists of the path and filename of the server’s document (the estimate spreadsheet in this case) stored in the client document. When you double-click on your linked object, OLE runs the object’s server application (Microsoft Excel). Because it’s a linked object, OLE asks the server to load the linked-to file from disk (the estimate spreadsheet from which your table data was drawn).
Unlike embedded objects, linked objects are said to possess an "automatic link" while they are being edited in the server. This means that while the user is editing a linked object in the server application, changes may be propagated to the client as those changes are made in the server. Clients and servers support this automatic link behavior. (This used to be referred to as a hot link.)
LANs are becoming increasingly common, so it’s important to know what OLE does and does not do across a network. Clients and server applications must reside on the same machine. This is because OLE has been implemented using Windows Dynamic Data Exchange (DDE). DDE is the mechanism that enables clients and servers to talk to each other. OLE will not work between client and server applications running on different machines because standard Windows DDE doesn’t work over a network.
This doesn’t mean users of OLE don’t benefit from networks. Linking is particularly valuable when the linked-to document is shared on a network file server. OLE’s linked objects work with LAN-stored files transparently and seamlessly. This is because files on network drives are indistinguishable from files stored on local disk drives to the operating system.
Remember that a linked object contains a path and filename specifying the file to which the object is linked. In the linked object scenario described above, you created a linked object in the word processor document using schedule data from a spreadsheet. This is particularly powerful when the spreadsheet is stored on a file server and accessible to everyone across the network working on the project.
OLE is of course also subject to the limitations of the network. If the LAN server containing the document your object is linked to is inaccessible, the server application (which runs on your machine) won’t be able to load the file.
Because linked objects are based on a path and filename that may change as the linked-to (server’s) file is moved around in the file system, clients should provide a way for users to perform housekeeping, or "link maintenance," tasks. The Links dialog box is the recommended method (see Figure 5).
Figure 5 The recommended method for clients to let users perform links maintenance.
This dialog has several features. First, it allows the user to specify new path and filenames for files their objects are linked to. It also allows the user to update a link, which involves invoking the object’s server and asking it to supply current data from the linked-to file. The dialog allows the user to specify whether links should be updated manually or automatically each time the client document is opened. The user can also use the Links dialog to cancel a link, retaining only the presentation data. Canceling a link doesn’t delete the object: it simply converts a linked object into a static object consisting of presentation data only. OLE keeps neither native data nor link information for static objects.
OLE is not limited to putting pictures into word processing documents. If you had a sound digitization program with OLE server support, you could record a spoken message and then embed it into a piece of E-mail in an E-mail client application. You could then send that E-mail to a coworker, who could play the message back provided he or she had the same sound server installed on their machine.
Presentation is not limited to metafiles. Clients and servers negotiate the presentation format by comparing the list of formats they support and choosing the "richest" one. Any format that the client and server support in common is acceptable: bitmaps, icons, text, or custom formats.
Windows 3.1 includes an object packager, PACKAGER.EXE, that gives users flexibility for determining presentation data for an object (see Figure 6). Packager is both an OLE client and an OLE server. Think of it as a middleman that helps clients present objects from both OLE servers and applications that are not OLE servers. How’s that again? How can an application that’s not an OLE server offer objects? Packager encapsulates things into its own class of OLE object. To OLE clients, Packager’s objects are just another ordinary class of object.
Figure 6 Packager encapsulates OLE and non-OLE objects into its own class of OLE object.
Packager can encapsulate objects from an OLE server or non-OLE objects like filenames and command lines. It uses information about file extensions from WIN.INI to invoke an appropriate application when the user wants to access the contents of an encapsulated non-OLE object.
Packager offers some flexibility in choosing presentation data for the encapsulated object. You can paste any sort of presentation data you wish into Packager, including icons, bitmaps, metafiles, and more, from any source. All that’s required is that you get it onto the Windows Clipboard.
Double-clicking an object is the usual way to invoke that object’s server; it’s part of a general OLE mechanism called verbs. Verbs are the set of actions that may be performed on an object. Double-clicking, by convention, invokes the primary verb supported by the server for the object’s class. The primary verb is the verb that the server implementor considers to be the most important, useful, or most frequently used verb. For most classes, this verb is Edit. Some classes have more verbs. A sound digitization or animation server might logically support a Play verb in addition to the Edit verb. Any operation a user may wish to perform on an object is a good candidate for the server to offer as a verb (see Figure 7).
Verbs are presented to the user via a menu item in the client application’s Edit menu, and possibly also through other user interface mechanisms that might be specific to a client application, such as buttons.
Figure 7 Servers, Classnames, and Verbs
Server Application | Internal Classname(s) | User-Readable Classname(s) | Primary Verb | Secondary Verb |
Server Demo
(Sample application from the Windows 3.1 SDK) |
ServerDemo | Server Demo | Edit | (none) |
Microsoft Packager | Package | Package | Activate Contents | Edit Package |
Microsoft Paintbrush | PBrush | Paintbrush Picture | Edit | (none) |
Microsoft SoundRec
(Digitizes, records, and plays sounds) |
SoundRec | Sound | Play | Edit |
Microsoft Works for Windows 2.0 | MSWorksSpreadsheet, | MS Works Spreadsheet, | Edit | (none) |
(Single application that supports two server classes) | MSWorksChart | MS Works Chart | Edit | (none) |
User interface consistency leverages the knowledge a user gained from other applications, allowing him or her to learn a new application quickly. I’ve described some user interface features above, but there are currently a few variations in the way different programs implement these user interface features.
Some applications implement an Insert menu that includes an Object menu pick, instead of a single Insert Object menu pick on their Edit menu. Some applications show the list of object classes as a cascading menu, instead of showing a dialog box with a list box containing the classes.
Earlier, you saw how to create a linked object using the Edit menu’s Paste Special menu pick, which produced the Paste Special dialog box. Some applications may provide a Paste Link item on the Edit menu to create a linked object in one direct step.
There is an OLE chapter in the Windows 3.1 Software Development Kit (SDK) user interface style guide. Applications supporting OLE should comply with this guide.
As mentioned earlier, OLE uses Windows DDE as the underlying data transport mechanism and for conversation between the client and server. DDE has a reputation similar to the one OLE is getting: writing DDE code is hairy.
OLE’s DDE conversation obeys a standard, documented, public protocol. To add OLE support to an application, you could write all the code necessary to manage an OLE DDE conversation, using just OLE’s DDE specification (and your knowledge of how DDE works, of course). Microsoft has produced a DDE Management Library (DDEML) to make implementing DDE easier, if for some reason you choose to implement OLE this way. The DDEML library is shipped with Windows 3.1 and also works with Windows 3.0.
Fortunately, it is possible to get OLE working in your application without knowing much about DDE or having to write any DDE code. Microsoft has created a set of OLE libraries that manage the DDE conversation for your application. The OLE libraries also provide some other services, which I’ll describe later. These libraries do not use or require the DDEML library mentioned above.
One more word about DDE is in order, and that word is protocol. The standard DDE protocol for OLE is called StdFileEditing. Since DDE is used for purposes other than OLE, OLE has features that allow for coexistence with other protocols. This is only important if your application uses DDE for non-OLE purposes. I’ll say a little more about protocols in the sections covering specific features affecting DDE, of which there are few.
Server applications are written using the OLE server API and include the OLESVR.LIB library via the linker (see Figure 8). Likewise, client applications are written using the OLE client API and include the OLECLI.LIB library at link time. The client and server APIs are defined in a C header file, OLE.H, that clients and servers include during compilation. Like WINDOWS.H, OLE.H contains constant definitions, data structure typedefs, and prototypes of client and server API functions.
Figure 8 OLE's Library-based Architecture
Both the client and server LIB files have corresponding DLLs, which do most of the real work. The fact that most of the work is done in these DLLs does not affect your application in any way, so "OLE libraries" refers to the LIBs and DLLs collectively.
The client and server applications must maintain data structures that correspond to OLE objects. Clients must manage all user interface features, and may call library functions to manage objects and perform operations on them. Servers provide a set of services that the libraries will use to do the client’s bidding. The libraries manage the DDE conversation between client and server, can render objects for the client, and launch and shut down server applications.
There is one more type of component in the OLE architecture, the object handler. This is a DLL that a server author can write to accompany a server application. You might think of it as a sort of server’s assistant, to which straightforward operations without user interface requirements may be offloaded. Object handlers are documented in the Windows SDK 3.1, so I won’t cover them.
You will need the items listed in Figure 9 to implement OLE in your Windows-based application if you’re using the OLE libraries.
Figure 9 Files Used to Implement OLE
The OLE Library Files
OLESVR.LIB
OLESVR.DLL
OLECLI.LIB
OLECLI.DLL
OLE Header File
OLE.H
The System Registration Database
SDKREGED.EXE
REGLOAD.EXE
SHELL.DLL
SHELL.LIB
SHELL.H
The OLE 1.0 specification is a comprehensive description of OLE, covering the conceptual model, the user interface, a function reference, and the DDE protocol specification. This document is available on CompuServeÒ in the MSL and MSOPSYS forums and on Microsoft Online.
To implement OLE features in your Windows application by managing the OLE DDE conversation yourself, instead of using the libraries, you will need the OLE DDE Protocol Specification. This is an appendix to the OLE Specification.
Either using the libraries or implementing DDE yourself, you may find a tool that lets you observe in detail the DDE conversation occurring between the client and server useful. One tool that does this is DDEWatch, from Horizon Technologies.
Earlier, I mentioned that the libraries can launch server applications. To facilitate this and other capabilities, there is a new centralized service from Windows: the system registration database.
Until now, users had to remember which applications they’d used to work with a given data object. The WIN.INI file does have an [Extensions] section to associate an appropriate application executable with a data file having a given file extension, but there is still a fair amount of work that can be offloaded from the user to applications.
The system registration database is a binary file named REG.DAT. In Windows 3.1, this is a standard file, stored in the subdirectory in which Windows is installed. Some of the enhancements of Windows 3.1, including OLE, worked under Windows 3.0, so REG.DAT may be present on some machines running Windows 3.0. If you install Windows 3.1 in a different directory from Windows 3.0, your REG.DAT may be out of sync. REG.DAT contains information about every OLE server application that has been installed on the machine.
To support the Clipboard-based Paste-Embed and Paste-Link features, there are three new clipboard formats for clients and servers: Native, ObjectLink, and OwnerLink.
The Native format is the server’s native data for an object. In other words, it is the binary data the server would normally save to a file. No other application interprets this data. It is just stored in the client as an embedded object.
The ObjectLink format facilitates linked objects. It has three parts: classname, document name, and item name. All three are null-terminated character strings, with an extra null character appended to indicate the end of the structure. A classname is just a string identifying a type of object and the server that works with that type of object. The document name is the path and filename of the document from which the object was copied.
Use of the item name is defined by the server. It specifies the object in the context of a larger server document containing the object. A spreadsheet server might use the item name to hold cell references such as A1:B4. In this case the three elements making up the ObjectLink might be:
Microsoft Excel Worksheet
D:\EXCEL\MYSHEET.XLS
A1:B4
A paint server might use the item name to hold the coordinates of a rectangle, (100,100)-(200,200), to indicate that the object is a portion of a larger bitmap.
Embedded objects are made possible via the OwnerLink format. This format looks similar to the ObjectLink format--classname, document name, and item name, but unlike linked objects the document name is server-defined.
Clients and servers should call RegisterClipboardFormat to obtain format identifiers for all three of these. Servers offer these formats, and clients look for them on the Clipboard, enabling or disabling user interface features accordingly. Data in these formats will be passed back to the server through callback functions.
All Windows-based applications use callback functions, which are functions in your application called directly from Windows. A window procedure is a callback, as is a dialog box procedure. Implementing OLE client or server support requires adding another form of callback to your code, so that the OLE libraries can provide data to your application and ask your application to perform commands.
To implement callbacks, OLE uses a mechanism called a VTBL. The term VTBL comes from the object-oriented concept of a virtual table, in which functions, or "object methods," are associated with each instance of the data comprising the object. I use the term VTBL to refer to both this conceptual construct and the specific data structure used in OLE. In the latter case, a VTBL is a C structure containing a group of pointers to functions within your application.
Both clients and servers must use VTBLs. I’ll use one of the client’s VTBLs, the Stream VTBL, as a sample. All client applications must provide this, and there are other VTBLs that clients and servers must provide. Figure 10 is a diagram of the Stream VTBL.
Figure 10 The Stream VTBL
The functions Get and Put in Figure 11 are callback functions, the functions that OLE will be calling. The VTBL itself is composed of several parts; the first in this case is a VTBL structure of type OLESTREAMVTBL (see figure 11) that contains only pointers to your functions (Get and Put). The next part is a structure (OLESTREAM in this example), which in the current version of OLE contains only a pointer to the VTBL structure. The last part is a structure that you define to contain the OLESTREAM structure and other members to hold data you wish to associate with the Stream VTBL. I call this a wrapper structure because it wraps your data with one of OLE’s structures. You define this wrapper structure in your code as follows; note the fh file handle member in the APPSTREAM structure.
typedef struct _APPSTREAM
{
OLESTREAM os;// OLE’s part must come first!
o
o // Your members go here.
o
int fh; // File handle
} APPSTREAM;
These parts are common to all VTBLs. The VTBL in Figure 12 corresponds to a Stream, which identifies a logical channel through which data is stored and retrieved in a client. Other VTBLs correspond to objects, documents, and servers. Servers must provide a total of 25 callback functions grouped into three VTBLs. Clients only need three callbacks grouped into two VTBLs, one of which is the Stream VTBL described here.
Figure 11 OLE.H Typedefs for the Stream VTBL
typedef struct _OLESTREAMVTBL
{
DWORD (pascal far *Get) (LPOLESTREAM, LPSTR, DWORD);
DWORD (pascal far *Put) (LPOLESTREAM, LPSTR, DWORD);
} OLESTREAMVTBL;
typedef OLESTREAMVTBL FAR *LPOLESTREAMVTBL;
typedef struct _OLESTREAM
{
LPOLESTREAMVTBL lpstbl;
} OLESTREAM;
Figure 12 Initializing a Stream VTBL
/*
* InitStream()
*
* Create and fill the STREAMVTBL. Create a stream
* structure and initialize pointer to stream vtbl.
*
* Returns LPAPPSTREAM - if successful a pointer to a
* stream structure, otherwise NULL.
*/
static LPAPPSTREAM InitStream(HANDLE hInstance)
{
LPAPPSTREAM lpStream = NULL;
if (!(lpStream = (LPAPPSTREAM)GlobalLock(
GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, sizeof(APPSTREAM)))))
goto Error;
if (!(lpStream->os.lpstbl = (LPOLESTREAMVTBL)GlobalLock(
GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, sizeof(OLESTREAMVTBL)))))
goto Error;
lpStream->os.lpstbl->Get = (DWORD (FAR PASCAL *)
(LPOLESTREAM, LPSTR, DWORD))
MakeProcInstance((FARPROC)ReadStream, hInst);
lpStream->os.lpstbl->Put = (DWORD (FAR PASCAL *)
(LPOLESTREAM, LPSTR, DWORD))
MakeProcInstance((FARPROC)WriteStream, hInst);
return lpStream; //* SUCCESS return
Error:
ErrorMessage(E_FAILED_TO_ALLOC);
EndStream(lpStream);
return NULL; //* ERROR return
}
/*
* EndClient()
*
* Perform cleanup prior to app termination. The OLECLIENT
* memory blocks and procedure instance thunks freed.
*/
static void EndStream(LPAPPSTREAM lpStream)
{
HANDLE hGeneric;
if (lpStream) //* is there a STREAM struct?
{
if (lpStream->os.lpstbl)
{
FreeProcInstance((FARPROC)lpStream->os.lpstbl->Get);
FreeProcInstance((FARPROC)lpStream->os.lpstbl->Put);
hGeneric = (HANDLE)GlobalHandle(HIWORD(lpStream->os.lpstbl));
GlobalUnlock(hGeneric);
GlobalFree(hGeneric);
}
hGeneric = (HANDLE)GlobalHandle(HIWORD(lpStream));
GlobalUnlock(hGeneric);
GlobalFree(hGeneric);
}
}
Your application is responsible for providing memory for these structures. They may be kept in your application’s data segment, either as global variables or dynamically allocated blocks on the local heap. You will probably want to allocate memory for them from the global heap.
Let’s think about how this applies to objects for a moment. Each object your client or server is working with should have its own data structure, of type OLEOBJECT in servers, and of type OLECLIENT in clients. These correspond to the OLESTREAM part of the VTBL in Figure 10. OLE defines the OLEOBJECT and OLECLIENT structures in OLE.H. You should define wrapper structures around these, just as the APPSTREAM was defined as a wrapper around the OLESTREAM structure.
Since you will very likely have to deal with more than one object simultaneously in your client or server application, you will need one instance of your wrapper data structure for each object. When OLE calls one of your callbacks, it will pass it a pointer to your wrapper corresponding to the object OLE is referring to. This is why you will probably find it very convenient to store all kinds of data in your wrapper, so your callback function can get at it very quickly and easily.
While it does make sense for you to have unique instances of your wrappers for each object, document, server, and stream, it is usually not necessary to have multiple instances of the VTBL structure. It is fine if all instances of a given type of wrapper all refer to the same VTBL structure. For example, see Figure 13, which shows a VTBL that all servers must provide, the Object VTBL. Note its similarity to the client’s Stream VTBL. Most servers will need multiple instances of the wrapper structures (the OBJ structure here, wrapping the OLEOBJECT structure). Each OBJ wrapper can contain the same OLEOBJECT structure, which points to one OLEOBJECTVTBL structure.
Figure 13 Unique Wrappers, Shared VTBL
Windows multitasking and scheduling has a few ramifications for OLE clients and servers. Because Windows is not a preemptive multitasking environment, every Windows-based application must make some effort to let other applications run. When Windows stops running one application and starts running another, it is referred to as a context switch. Context switching results from an application calling PeekMessage or GetMessage. Most applications do this from their main message loop, commonly the WinMain function.
DDE is one of the ways Windows facilitates communication between applications. Like much of the rest of Windows functionality, Windows uses messages to make DDE happen. Two applications hold a DDE conversation by sending and receiving messages between each other,using SendMessage or PostMessage. OLE uses PostMessage, so the DDE messages go through the destination application’s queue.
Sometimes, your application will call an OLE API asking it to do something that requires information from the other library in the DDE conversation. For example, when a server calls OleRevokeServerDoc to tell OLE that a user has closed a document, the server library needs information from the client library. In these cases, the OLE API (OleRevokeServerDoc in this case) will return the code OLE_WAIT_FOR_RELEASE.Servers have two OLE APIs that may return OLE_WAIT_FOR_RELEASE. Clients have 23.
This means that OLE needs time to fulfill a request. It has posted a message to the queue of the other library. Windows needs to do a context switch before the other library can handle the message. Because of this, it is important that you follow some guidelines when writing your application. For starters, don’t free up data structures related to the call, and don’t exit--OLE will be calling one of your callbacks. You should continue to peek or get messages and dispatch them because the client application needs to get some CPU time, and Windows can only give it while you are calling PeekMessage, GetMessage, or WaitMessage. Remember, these are the only Windows functions that give Windows the chance to do a context switch and let another application run.
Another reason is because OLE is based on DDE. Your library is waiting for the right DDE message, upon the receipt of which it will call one of your callbacks to release you. Dispatching messages will ensure that the DDE message gets to the library.
Let’s look at some of the things that a client application has to do to support OLE. Below are the basic features every client application must provide. I’ll be looking at some of these in more depth later and providing source code from the sample client application in the Windows 3.1 SDK.
Installation If your client application must work in Windows 3.0, it will need the OLE client and Shell libraries installed.
Initialization There is a small amount of extra work that an OLE client application must perform during initialization.
Creating/Receiving Objects There are two scenarios in which clients create or receive objects: Insert Object, and pasting from the Clipboard to create a linked or embedded object. A client must support at least one of these capabilities. The Insert Object command is a new feature, whereas the paste methods are modifications to an application’s Clipboard code.
Object Display While not specifically required in a client application, almost every client application will display the OLE objects its document contains. This affects an application’s painting and scrolling code.
Object Activation This is the ability to have OLE invoke the appropriate server to perform the object’s primary verb, usually Edit. This is usually done when the user double-clicks on the display representation of the object, so it affects the mouse code. It may also be a command accessed from a menu or other user interface method.
Object Storage and Retrieval This involves providing the ability to permanently store the objects someplace. Most client applications will store objects in a disk file with other information that the client keeps for a document. This feature will require modifications to the application’s file loading and saving code.
Object Selection Most applications support the notion of selection: an object is highlighted and subjected to commands. This commonly affects existing selection code, which is usually intricately tied to display, mouse, and keyboard support code in an application.
Object Destruction To complement the creation of objects, most clients allow the user to destroy objects from the client’s document. This is usually done by pressing the Backspace key when the object is selected or choosing the Cut command on the Edit menu of most applications.
In addition to the baseline features listed above, there are a number of client features of secondary importance.
Links Maintenance This is the dialog box described earlier. It is more of a convenience feature than a necessity since the user can simply create a new object, instead of repairing a broken link using this dialog box. This feature involves a fair amount of code to support the dialog box and the various commands provided through it. The sample client application in the Windows 3.1 SDK contains a complete Links Maintenance feature implementation.
Verbs You may or may not wish to provide an object’s complete list of verbs to the user. The primary verb (usually Edit) and possibly the secondary "Play" verb where appropriate should cover the most common usages. This is usually a set of items on a cascading menu and requires using the Shell API to retrieve the list of supported verbs for the selected object’s class. (More on the Shell API below.)
Moving and Resizing Objects Many applications allow the user to move and resize objects within a document.
Let’s look at some of these basic features in more detail. The OLE client library has 56 functions for your application to call and make all of this happen. For example, there are ten different functions just for creating objects. Figure 14 lists all the functions comprising the Client API.
Figure 14 The OLE Client API
Object Creation Functions |
OleCopyFromLink |
OleCreate |
OleCreateFromClip |
OleCreateFromFile |
OleCreateFromTemplate |
OleCreateInvisible |
OleCreateLinkFromClip |
OleCreateLinkFromFile |
General Object Manipulation Functions |
OleActivate |
OleClone |
OleClose |
OleCopyToClipboard |
OleDelete |
OleDraw |
OleLoadFromStream |
OleObjectConvert |
OleReconnect |
OleRelease |
OleRename |
OleRequestData |
OleSaveToStream |
OleSetBounds |
OleSetColorScheme |
OleSetData |
OleSetHostNames |
OleSetLinkUpdateOptions |
OleSetTargetDevice |
OleUpdate |
Miscellaneous Functions |
OleExecute |
OleLockServer |
OleRegisterClientDoc |
OleRenameClientDoc |
OleRevertClientDoc |
OleRevokeClientDoc |
OleSavedClientDoc |
OleUnlockServer |
Information Functions |
OleEnumFormats |
OleEnumObjects |
OleEqual |
OleGetData |
OleGetLinkUpdateOptions |
OleIsDcMeta |
OleQueryBounds |
OleQueryClientVersion |
OleQueryCreateFromClip |
OleQueryLinkFromClip |
OleQueryName |
OleQueryOpen |
OleQueryOutOfDate |
OleQueryProtocol |
OleQueryReleaseError |
OleQueryReleaseMethod |
OleQueryReleaseStatus |
OleQueryServerVersion |
OleQuerySize |
OleQueryType |
OLE comes standard with Windows 3.1, but the OLE libraries also work in Windows 3.0. For a client or server to run under Windows 3.0 using the OLE libraries, the application must include copies of the OLE libraries and install those libraries at the same time the application is installed. Installing them involves copying OLECLI.DLL and OLESVR.DLL into the SYSTEM subdirectory of the directory in which Windows is installed.
There is something to watch out for though: it’s possible that the user has already installed another application that supports OLE, and that application may have already copied in the OLE libraries. This is only a problem when the other application shipped with newer libraries than you included.
Before copying the OLE libraries during your application’s install, you should check to ensure that your versions are newer than any that may already be installed. Luckily, there is a new way to do this. Another new feature included with Windows 3.1 is a scheme to place version information into all EXEs and DLLs. Like OLE, this scheme does not require Windows 3.1 and may be shipped with applications that run in Windows 3.0.
The scheme is based on a new kind of Windows resource, one that you define in your RC file (see Figure 15). The OLE libraries themselves have this version resource in them. Another part of this scheme is a way for applications to examine the version resource in any EXE or DLL that has one. This is done via the new Version API. This API is implemented in--yes, you guessed it--yet another new DLL, VERSION.DLL.
The program that installs your application should use this new feature to compare the versions of the libraries shipped with your application against that of the libraries, if any, already installed on the user’s machine. Your installation program should then install the libraries that ship with your application only if they are newer (see the sample code in Figure 16).
Figure 16 Comparing Library Versions During Installation
/*
* Function: IsFileOlder
*
* Purpose: Compare the version resource in the file identified by
* szFile (a fully qualified path string) against the version
* in the two long parameters. Return TRUE if the file's version
* is less than (older) that in the two version long params.
*/
BOOL
IsFileOlder(szFile, dwVersionMS, dwVersionLS)
char *szFile; // Full path name of file to check here
DWORD dwVersionMS; // Most-significant portion of file vern
// to compare against.
DWORD dwVersionLS; // Least-significant portion of file ven
// to compare against.
{
VS_FIXEDFILEINFO *pvsfi;
DWORD dwHandle;
LPVOID pbData;
HANDLE hVersionData;
DWORD cbData;
WORD cbLen;
char szFullPath[128];
cbData = GetFileVersionInfoSize((LPSTR)szFullPath, &dwHandle);
if (cbData = = NULL)
return(TRUE); // Assume file being tested is older.
hVersionData = GlobalAlloc(GMEM_MOVEABLE, cbData);
if (hVersionData = = NULL)
return(TRUE); // Assume file being tested is older.
pbData = GlobalLock(hVersionData);
if (!VerQueryValue(pbData, "\\", (VOID FAR * FAR *)&pvsfi, &cbL)
{
// Can't retrieve version resource information
GlobalUnlock(hVersionData);
GlobalFree(hVersionData);
return(TRUE); // Assume file being tested is older.
}
if (pvsfi->dwFileVersionMS < dwVersionMS ||
(pvsfi->dwFileVersionMS = = dwVersionMS &&
pvsfi->dwFileVersionLS < dwVersionLS))
return(TRUE); // File being tested is older.
GlobalUnlock(hVersionData);
GlobalFree(hVersionData);
return(FALSE); // File being tested is not older.
}
Most Windows applications perform a number of operations during initialization, such as the registration of window classes. Supporting OLE client features requires a few more things to be done during this initialization period.
Figure 17 shows sample code with OLE-related initialization. First, a client application should allocate and initialize the two client VTBLs: OLECLIENTVTBL and OLESTREAMVTBL. Next, the client should call the OleRegisterClientDoc function to allow OLE to prepare for possible calls to the client library and to provide OLE access to the OLECLIENTVTBL.
Figure 17 INITCLI.C
/***************************************************************************
* InitAsOleClient()
*
* Initiates the creation of stream and client vtbls. These vtbls are very
* important for the proper operation of this application. The stream vtbl
* lets the OLE libraries know where the stream I/O routines
* reside. The stream routines are used by OleLoadFromStream and the like.
* The client vtbl is used to hold the pointer to the CallBack function.
* IMPORTANT: both the client and the stream structures have pointers to
* vtbls which have the pointers to the functions. Therefore, it is
* necessary to allocate space for the vtbl and the client structure
* which has the pointer to the vtbl.
**************************************************************************/
static BOOL InitAsOleClient( //* ENTRY:
HANDLE hInstance, //* application instance handle
HWND hwnd, //* main window handle
PSTR pFileName, //* document file name
LHCLIENTDOC *lhcDoc, //* pointer to document Handle
LPOLECLIENT *lpClient, //* pointer to client pointer
LPAPPSTREAM *lpStream //* pointer to APPSTREAM pointer
){
//* initiate client vtbl creation
if (!(*lpClient = InitClient(hInstance)))
{
SendMessage(hwnd, WM_SYSCOMMAND, SC_CLOSE, 0L);
return FALSE; //* ERROR return
}
//* initiate stream vtbl creation
if (!(*lpStream = InitStream(hInstance)))
{
SendMessage(hwnd, WM_SYSCOMMAND, SC_CLOSE, 0L);
return FALSE; //* ERROR return
}
if (*pFileName && RegDoc(pFileName,lhcDoc)
&& LoadFile(pFileName,*lhcDoc,*lpClient,*lpStream))
{
SetTitle(pFileName);
return TRUE; //* SUCCESS return
}
NewFile(pFileName, lhcDoc, *lpStream);
return TRUE; //* SUCCESS return
} //* SUCCESS return
/****************************************************************************
* InitClient()
*
* Initialize the OLE client structure, create and fill the OLECLIENTVTBL
* structure.
*
* Returns LPOLECLIENT - if successful a pointer to a client structure
* , otherwise NULL.
***************************************************************************/
static LPOLECLIENT InitClient( //* ENTRY:
HANDLE hInstance //* application instance handle
){ //* LOCAL:
LPOLECLIENT lpClient=NULL; //* pointer to client struct
//* Allocate vtbls
if (!(lpClient = (LPOLECLIENT)GlobalLock(
GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, sizeof(OLECLIENT))
)))
goto Error; //* ERROR jump
if (!(lpClient->lpvtbl = (LPOLECLIENTVTBL)GlobalLock(
GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, sizeof(OLECLIENTVTBL))
)))
goto Error; //* ERROR jump
//* set the CALLBACK function
//* pointer
lpClient->lpvtbl->CallBack = MakeProcInstance(CallBack, hInst);
return lpClient; //* SUCCESS return
Error: //* ERROR Tag
ErrorMessage(E_FAILED_TO_ALLOC);
EndClient(lpClient); //* free any allocated space
return NULL; //* ERROR return
}
/****************************************************************************
* InitStream()
*
* Create and fill the STREAMVTBL. Create a stream structure and initialize
* pointer to stream vtbl.
*
* Returns LPAPPSTREAM - if successful a pointer to a stream structure
* , otherwise NULL .
***************************************************************************/
static LPAPPSTREAM InitStream( //* ENTRY:
HANDLE hInstance //* handle to application instance
){ //* LOCAL:
LPAPPSTREAM lpStream = NULL; //* pointer to stream structure
if (!(lpStream = (LPAPPSTREAM)GlobalLock(
GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, sizeof(APPSTREAM))
)))
goto Error; //* ERROR jump
if (!(lpStream->lpstbl = (LPOLESTREAMVTBL)GlobalLock(
GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, sizeof(OLESTREAMVTBL))
)))
goto Error; //* ERROR jump
//* set stream func. pointers
lpStream->lpstbl->Get = (DWORD (FAR PASCAL *)(LPOLESTREAM, LPSTR, DWORD))
MakeProcInstance((FARPROC)ReadStream, hInst);
lpStream->lpstbl->Put = (DWORD (FAR PASCAL *)(LPOLESTREAM, LPSTR, DWORD))
MakeProcInstance((FARPROC)WriteStream, hInst);
return lpStream; //* SUCCESS return
Error: //* ERROR Tag
ErrorMessage(E_FAILED_TO_ALLOC);
EndStream(lpStream);
return NULL; //* ERROR return
}
o
o
o
/***************************************************************************
* RegDoc()
*
* Register the client document with the OLE library.
**************************************************************************/
static BOOL RegDoc( //* ENTRY:
PSTR pFileName, //* file name
LHCLIENTDOC *lhcptrDoc //* pointer to client document handle
){
if (Error(OleRegisterClientDoc(szAppName, (LPSTR)pFileName, 0L, lhcptrDoc)))
{
ErrorMessage(W_FAILED_TO_NOTIFY);
return FALSE; //* ERROR return
}
return TRUE; //* SUCCESS return
}
The two main methods of creating or receiving objects are the Insert Object method and pasting from the Clipboard to create a linked or embedded object. The code in Figure 18 performs the Insert Object command. The Insert Object dialog box is straightforward to implement; the code in Figure 19 uses the Shell API to greatly simplify management of the list box containing available object classnames. Figure 20 has code for pasting to create an embedded or linked object.
Displaying OLE objects can be done in two primary ways. The client can either do its own rendering, given the data from the server, or it can have OLE do the rendering (see Figure 21). The first method involves retrieving the object’s presentation data (by calling the OleGetData function), and then calling Windows GDI functions as necessary to render the object. There are several other functions that give the client more control of this rendering (such as OleSetBounds, OleSetTargetDevice, and OleSetColorScheme), even though OLE does the rendering.
Figure 18 INSOBJ.C
/* From CLIDEMO.RC */
o
o
o
DTCREATE DIALOG 50, 26, 183, 62
STYLE WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Insert New Object"
BEGIN
LTEXT "Object Type:", -1, 7, 5, 123, 8, NOT WS_GROUP
LISTBOX IDD_LISTBOX, 5, 15, 125, 41, LBS_SORT | WS_TABSTOP |
WS_VSCROLL
PUSHBUTTON "OK", IDOK, 140, 8, 35, 14, WS_TABSTOP
PUSHBUTTON "Cancel", IDCANCEL, 140, 25, 35, 14, WS_TABSTOP
END
/* From GLOBAL.H */
o
o
o
#define DOC_CLEAN 0 //* Dirty() methods
#define DOC_DIRTY 1
#define DOC_UNDIRTY 2
#define DOC_QUERY 3
typedef struct _APPITEM *APPITEMPTR;
typedef struct _APPITEM { //* Application item
LPOLECLIENTVTBL lpClientTbl;
HWND hwnd;
LPOLEOBJECT lpObject; //* OLE object pointers
LPOLEOBJECT lpObjectUndo; //* undo object
LONG otObject; //* OLE object type
LONG otObjectUndo;
OLEOPT_UPDATE uoObject; //* OLE object update option
OLEOPT_UPDATE uoObjectUndo;
ATOM aLinkName; //* link name atom
ATOM aLinkUndo;
LPSTR lpLinkData; //* pointer to link data
BOOL fVisible; //* TRUE: item is to be displayed
BOOL fOpen; //* server open? --for undo objects
BOOL fRetry; //* retry flag for busy servers
BOOL fNew;
BOOL fServerChangedBounds;
RECT rect; //* bounding rectangle
LHCLIENTDOC lhcDoc; //* client document handle
} APPITEM;
/* From OBJECT.C */
o
o
o
/***************************************************************************
* ObjInsert()
*
* Query the user for object type to insert and insert the new OLE object
***************************************************************************/
void FAR ObjInsert( //* ENTRY:
LHCLIENTDOC lhcDoc, //* OLE document handle
LPOLECLIENT lpClient //* pointer to OLE client structure
){ //* LOCAL:
FARPROC lpfnInsertNew; //* Insert New dialog proc instance thunk
LPOLEOBJECT lpObject; //* pointer to OLE object
APPITEMPTR pItem; //* item pointer
char szClassName[CBPATHMAX];//* Class name for OleCreate()
char szTmp[CBOBJNAMEMAX]; //* buffer to unique object name
lpfnInsertNew = MakeProcInstance(fnInsertNew, hInst);
if (DialogBoxParam(hInst, MAKEINTRESOURCE(DTCREATE),hwndFrame,
lpfnInsertNew, (long)((LPSTR)szClassName)) != IDCANCEL)
{
if (pItem = PreItemCreate(lpClient, FALSE, lhcDoc))
{
if ( Error( OleCreate(STDFILEEDITING,(LPOLECLIENT)pItem,
(LPSTR)szClassName, lhcDoc,CreateNewUniqueName(szTmp),
&lpObject,olerender_draw, 0)))
{
ErrorMessage(E_FAILED_TO_CREATE_OBJECT);
FreeAppItem(pItem);
}
else
PostItemCreate(lpObject, OT_EMBEDDED, NULL, pItem);
}
}
FreeProcInstance(lpfnInsertNew);
Dirty(DOC_DIRTY);
}
/****************************************************************************
* PreItemCreate()
*
* This routine allocates an application item structure. A pointer to this
* structure is passed as the client structure, therefore we need to
* have a pointer to the vtbl as the first entry. We are doing this
* to allow access to the application item information during an OLE
* DLL callback. This approach simplifies matters.
*
* Returns APPITEMPTR - a pointer to a new application item structure
* which can operate as a client structure.
***************************************************************************/
APPITEMPTR FAR PreItemCreate( //* ENTRY:
LPOLECLIENT lpClient, //* OLE client pointer
BOOL fShow, //* show/no-show flag
LHCLIENTDOC lhcDoc //* client document handle
){ //* LOCAL:
HANDLE hitem; //* temp handle for new item
APPITEMPTR pItem; //* application item pointer
if (hitem = LocalAlloc(LMEM_MOVEABLE | LMEM_ZEROINIT, sizeof(APPITEM)))
if (pItem = (APPITEMPTR)LocalLock(hitem))
{ //* set the vtbl pointer
pItem->lpClientTbl = lpClient->lpvtbl;
pItem->lpObjectUndo = NULL;
pItem->fVisible = fShow;
pItem->fServerChangedBounds = FALSE;
pItem->lhcDoc = lhcDoc;
return pItem; //* SUCCESS return
}
ErrorMessage(E_FAILED_TO_ALLOC);
return NULL; //* ERROR return
}
/***************************************************************************
* FreeAppItem()
*
* Free application item structure and destroy the associated structure.
**************************************************************************/
VOID FAR FreeAppItem( //* ENTRY:
APPITEMPTR pItem //* pointer to application item
){ //* LOCAL:
HANDLE hWork; //* handle used to free
if (pItem)
{ //* destroy the window
if (pItem->hwnd)
DestroyWindow(pItem->hwnd);
hWork = LocalHandle((WORD)pItem);//* get handle from pointer
if (pItem->aLinkName)
DeleteAtom(pItem->aLinkName);
LocalUnlock(hWork);
LocalFree(hWork);
}
}
/***************************************************************************
* Error()
*
* This function checks for error conditions
* generated by OLE API calls. For OLE_WAIT_FOR_RELEASE,
* we keep track of the number of objects waiting, When
* this count is zero, it is safe to exit the application.
*
* Returns OLESTATUS - 0 if OLE_WAIT_FOR_RELEASE or OLE_OK
* otherwise the OLESTATUS returned after an action
* is taken.
*************************************************************************/
OLESTATUS FAR Error( //* ENTRY
OLESTATUS olestat //* OLE status
){
switch (olestat)
{
case OLE_WAIT_FOR_RELEASE:
if (!cOleWait)
Hourglass(TRUE);
cOleWait++; //* increment wait count
case OLE_OK:
return 0;
case OLE_ERROR_STATIC: //* static object
ErrorMessage(W_STATIC_OBJECT);
break;
case OLE_ERROR_REQUEST_PICT:
case OLE_ERROR_ADVISE_RENAME:
case OLE_ERROR_DOVERB:
case OLE_ERROR_SHOW:
case OLE_ERROR_OPEN:
case OLE_ERROR_NETWORK:
case OLE_ERROR_ADVISE_PICT:
case OLE_ERROR_COMM: //* Invalid links
InvalidLink();
break;
case OLE_BUSY:
ErrorMessage(E_SERVER_BUSY);
default:
break;
}
return olestat;
}
/****************************************************************************
* PostItemCreate()
*
* This function creates a child window which will contain the newly
* created OLE object. A pointer to our item information is stored in the
* extra bytes of this window. This is where we internally keep track
* of information related to the object as well as the
* pointer to the object for subsequent OLE API calls. This routine is
* called after an OLE object has been created by the client library.
*
* Returns BOOL - TRUE if application item has been created.
****************************************************************************/
BOOL FAR PostItemCreate( //* ENTRY:
LPOLEOBJECT lpObject, //* OLE object pointer
long otObject, //* OLE object type
LPRECT lprcObject, //* object bounding rect
APPITEMPTR pItem //* application item pointer
){ //* LOCAL:
int i; //* index
RECT rc; //* bounding rectangle
char pData[OBJECT_LINK_MAX];//* copy of link data
if (lprcObject) //* if the size of the objects
rc = *lprcObject; //* bounding rectangle is not
else if (OleQueryBounds(lpObject, &rc) = = OLE_OK)
ConvertToClient(&rc);
else
SetRect(&rc, 0, 0, 0, 0);
if (!(pItem->hwnd = CreateWindow( //* Create the child window
szItemClass, "",
WS_BORDER | WS_CHILD | WS_CLIPSIBLINGS | WS_THICKFRAME,
rc.left,rc.top,
rc.right - rc.left + 2 * GetSystemMetrics(SM_CXFRAME),
rc.bottom - rc.top + 2 * GetSystemMetrics(SM_CYFRAME),
hwndFrame, NULL, hInst, NULL
))) goto Error;
//* in windows extra bytes
SetWindowWord(pItem->hwnd, 0, (WORD)pItem);
pItem->otObject = otObject;
pItem->lpObject = lpObject;
pItem->fRetry = TRUE;
if( pItem->otObject = = OT_EMBEDDED )//* if object is embedded tell library
{ //* the container name and object name.
int cb = CBOBJNAMEMAX; //* The name will be the server window title.
char sz[CBOBJNAMEMAX]; //* when the object is edited.
OleQueryName(lpObject, (LPSTR)sz, &cb );
WaitForObject(pItem->lpObject);
Error(OleSetHostNames(lpObject, (LPSTR)szAppName, (LPSTR)sz ));
WaitForObject(pItem->lpObject);
}
else if (pItem->otObject = = OT_LINK)//* if the object is linked
{ //* retrieve update options
WaitForObject(pItem->lpObject);
if(Error(OleGetLinkUpdateOptions(pItem->lpObject, &pItem->uoObject)))
goto Error;
if (ObjGetData(pItem,pData))
{
for (i=0; pData[i];i++); //* Skip past the server name
pItem->aLinkName = AddAtom(&pData[++i]);
}
else
pItem->aLinkName = AddAtom("");
}
iObjects++;
//* a user interface recommendation.
return TRUE; //* SUCCESS return
Error: //* ERROR Tag
ErrorMessage(E_FAILED_TO_CREATE_CHILD_WINDOW);
FreeAppItem(pItem);
return FALSE; //* ERROR return
}
/* From DIALOG.C */
o
o
o
/****************************************************************************
* fnInsertNew()
*
* Dialog procedure for the Insert New dialog.
*
* Returns int - TRUE if message processed, FALSE otherwise
***************************************************************************/
BOOL FAR PASCAL fnInsertNew( //* ENTRY:
HWND hDlg, //* standard dialog box paramters
unsigned msg,
WORD wParam,
LONG lParam //* (LPSTR) class name
){ //* LOCAL:
HWND hwndList; //* handle to listbox
static LPSTR lpClassName; //* classname for return value
hwndList = GetDlgItem(hDlg, IDD_LISTBOX);
switch (msg)
{
case WM_INITDIALOG:
if (!RegGetClassNames(hwndList))
EndDialog(hDlg, IDCANCEL);
lpClassName = (LPSTR)lParam;
SetFocus(hwndList);
SendMessage(hwndList, LB_SETCURSEL, 0, 0L);
return (FALSE);
case WM_COMMAND:
switch (wParam)
{
case IDD_LISTBOX:
if (HIWORD(lParam) != LBN_DBLCLK)
break;
case IDOK:
if (!RegCopyClassName(hwndList, lpClassName))
wParam = IDCANCEL;
case IDCANCEL:
EndDialog(hDlg, wParam);
break;
}
break;
}
return FALSE;
}
/* From UTILITY.C */
o
o
o
/****************************************************************************
* ErrorMessage()
*
* Display a message box containing the specified string from the table.
*
* id WORD - Index into string table.
***************************************************************************/
VOID FAR ErrorMessage( //* ENTRY:
WORD id //* message ID
){ //* LOCAL:
char sz[CBMESSAGEMAX]; //* string
HWND hwnd; //* parent window handle
if (IsWindow(hwndProp))
hwnd = hwndProp;
else if (IsWindow(hwndFrame))
hwnd = hwndFrame;
else
return;
LoadString(hInst, id, sz, CBMESSAGEMAX);
MessageBox(hwnd, sz, szAppName, MB_OK | MB_ICONEXCLAMATION);
}
/***************************************************************************
* ConvertToClient()
*
* This function will convert to client from himetric.
**************************************************************************/
void FAR ConvertToClient( //* ENTRY:
LPRECT lprc //* pointer to bounding rectangle
){ //* LOCAL
HDC hDC; //* handle to a device context used
//* to do coordinate system transforms
short mm; //* save mapping mode
//* If we have an empty rectangle,
//* start at default size
if (!(lprc->left || lprc->top || lprc->right || lprc->bottom))
SetRect(lprc, 0, 0, CXDEFAULT, CYDEFAULT);
else
{
hDC = GetDC(NULL); //* get a DC to use to convert
mm = SetMapMode(hDC, MM_HIMETRIC);
LPtoDP(hDC, (LPPOINT)lprc, 2); //* convert to client
SetMapMode(hDC, mm);
ReleaseDC(NULL, hDC);
lprc->left = lprc->top = 0;
}
}
/***************************************************************************
* CreateNewUniqueName()
*
* Create a string name unique to this document. This is done by using the
* prefix string("OleDemo #") and appending a counter to the end of the
* prefix string. The counter is incremented whenever a new object is added.
* String will be 14 bytes long.
*
* Return LPSTR - pointer to unique object name.
***************************************************************************/
LPSTR FAR CreateNewUniqueName( //* ENTRY:
LPSTR lpstr //* destination pointer
){
wsprintf( lpstr, "%s%04d", OBJPREFIX, iObjectNumber++ );
return( lpstr );
}
/****************************************************************************
* Dirty()
*
* Keep track of whether modifications have been made
* to the document or not.
*
* iAction - action type:
* DOC_CLEAN set document clean flag true
* DOC_DIRTY the opposite
* DOC_UNDIRTY undo one dirty op
* DOC_QUERY return present state
*
* Returs int - present value of fDirty; 0 is clean.
***************************************************************************/
int FAR Dirty( //* ENTRY:
int iAction //* see above comment
){ //* LOCAL:
static int iDirty = 0; //* dirty state >0 is dirty
switch (iAction)
{
case DOC_CLEAN:
iDirty = 0;
break;
case DOC_DIRTY:
iDirty++;
break;
case DOC_UNDIRTY:
iDirty--;
break;
case DOC_QUERY:
break;
}
return(iDirty);
}
/****************************************************************************
* Hourglass()
*
* Puts up or takes down the hourglass cursor as needed.
*
* int bToggle - TRUE turns the hour glass on
* HG_OFF turns it off
***************************************************************************/
VOID FAR Hourglass( //* ENTRY:
BOOL bOn //* hourglass on/off
){ //* LOCAL:
static HCURSOR hcurWait = NULL; //* hourglass cursor
static HCURSOR hcurSaved; //* old cursor
static iCount = 0;
if (bOn)
{
iCount++;
if (!hcurWait)
hcurWait = LoadCursor(NULL, IDC_WAIT);
if (!hcurSaved)
hcurSaved = SetCursor(hcurWait);
}
else if (!bOn)
{
if (--iCount < 0 )
iCount = 0;
else if (!iCount)
{
SetCursor(hcurSaved);
hcurSaved = NULL;
}
}
}
/***************************************************************************
* WaitForObject()
*
* Dispatch messages until the specified object is not busy.
* This allows asynchronous processing to occur.
*
* lpObject LPOLEOBJECT - pointer to object
**************************************************************************/
void FAR WaitForObject( //* ENTRY:
LPOLEOBJECT lpObject //* pointer to OLE object
){ //* LOCAL
while (OleQueryReleaseStatus(lpObject) = = OLE_BUSY)
ProcessMessage(hwndFrame, hAccTable);
}
/****************************************************************************
* ProcessMessage()
*
* Obtain and dispatch a message. Used when in a message dispatch loop.
*
* Returns BOOL - TRUE if message other than WM_QUIT retrieved
* FALSE if WM_QUIT retrieved.
***************************************************************************/
BOOL FAR ProcessMessage( //* ENTRY:
HWND hwndFrame, //* main window handle
HANDLE hAccTable //* accelerator table handle
){ //* LOCAL:
BOOL fReturn; //* return value
MSG msg; //* message
if (fReturn = GetMessage(&msg, NULL, NULL, NULL))
{
if (cOleWait || !TranslateAccelerator(hwndFrame, hAccTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return fReturn;
}
Figure 19 Performing the Insert Object Command
o
o
o
case WM_INITDIALOG:
if (!RegGetClassNames(hwndList))
EndDialog(hDlg, IDCANCEL);
o
o
o
case WM_COMMAND:
switch (wParam)
{
case IDOK:
if (!RegCopyClassName(hwndList, lpClassName))
wParam = IDCANCEL;
o
o
o
Figure 20 PASTE.C
/* From OBJECT.C */
o
o
o
/***************************************************************************
* ObjPaste()
*
* This function obtains an object from the clipboard.
* Handles both embedded and linked objects. An item window is
* created for each new object.
*
* Returns BOOL - TRUE if object was pasted successfully.
**************************************************************************/
void FAR ObjPaste( //* ENTRY:
BOOL fPaste, //* Paste/PasteLink flag
LHCLIENTDOC lhcDoc, //* client document handle
LPOLECLIENT lpClient //* pointer to client
){ //* LOCAL:
LPOLEOBJECT lpObject; //* object pointer
long otObject; //* object type
APPITEMPTR pItem; //* application item pointer
char szTmp[CBOBJNAMEMAX]; //* temporary object name string
if (!(pItem = PreItemCreate(lpClient, TRUE, lhcDoc)))
return; //* ERROR return
if (!OpenClipboard(hwndFrame))
goto Error; //* ERROR jump
if (fPaste) //* PASTE the object.
{ //* Try "StdFileEditing" protocol
if (Error(OleCreateFromClip(STDFILEEDITING,(LPOLECLIENT)pItem,lhcDoc,
CreateNewUniqueName(szTmp),&lpObject, olerender_draw,0)))
{
//* next try "Static" protocol
if (Error(OleCreateFromClip(
STATICP, (LPOLECLIENT)pItem, lhcDoc,
CreateNewUniqueName(szTmp), &lpObject, olerender_draw, 0)))
goto Error; //* ERROR jump
}
}
else
{ //* LINK therefore must be
// "STdFileEditing" protocol
if (Error(OleCreateLinkFromClip(
STDFILEEDITING,(LPOLECLIENT)pItem, lhcDoc,
CreateNewUniqueName(szTmp), &lpObject, olerender_draw, 0)))
goto Error; //* ERROR jump
}
OleQueryType(lpObject, &otObject);
CloseClipboard();
if (!PostItemCreate(lpObject, otObject, NULL, pItem))
return; //* ERROR return
ShowNewWindow(pItem);
return; //* SUCCESS return
Error: //* TAG Error
ErrorMessage(E_GET_FROM_CLIPBOARD_FAILED);
CloseClipboard();
FreeAppItem(pItem);
return; //* ERROR return
}
/* From UTILITY.C */
o
o
o
/****************************************************************************
* ShowNewWindow()
*
* Show a new application item window.
***************************************************************************/
void FAR ShowNewWindow( //* ENTRY:
APPITEMPTR pItem
){
if (pItem->fVisible)
{
pItem->fNew = TRUE;
SetTopItem(pItem);
ShowWindow(pItem->hwnd,SW_SHOW);
}
else
ObjDelete(pItem,DELETE);
}
Figure 21 Code to Have OLE Display an Object
/* From OBJECT.C, ItemWndProc */
o
o
o
case WM_PAINT:
BeginPaint(hwnd, (LPPAINTSTRUCT)&ps);
GetClientRect(hwnd, &rc);
pItem = (APPITEMPTR)GetWindowWord(hwnd, 0);
//* Call OLE draw
Error(OleDraw(pItem->lpObject, ps.hdc, &rc, NULL, NULL));
EndPaint(hwnd, (LPPAINTSTRUCT)&ps);
break;
o
o
o
Recall that every server supports at least one verb that the server can perform on objects of a particular class. Servers specify verbs in the system registration database. Back in the client, when the user double-clicks an object, you may wish to have your client application issue the object’s primary verb, which typically invokes the object’s server to edit the object. To do this, the client application needs to call the OleActivate function with a number indexing the verb it wishes to execute. Most clients execute the primary verb, so there is an easy way to do this: just pass the constant OLE_VERB_PRIMARY (defined in OLE.H) to the OleActivate function (see Figure 22).
Other verbs and their indexes may be available from the system registration database, accessible in your client application via the Shell API (discussed below).
In the OLE scheme for storing linked or embedded objects in your client application’s data files, you treat OLE objects like black boxes: you don’t interpret the contents of the records you allocate for objects in your file, you just treat them like a block of binary data that you store and retrieve for the OLE client library.
The scheme is based on the stream model of I/O. When you wish to store an object in a file, on disk, in a database record, or whatever, you call OleSaveToStream. Before returning from that function, OLE will call your Put callback one or more times via your OLESTREAMVTBL. In your Put callback, you should receive a block of memory from OLE containing a specified number of bytes that define the object being saved. You should then write those bytes to disk (see Figure 23) or a database record, or buffer them someplace.
To create an OLE object from its stored record, you call OleLoadFromStream. Before returning from that function, the OLE client library will call your Get callback one or more times via your OLESTREAMVTBL. In your Get callback, you should prepare a block of memory containing the number of bytes that OLE asks for via the Get callback’s parameters. Figure 24 contains sample code to load an OLE object from a disk file.
Figure 23 Storing an OLE Object from a Client Application
/****************************************************************************
* ObjWrite()
*
* This function writes an object to the specified
* file. The file pointer will be advanced past the end of
* the written object.
*
* Returns BOOL - TRUE if object written successfully
***************************************************************************/
BOOL FAR ObjWrite( //* ENTRY:
LPAPPSTREAM lpStream, //* application stream pointer
APPITEMPTR pItem //* application item pointer
){ //* LOCAL:
POINT pt; //* center of rec point
RECT rc; //* bounding rectangle
int cbTmp;
char szTmp[PROTOCOL_STRLEN];//* protocol string
cbTmp = CBOBJNAMEMAX;
OleQueryName(pItem->lpObject, szTmp, &cbTmp);
if (_lwrite(lpStream->fh, szTmp, CBOBJNAMEMAX) < CBOBJNAMEMAX )
return FALSE;
if (pItem->otObject = = OT_STATIC)
wsprintf(szTmp, "%-15s", STATICP);
else
wsprintf(szTmp, "%-15s", STDFILEEDITING);
if (_lwrite(lpStream->fh, szTmp, PROTOCOL_STRLEN) < PROTOCOL_STRLEN )
return FALSE;
if (Error(OleSaveToStream(pItem->lpObject, (LPOLESTREAM)lpStream)))
return FALSE;
GetClientRect(pItem->hwnd, (LPRECT)&rc);
pt = *(LPPOINT)&rc;
ClientToScreen(pItem->hwnd, (LPPOINT)&pt);
ScreenToClient(hwndFrame, (LPPOINT)&pt);
OffsetRect(
&rc,
pt.x - rc.left - GetSystemMetrics(SM_CXFRAME),
pt.y - rc.top - GetSystemMetrics(SM_CYFRAME)
);
if (_lwrite(lpStream->fh, (LPSTR)&rc, sizeof(RECT)) < sizeof(RECT)
|| _lwrite(lpStream->fh, (LPSTR)&(pItem->otObject), sizeof(long)) <
sizeof(long))
return FALSE;
return TRUE; //* SUCCESS return
}
Figure 24 Loading an OLE Object into a Client Application
/****************************************************************************
* ObjRead()
*
* Read an object from the specified file. The file pointer will
* be advanced past the object.
*
* HANDLE fh - DOS file handle of file to be read from
*
* returns HWND - window handle to item window containing the OLE object
***************************************************************************/
BOOL FAR ObjRead( //* ENTRY:
LPAPPSTREAM lpStream, //* application stream pointer
LHCLIENTDOC lhcDoc, //* document handle
LPOLECLIENT lpClient //* pointer to OLE client structure
){ //* LOCAL:
APPITEMPTR pItem; //* application item pointer
LPOLEOBJECT lpObject; //* pointer ole object
long otObject; //* type of object
RECT rcObject; //* object rect
char szTmp[CBOBJNAMEMAX]; //* temporary string buffer
char szProto[PROTOCOL_STRLEN+1];//* protocol string
int i; //* index
if (_lread(lpStream->fh, szTmp, CBOBJNAMEMAX) < CBOBJNAMEMAX )
return FALSE;
if (_lread(lpStream->fh, szProto, PROTOCOL_STRLEN) < PROTOCOL_STRLEN )
return FALSE;
for (i=0; szProto[i] != ' '; i++);
szProto[i] = NULL;
ValidateName( szTmp );
if (!(pItem = PreItemCreate(lpClient, TRUE, lhcDoc)))
return FALSE;
if (Error(OleLoadFromStream((LPOLESTREAM)lpStream,
szProto,(LPOLECLIENT)pItem,
lhcDoc, szTmp, &lpObject)))
goto Error;
if (_lread(lpStream->fh, (LPSTR)&rcObject, sizeof(RECT)) < sizeof(RECT))
goto Error;
if (_lread(lpStream->fh, (LPSTR)&otObject, sizeof(long)) < sizeof(long))
goto Error;
if (PostItemCreate(lpObject, otObject, &rcObject, pItem))
{
pItem->fNew = TRUE;
ObjSetBounds(pItem);
return TRUE; //* SUCCESS return
}
else
return FALSE;
Error: //* ERROR Tag
FreeAppItem(pItem);
return FALSE;
}
Unlike servers, OLE client applications do not need to add information about themselves to the registration database. Client applications do, however, need to query this database to obtain information about servers. A new Windows 3.1 library, SHELL.DLL, helps you do this (see Figure 25). The Shell API provides about a half-dozen functions that allow you to obtain the following:
The list of classnames available to the user via the Insert Object command
The list of verbs available to the user when an object is selected
Error messages in message boxes, when an error occurs involving an OLE object of a given classname
The classname displayed in the Link Management dialog box
The classname displayed in the Paste Special dialog box
The classname and verbs displayed on the status line, reflecting information about an OLE-related menu item
The most commonly retrieved item will be the readable form of a classname, the form of the classname that you want the user to see. Server applications will make sure that this is localized to the appropriate foreign language if necessary.
Figure 25 SHELL.DLL, a Client, and REG.DAT
For sample code that uses the Shell API, see the fnInsertNew function in Figure 19, the Insert Object code. In that function, all calls to functions with names that start with "Reg" are calls to the Shell API. Servers aren’t required to use the Shell API but may use it to update information about themselves in the system registration database.
Let’s go back now to VTBLs. All client applications need to provide an OLECLIENTVTBL. This gives OLE access to a callback function called CallBack. The OLE client library makes this callback in various situations to give you information about the state of an object. Client applications will probably need only one OLECLIENTVTBL. But there should be one instance of an OLECLIENT wrapper for each OLE object your user decides to work with.
Earlier, I mentioned that 23 of the 56 functions in the Client API may return OLE_WAIT_FOR_RELEASE. When you receive this return value after calling one of these functions, you must not call any other OLE functions that affect that object until OLE releases the object. You may call OLE functions for other objects not used in the original call. OLE releases an object by calling your callback with the OLE_RELEASE notification code in a parameter. One good way to keep track of this is to keep a Boolean variable for each object indicating whether or not the object is awaiting release. The object’s wrapper structure is a good place for this.
One pitfall you might run into here is an undocumented reentrancy problem in the current client library. Let’s say the user has deleted an object from a client document. Your client application will need to call the OleDelete function to delete the object. If the object is still busy from some previous operation (an earlier function call had returned OLE_WAIT_FOR_RELEASE), then you must wait for OLE to call your callback function with OLE_RELEASE. When OLE does this, you will want to delete the object using OleDelete. You must not do this from within your callback function, for this will cause a UAE. You must return from your callback function and call OleDelete any time after returning. You could post a message to yourself to delete it later or set some global state to delete it during idle time.
Adding OLE support to your application is complicated. Even if you’re not planning to add support to applications immediately, knowing what’s required will ease implementation later. The next article will discuss implementing server support.
Automatic Link There is said to be an automatic link between a client containing a linked object and that object’s server while the server is editing the object. For most servers, changes made to an object will rapidly be applied to the presentation data in the client. This does not apply to embedded objects.
Blocking Blocking is done by the server when it does not want any of its callbacks to be called until the server is again ready. The server uses the OleBlockServer and OleUnblockServer functions to notify the libraries when to disable and enable callbacks.
Broken Link A broken link occurs when a file to which an object is linked has been deleted or moved. The linked objects cannot access the file to which they refer. The link must be repaired by entering the file’s new path and filename into the Links dialog for each affected object.
Callback A function in an application, called from another application or system component such as the OLE libraries.
Class A unique type of object produced by a server. Represented by a classname.
Classname The text name given to a class.
Client An application that receives, stores, and usually presents objects created by servers.
DDE Windows Dynamic Data Exchange. A method of exchanging data and conducting conversations between two Windows-based applications, based on sending messages.
Embedding Associating native data with the presentation data for an object in a client.
Handler See Object Handler.
Linking Associating the path and filename of a data file with the presentation data for an object in a client.
Locking A client ensuring that a server’s code segments are kept in memory to improve performance.
Native Data The possibly proprietary binary format that a server application has chosen for storing an object. Usually a disk file format.
Object Any unit of data product created by a server application, combined with a method of performing operations on it. Examples: a range of spreadsheet cells, a piece of digitized sound.
Object Handler A DLL accompanying a server that performs work that would otherwise require invoking the server application itself.
ObjectLink The Clipboard format required for creating a linked object from the Clipboard.
OwnerLink The Clipboard format required for creating an embedded object from the Clipboard.
Presentation Data The representation of an object within an OLE client.
Presentation Format The format a client has chosen for representing an OLE object.
Primary Verb OLE servers specify one of their verbs to be the primary verb. Edit is usually specified as the primary verb and is commonly invoked by clients when the user double-clicks on an object.
Reg file A text description of a server’s EXE path and filename, the DOS filename extension of its data files, its classname, and the verbs and protocols it supports.
REG.DAT The binary file containing the system registration database.
Registration a) Adding information to the system registration database at the time a server is installed. b) Notifying the OLE libraries that a client or server application has been run and initialized, and is ready to receive calls to its callbacks.
REGLOAD.EXE An SDK utility that adds text server application information from a REG file to a system registration database. This is commonly done while installing a server application.
SDKREGED.EXE An SDK utility that helps you create a text REG file describing your server application.
Server An application that creates, edits, and shares with client applications some class of OLE object.
Shell API The set of services provided by Windows for storing and retrieving information in the system registration database.
Static An alternative to linking and embedding, in which no server support is available for the object. The object is neither linked nor has any native data associated with it.
StdFileEditing The standard OLE DDE protocol. Other protocols are supported through the OLE library’s OleExecute client API and the server’s Execute callbacks.
System Registration Database The central repository for information about servers: their EXE path and filename, the MS-DOS filename extension of their data files, their classname, and the verbs and protocols they support.
Verb An operation that a server can perform on an object. Servers register verbs in the system registration database. Clients provide a menu of all of the selected object’s verbs.
VTBL A type of OLE data structure containing a set of pointers to functions inside client or server applications.
1For ease of reading, "Windows" refers to the Microsoft Windows operating system. Windows is a trademark that refers only to this Microsoft product.