One of the greatest advantages of using OLE Automation from within FoxPro is due to the new object oriented extensions. Because you now have the ability to create custom classes, and because classes serve to encapsulate both data and behavior, you can build classes that shield you from having to worry about the peculiarities of a specific type of OLE server application.
To illustrate this, let's look at the differences between controlling Microsoft Word and Microsoft Excel by beginning to develop a class that defines how we'll want to control all of our OLE applications. We will not design this class to be instantiated directly. (Classes that are designed in this fashion are called abstract classes). Rather, we will create a subclass of this class for each specific OLE application that we want to control.
#INCLUDE "\VFP\FOXPRO.H" DEFINE CLASS OLEApplication AS Custom *-- oOLEApp - Holds reference to our OLE Application Server *-- cOLERegName - The name of the server object as found in *-- the registration database. *-- lCloseAppWhenDone - .T. if we started the server for the *-- time within this class PROTECTED oOLEApp, ; cOLERegName, ; lCloseAppWhenDone oOLEApp = "" cOLERegName = "" lCloseAppWhenDone = .T. *-- Methods FUNCTION Init() *-- First make sure that the user is not trying to create an *-- instance of this class. IF EMPTY(this.cOLERegName) =MessageBox("Cannot create object directly from class OLEApplication", ; MB_ICONSTOP, ; "") RETURN .F. ENDIF *-- Attempt to start the application *-- First, check to see if app is already running IF this.AppRunning() *-- Grab the current instance this.oOLEApp = this.GetCurrentInstance() ELSE *-- Create a new instance. this.oOLEApp = this.CreateNewInstance() ENDIF ENDFUNC *-- Additional methods with are described below get *-- inserted here. ENDDEFINE
This defines the core functionality of our abstract OLE Application class. We first check to ensure that the user of the class is not trying to instantiate it directly. If they are, we just cancel the INIT method, which prevents the object from being created. Secondly, we check to see if the application is running. If it is, we create a reference to it. If not, we create a new instance.
You may be wondering why you need to determine if the application you're trying to control is currently running. The reason is that certain applications let you create multiple instances, while others do not. If you were to execute the following three commands from the command window, you would wind up with three separate running instances of Microsoft Excel:
xl1 = CREATEOBJECT("Excel.Application") xl2 = CREATEOBJECT("Excel.Application") xl3 = CREATEOBJECT("Excel.Application")
If you try this with Microsoft Word, you'll wind up with just one instance, and three references to the same instance. So how do we determine if an application is already running? Here are two suggestions.
The first way involves use of the GETOBJECT() function to attempt to retrieve a reference to an OLE Automation server. At the time of this writing, this option works fine with Microsoft Excel, but not with Microsoft Word. We'll override this function when we define our Word of Windows class, but for now, let's define this option as a protected function within our OLEApplication class. (We define the function as protected because we will only be calling this function from within the OLEApplication class or any class that we derive from it. There is no need to expose it to the "outside world").
PROTECTED FUNCTION AppRunning() *-- Returns .T. if app is already running LOCAL lcOldError, ; llRunning llRunning = .T. lcOldError = ON("ERROR") ON ERROR llRunning = .F. *-- Attempt to get a reference to a running application =GETOBJECT("", this.cOLERegName) ON ERROR &lcOldError this.lCloseAppWhenDone = !llRunning RETURN llRunning ENDFUNC
The second way of determining if an application is already running involves the use of DDE. So as not to disrupt the definition we are building of the OLEApplication class, I will defer discussing this alternative method until we create our Word for Windows class, a subclass of the OLEApplication class.
To complete the definition of the OLEApplication class, we create two methods, also defined as protected functions, that we will use to create references to new instances or retrieve references to current instances. You'll notice that each method contains only one statement. We could have eliminated the overhead of the function call and placed the CREATEOBJECT() and GETOBJECT() calls directly in the Init method, but, if we needed to, we would not be able to customize the behavior of these methods in our subclasses. An example of customizing the behavior of these methods can be found in the GetCurrentInstance() method of the Word for Windows class. (See below)
PROTECTED FUNCTION CreateNewInstance() RETURN CREATEOBJECT(this.cOLERegName) ENDFUNC PROTECTED FUNCTION GetCurrentInstance() RETURN GETOBJECT("", this.cOLERegName) ENDFUNC
Let's define our first subclass to handle OLE Automation with Microsoft Excel. Most of the inherited functionality from the OLEApplication class works just fine with Microsoft Excel. The first thing we need to do is initialize the cOLERegName property with the appropriate name of the Microsoft Excel Application object as it appears in the registration database. Secondly, we need to setup a Destroy event method that sends the Quit command to Microsoft Excel whenever the object is being destroyed. The Destroy event method will fire whenever we explicitly release a Microsoft Excel object, or whenever the Microsoft Excel object goes out of scope.
Note that we do not tell Microsoft Excel to quit if the internal flag, lCloseAppWhenDone, is set. This flag will be .F. if Microsoft Excel was already running when we created an instance of this class. By providing this functionality, we are making Microsoft Excel work more closely to resemble Microsoft Word, where the application is closed when the reference to it is released from memory.
DEFINE CLASS Excel AS OLEApplication *-- Inherited properties cOLERegName = "Excel.Application" *-- Methods FUNCTION Destroy() IF TYPE("this.oOLEApp") == "O" AND ; this.lCloseAppWhenDone this.oOLEApp.Quit() ENDIF ENDFUNC ENDDEFINE
Our second subclass deals with intricacies of dealing with Microsoft Word. Not that Microsoft Word is any more difficult to deal with than Microsoft Excel, it just responds differently in different ways. I mentioned earlier that the GETOBJECT() function does not work properly with Microsoft Word to check if an instance is running. We therefore turn to DDE to attempt to establish a link with Microsoft Word. If we are successful, we know that Microsoft Word is running, so we immediately terminate the link.
DEFINE CLASS WinWord AS OLEApplication *-- Inherited properties cOLERegName = "Word.Basic" *-- Methods FUNCTION Init() IF !OLEApplication::Init() RETURN .F. ENDIF this.oOLEApp.AppMinimize() ENDFUNC PROTECTED FUNCTION AppRunning() LOCAL lnChannel, ; llRunning, ; llDDEOldSafety llDDEOldSafety = DDESETOPTION("Safety") *-- Prevent the prompt to start the application =DDESETOPTION("Safety", .F.) *-- Try to establish a link on the System topic lnChannel = DDEINITIATE("WinWord", "System") IF lnChannel <> -1 *-- It's running this.lCloseAppWhenDone = .F. =DDETERMINATE(lnChannel) llRunning = .T. ENDIF =DDESETOPTION("Safety", llDDEOldSafety) RETURN llRunning PROTECTED FUNCTION GetCurrentInstance() RETURN CREATEOBJECT(this.cOLERegName) ENDFUNC ENDDEFINE
Notice that we have also defined a new implementation for the GetCurrentInstance() method. This is because of Microsoft Word's inability to respond to the GETOBJECT() method. To get the current instance of Microsoft Word, we can safely use the CreateObject() method.
It is interesting to note that Microsoft Word does not support creating multiple instances of itself through OLE Automation, while Microsoft Excel does. The use of the classes presented here will prevent you from having to worry about inadvertently creating another instance of Microsoft Excel.
Another interesting difference between these two applications is that when releasing a reference to Microsoft Word that was started by OLE Automation, the Microsoft Word instance is automatically terminated. However, a reference to Microsoft Excel will not terminate in this fashion. Instead, we must sent the Quit method to the Microsoft Excel object, explicitly instructing it to terminate. Furthermore, Microsoft Excel will terminate even if the reference to it was created from a previously running instance.
Although you could if you wanted to, you probably won't want to create custom methods for every single method of every single object in an OLE server application. Instead, you may want to provide a quick way to indirectly access the properties and methods of the application object itself, without removing it's protected status. If we were to simply provide direct access to the application instance, we would loose control over it. The user would then have the ability to disrupt the environment by incorrectly setting properties or calling methods that we may not want them to call. Instead, we can keep the application instance as a protected member of our class, and provide three methods that provide indirect access to the internal application instance.
The first two methods are implemented the same way. They are intentionally left as two separate methods because they serve two different purposes, and also to make it easier to subclass in the future.
*-- This method takes a method name as a parameter *-- and executes it. FUNCTION Do(tcMethod) RETURN EVAL("this.oOLEApp." + tcMethod) ENDFUNC *-- This method takes a property name as a parameter *-- and returns its value. (It can even return references *-- to container objects). FUNCTION Get(tcProperty) RETURN EVAL("this.oOLEApp." + tcProperty) ENDFUNC *-- This method takes a property name and a value as *-- parameters, and sets the value of the property *-- to the value parameter. FUNCTION Set(tcProperty, tuValue) LOCAL lcCommand lcCommand = "this.oOLEApp." + tcProperty + "=" lcCommand = lcCommand + this.ConvertToChar(tuValue) &lcCommand ENDFUNC
We'll add just one more method here to convert a generic parameter of any type to type character. This method could just as easily have been implemented as a standalone function in it's own PRG, or as a function of a procedure file. For now, it remains a protected method of this class.
*-- Takes a parameter of any type and converts it *-- a character string for use in the Set method. PROTECTED FUNCTION ConvertToChar(tuParam) LOCAL lcRetVal, ; lcType lcRetVal = "" lcType = TYPE("tuParam") DO CASE CASE lcType = "C" lcRetVal = "'" + tuParam + "'" CASE INLIST(lcType, "N", "B") lcRetVal = STR(tuParam) CASE lcType = "L" lcRetVal = IIF(tuParam, ".T.", ".F.") ENDCASE RETURN lcRetVal ENDFUNC
And that's all there is to it. Through these methods, we provide access to the internal application instance through a custom interface to the class. We could implement a set of rules governing which methods we want the user to be able to call, thereby protecting the users of the class from doing anything they aren't supposed to. If we desire, we could ultimately remove these three methods (actually four if you include the ConvertToChar utility method), and add custom methods that perform specific tasks we want to accomplish. You don't have to restrict yourself by mapping each method of every object. Rather, you could create higher level methods that perform higher level tasks, which shield the user of the class from having to know the details of the OLE Automation commands that are required to perform the desired functionality. We could do this gradually as application requirements change, while still retaining the core functionality we have here.
OOP and OLE Automation—perfect together.