The previous two chapters covered the nuts and bolts of OOP in Visual FoxPro. You learned the applicable concepts behind object orientation in Visual FoxPro in Chapter 13, "Introduction to Object-Oriented Programming," and you learned how classes are created in Chapter 14, "OOP with Visual FoxPro."
In this chapter, you learn the typical types of classes you create with Visual FoxPro. Different categories of classes are investigated and explained, and examples of each type of class are presented as well.
Before I go further, I must say that there is no realistic limit to the types of classes you can create with Visual FoxPro (unless you think there are limits to your imagination). For the most part, the classifications presented in this chapter should be representative of what you can do. Don't let anyone tell you that a class you have dreamed up is invalid because it is not listed or does not fit into a category detailed in this or any book. You are the master of your system's destiny. Don't let anyone tell you otherwise.
The types of classes created in this chapter can be placed in two general categories: visual classes and nonvisual classes.
A visual class is a class designed for display purposes. For example, a form is a visual class; so is a CommandButton and a CheckBox. The main purpose behind visual classes is for interface (usually called a Graphical User Interface or GUI) work.
Within this class category you can typically design classes in the following subcategories:
Single controls
Combination controls
Containers
Forms
Toolbars
A Terminology Issue |
One of the base classes that comes built-in to Visual FoxPro is named control. In addition to this base class, all other classes used within a form (such as TextBox, CommandButton, and so on) are also given the generic name of control. Sound confusing? You're right, it is. |
Showing Classes in Print |
I want to take a moment to explain the code presented in this chapter. You develop classes in Visual FoxPro visually (using the Visual Class Designer). However, for the purpose of this chapter, code is the best way to present the contents of a class. |
A single control is a class designed to be used as a control on a form. This type of class is based on any FoxPro form control class that does not support the capability to add additional objects to itself. These classes include CommandButtons, TextBoxes, Labels, and so on.
There are two kinds of single control classes you typically create. The first kind is a single control class designed to set the default of the display characteristics for the control for future use. For example, if you were to decide on a standard font for all your controls (Windows 95 uses 9-point Arial as its standard), you could create single control classes with the proper settings and then subclass them. This ensures a common look throughout your applications.
The second type of single control classes are created after you are done with the standard look and feel. The classes you create from there on will in all likelihood be for functionality. I will illustrate this second type of class with a few examples.
The OK Command Button A good example of a single control class is an OK button, which, when pressed, releases the form it is on. OkButton is a class that does this.
The following code shows a sample OK button:
* Class.............: Okbutton * Author............: Menachem Bazian, CPA * Notes.............: Exported code from Class Browser. ************************************************** *-- Class: okbutton *-- ParentClass: CommandButton *-- BaseClass: CommandButton *-- CommandButton that releases the form it's on. * DEFINE CLASS okbutton AS CommandButton AutoSize = .F. Height = 29 Width = 41 Caption = "OK" Default = .T. Name = "okbutton" PROCEDURE Click RELEASE thisform ENDPROC ENDDEFINE * *-- EndDefine: okbutton **************************************************
This button can be dropped on any form. When it is clicked, it will release the form. Figure 15.1 shows what the OK button looks like when you drop it on a form.
Figure 15.1 : A form with an OK button.
Notice that the method for releasing the form is to use RELEASE THISFORM in the Click event. The THISFORM keyword is mentioned in Chapter 14. This command releases the form the OK button is on.
By setting the Default property to .T., the command button's Click event is made to fire automatically when the user presses the Enter key when the cursor is not in an EditBox control. Conversely, if you want the button's Click event to fire when the user hits the Esc key, you should set the Cancel property of the button to .T..
Subclassing a Single Control Class Technically, every class created in Visual FoxPro is a subclass of another class. Even the Custom-based classes are subclasses (Custom is the name of a FoxPro base class).
After you create a class, you can subclass it. How this works is detailed in Chapter 14, but here's a little example of it. Suppose you want a special version of the OK button that has tooltip text attached to it. (Tooltip text is the text that pops up when the mouse hovers over a control.) The following code, which creates a class called OkButtonWithToolTip, shows how to do this:
* Class.............: okbuttonwithtooltip * Author............: Menachem Bazian, CPA * Notes.............: Exported code from Class Browser. ************************************************** *-- Class: okbuttonwithtooltip (d:\data\docs\books\vfu\code\oop2.vcx) *-- ParentClass: okbutton (d:\data\docs\books\vfu\code\oop2.vcx) *-- BaseClass: CommandButton *-- Subclass of an OK button - adds a ToolTip. * DEFINE CLASS okbuttonwithtooltip AS okbutton Height = 29 Width = 41 ToolTipText = "Releases the form" Name = "okbuttonwithtooltip" ENDDEFINE * *-- EndDefine: okbuttonwithtooltip **************************************************
That's all there is to it. Notice that there is very little code attached to this button. The Click event is not represented here at all; it is inherited from the parent class. In fact, the only relevant code in this class is the value in the ToolTipText property.
For the record, most controls have a ToolTipText property. You might notice that although the property has been filled, the tooltip text does not show up when the mouse hovers over the control. If this happens, check out the ShowTips property on the form and make sure that it is set to .T.. Figure 15.2 shows an example of what you should expect to see.
Figure 15.2 : A sample button with tooltip text.
To make sure that your tooltip shows up all the time, you can place code in the MouseMove event that checks the setting of the ShowTips property on the form and sets it to .T.. If you're wondering why the code does not go in the Init of the button, the answer has to do with the order in which controls are initialized with a container. You will learn about this in a little bit.
Why Subclass? OkButtonWithToolTip seems to be a rather silly example of a subclass. After all, adding tooltip text is very simple. Why subclass the button at all? Why not make the change to the OK button itself? It's a judgment call, certainly. However, it is vital to bear in mind that changing a class will change every instance of that class as well as all the descendant subclasses. If you modify the base class, you will be making the assumption that all instances of the OK button will have this tooltip. This assumption might not be a valid one. Hence, you choose to subclass.
Another Single Control Example Believe it or not, there are a series of published standards for Windows GUI interfaces. In fact, there is one standard for the Windows 3.1-based interface, which applies to Windows 3.1, Windows for Workgroups, and Windows NT 3.x, and another for Windows 95 and Windows NT 4.0. Windows 98 and Windows NT 5.0 have still other interface guidelines. One standard for Windows applications calls for error messages to be placed in a MessageBox()-type window. Visual FoxPro has the capability to automatically display an error message when the Valid method returns .F.. The error message is the character string returned from the ErrorMessage method. The problem is that the ErrorMessage method puts the error message in the form of a WAIT WINDOW instead of a MessageBox.
The ErrorMessage event fires automatically when the Valid method returns .F., that is, Valid fires when the control tries to lose focus. Therefore, the solution would seem to be to put a MessageBox()-type message in the ErrorMessage method and not return a value.
However, there is one little problem. If the ErrorMessage does not return a string, Visual FoxPro displays a default message of Invalid Input. The only way to turn this off is to set the SET NOTIFY command to OFF.
Not a big problem, right? Unfortunately, other classes might change this setting or might rely on the setting to be ON. In effect, you cannot rely on the SET NOTIFY setting unless you set it yourself. To do so, use SET NOTIFY OFF when the control gains focus and then set it back the way it was on the way out (that is, when the control loses focus).
NoNotifyTextBox is a sample of this type of control. A protected property called cNotifySetting has been added to hold the setting of SET NOTIFY before the class changes it. Here's the code:
* Class.............: NoNotifyTextBox * Author............: Menachem Bazian, CPA * Notes.............: Exported code from Class Browser. ************************************************** *-- Class: nonotifyTextBox *-- ParentClass: TextBox *-- BaseClass: TextBox *-- Text box that sets NOTIFY off for error messaging. * DEFINE CLASS nonotifyTextBox AS TextBox Height = 24 Width = 113 Name = "nonotifyTextBox" *-- The setting of SET NOTIFY when the control got focus. PROTECTED cnotifysetting PROCEDURE LostFocus LOCAL lcNotify lcNotify = this.cNotifySetting SET NOTIFY &lcNotify ENDPROC PROCEDURE GotFocus this.cNotifySetting = SET("notify") SET NOTIFY OFF ENDPROC ENDDEFINE * *-- EndDefine: nonotifyTextBox **************************************************
Notice that the property cNotifySetting is protected. This follows the guidelines discussed in the last chapter for protecting properties. Because this property has no use to the outside world and, in fact, could harm the system if changed by the outside world, you just hide it and forget about it.
After you create classes like NoNotifyTextBox, you can use them in forms or subclass them as you see fit and be sure that you will have the behavior you are looking for.
Sometimes you might want to combine several controls together to operate as one. For example, take the task of specifying the name of an existing file on a form. This requires two controls interacting as one: a TextBox that enables the user to enter a filename to validate a file's existence and a CommandButton that displays a GetFile() box and places the results in the TextBox.
You could create each control separately and drop them individually on a form. However, because both controls are coming together to do one task, it makes sense to make one control out of the two and drop them on the form as one control. This achieves several goals. First, you can encapsulate all the behavior and information in one place. Second, it makes it easier to add this functionality to a form. Third, you can duplicate the look and functionality on other forms easily. Fourth, it avoids any code at the form level (code necessary to get the two controls to interact).
The base class for this is the control class. The control class is a class designed to create composite classes where several controls come together to act as one. Conceptually, the control class is not much of anything except a package into which controls can be placed. You will learn the specifics of the control class "package" in the following section, "The General Idea." First, Listing 15.1 shows a sample class called GetAFile, which illustrates what you try to accomplish with a combination control.
* Class.............: Getafilecontrol * Author............: Menachem Bazian, CPA * Notes.............: Exported code from Class Browser. ************************************************** *-- Class: getafilecontrol *-- ParentClass: control *-- BaseClass: control *-- A combination of controls that allow a user to select an existing file. * DEFINE CLASS getafilecontrol AS control Width = 358 Height = 26 BackStyle = 0 BorderWidth = 0 cvalue = "" *-- The caption to display on the GETFILE box. cdisplaycaption = "Please select a file." *-- Caption for the OPEN button. See GetFile() for more information on this. copenbuttoncaption = "Open" *-- File extensions to allow for. See GetFile() *-- for more information on this. cfileextensions = "" *-- The type of buttons on the GetFile() dialog. *-- See GetFile() for more information on this. nbuttontype = 0 *-- Should the path shown be a minimum path or not? lminimumpath = .T. Name = "getafilecontrol" ADD OBJECT cmdgetfile AS CommandButton WITH ; Top = 0, ; Left = 330, ; Height = 24, ; Width = 24, ; Caption = "...", ; Name = "cmdGetFile" ADD OBJECT cfilenameTextBox AS nonotifyTextBox WITH ; Value = "", ; Format = "!", ; Height = 24, ; Left = 0, ; Top = 0, ; Width = 325, ; Name = "cFileNameTextBox" PROCEDURE Refresh this.cFileNameTextBox.Value = this.cValue this.cFileNameTextBox.SetFocus() ENDPROC *-- Accepts a string parameter and validates that it is an existing file. PROCEDURE validatefilename LPARAMETERS tcFileName LOCAL llRetVal llRetVal = EMPTY(tcFileName) OR FILE(ALLTRIM(tcFileName)) IF !llRetVal =MessageBox("File does not exist: " + ALLTRIM(tcFileName)) ENDIF tcFileName = ALLTRIM(tcFileName) IF llRetVal this.cValue = tcFileName ENDIF RETURN llRetVal ENDPROC *-- Display the files for the user to select from with a GetFile() dialog. PROCEDURE displayfiles LOCAL lcValue, lcDialogCaption, lcOpenButtonCaption, lnButtonType lcDialogCaption = this.cDisplayCaption lcOpenButtonCaption = this.cOpenButtonCaption lcFileExtensions = this.cFileExtensions lnButtonType = this.nButtonType lcValue = GETFILE(lcFileExtensions, ; lcDialogCaption, ; lcOpenButtonCaption, ; lnButtonType) IF !EMPTY(lcValue) IF this.lminimumpath lcValue = SYS(2014, lcValue) ENDIF this.cValue = lcValue ENDIF this.refresh() ENDPROC PROCEDURE cmdgetfile.Click this.parent.DisplayFiles() ENDPROC PROCEDURE cfilenameTextBox.Valid RETURN this.parent.validatefilename(this.value) ENDPROC ENDDEFINE * *-- EndDefine: getafilecontrol **************************************************
The General Idea First, some general theory: The idea is for the TextBox and the CommandButton to work together to enable the user to select a file. The TextBox provides for manual, type-it-in functionality. The CommandButton brings up a GetFile() dialog box from which the user can select a file. If a file is selected, it is shown in the TextBox.
The job of the container around these controls (that's the controls referred to in the class names) is to give the interface to the controls. From the outside world, the two controls packaged together are one control. The fact that a TextBox and a CommandButton are working together is something the developer of the class needs to know, but not something the developer using the control in an application needs to know. The job of the interface is to communicate with the outside world in terms of the options the user wants to set when selecting a file. An interface also holds the name of the file selected. This brings me to my next point.
The Control Package's Custom Properties The package around the controls has the following custom properties added to it:
cvalue = "" cdisplaycaption = "Please select a file." copenbuttoncaption = "Open" cfileextensions = "" nbuttontype = 0 lminimumpath = .T.
The cValue property is the name of the file selected by the user. I used the name cValue because it is equivalent to the Value property of most data controls: It holds the value returned by the control. By the way, if you're wondering about the c in cValue, it's a common naming convention for all custom properties: The first character of the name indicates the data type of the property (which in this case is Character).
The cDisplayCaption property is the message that displays on the GetFile() box, which is displayed by the command button. The property name is based on the name of the corresponding parameter discussed in the GetFile() help topic.
The cOpenButtonCaption property is the caption for the Open button. By default, the name of the button on the GetFile() dialog box is OK; however, this class sets the default to Open instead. Again, the name is a naming convention version of the parameter name in the help file.
The cFileExtensions property is a list of file extensions to show in the GetFile() box (you can still select any file you like, by the way). The list of file extensions is separated by semicolons or vertical bars (|). Visual FoxPro automatically parses out the file extensions in the GetFile() box. Again, the name is a naming convention version of the parameter name in the help file.
The nButtonType property is a numeric property that is also used for the GetFile() box. The number defaults to 0, which shows an OK button (which is really named Open by default) and Cancel button. A value of 1 shows OK, New, and Cancel buttons. A value of 2 shows OK, None, and Cancel buttons. Once again, the name is a naming convention version of the parameter name in the help file.
The lMinimumPath property specifies whether a minimum or absolute path is used. A filename specified with a path can be shown in one of two ways. The path can be an absolute path, in other words, the specified path is the path you want to store. The other way is to store a relative path, which adjusts the path to show the minimum path designation to get to the file from a directory. An absolute path, which is the type of value returned from GetFile(), can be converted to a relative path with SYS(2014). If lMinimumPath is set to .T. (as it is by default), the filename returned by the GetFile() dialog box is adjusted with SYS(2014) against the current directory.
The purpose of naming properties in this manner is to give the developer using the class a clue as to the values allowed, that is, it makes the class a little easier to use and understand.
Custom and Overridden Methods The Refresh method sets the text box's Value property to match the cValue property of the control package. The SetFocus call to the TextBox puts the cursor in the TextBox for editing.
ValidateFileName is a custom method that tests the validity of a filename entered in the text box and is called by the text box's Valid method.
DisplayFiles is the method that actually displays the GetFile() dialog box and uses the properties of the control package as part of the GetFile() call. After GetFile() is done and the user has made a selection, the selected value is placed in the control package's cValue property. A call to Refresh keeps everything in synch.
Object Members The object members are CmdGetFile and cFileNameTextBox.
CmdGetFile This is a command button. The Click method cmdGetFile calls DisplayFiles, which displays a GetFile() based on the settings in the control package. The returned value is then stored to the cValue property of the package, and the box's Refresh method is called. The Refresh method puts the text box in synch.
cFileNameTextBox This control enables the user to enter the filename manually. It is based on the NoNotifyTextBox class created previously in this chapter. Basing cFileNameTextBox on NoNotifyTextBox is a good example of how classes are reused in other classes.
The Valid method in this control checks the entered filename to make sure it is valid. An empty filename is considered valid; if it weren't, the user would have no way of exiting the TextBox control to get to the CommandButton. If the text box's Valid method returns .F., the ErrorMessage event calls the ErrorMessage method, which in turn displays an error message telling the user that file was not found.
If the entered filename is valid, the filename is stored in the cValue property of the control package.
Using the Combination Control in a Form As far as the outside world is concerned (the outside world is anything outside GetAFileControl), the operative property is the cValue property. In order to get the name of the file entered or selected by the user, the outside world just queries the cValue property. In order to set up the control, the outside world sets the properties discussed previously. Figure 15.3 shows a form with GetAFileControl on it.
Figure 15.3 : A form with GetAFileControl.
As far as the outside world is concerned, the only control that exists is GetAFileControl. The embedded TextBox and CommandButton controls do not exist. In other words, if this combination control were dropped on a form with the name GetAFileControl1, attempting to query GetAFileControl1.cFileNameTextBox.Value from the outside world would generate an error.
Why is this so? To understand why, you need to take a little closer look under the hood of the control class.
Now that you have seen an example of a combination control, take a step back and learn the technical issues related to working with this class.
First of all, although a control-based class is a composite class with additional member objects, the control class automatically makes all its member objects private. This means that anything using the class cannot see those member objects.
To illustrate this, take a look at Figure 15.4, which shows the property window of a form with GetAFileControl placed on it. I've expanded the list of objects to make the point. Notice that only the control package is on the list. The individual objects inside the control package do not exist as far as the form is concerned. You access the individual objects within the combination control class.
Figure 15.4 : Objects shown for the GetAFileControl instance.
This behavior makes perfect sense. Because you're creating a control as far as the outside world is concerned, all the components of the control are one object. This does introduce some interesting issues, which are discussed next one at a time.
Communicating with Combination Controls By definition, the outside world can only communicate with a combination control through the package. This means that any information passing first goes to the package; the package must then transmit the information to the individual members.
A good example of this is the cValue property added to the GetAFileControl class. cValue can be set from the outside world but will be shown in the TextBox only when the Refresh method is called-it is the Refresh method's responsibility to transmit the value to the TextBox. Likewise, TextBox and CommandButton have the responsibility of telling the package about changes to cValue that happen as a result of actions taken within their control.
You can take advantage of a new feature in Visual FoxPro 6 by using the assign and access methods for the cValue property. By adding a method named cValue_access, you are creating an access method. An access method is like an event method that fires whenever the property it is associated with is accessed. In the GetAFileControl class, you might add a cValue_access method like the one shown in the following:
PROCEDURE cValue_access IF THIS.cValue <> THIS.cFileNameTextBox.Value THIS.cValue = THIS.cFileNameTextBox.Value ENDIF RETURN THIS.cValue
This access method will run whenever any outside object tries to read the value of the cValue property, and its code ensures that the value of the TextBox and the cValue property are in synch with each other.
This also means that any methods to which the outside world needs access must be contained at the package level. For example, if the Click method of the CommandButton were applicable to the outside world, a method would have to be added to the package that the CommandButton would call (as opposed to placing the code directly within the CommandButton's Click method). Alternatively, the code could be placed in the CommandButton's Click method, but you would still have to add a method to the package for the outside world to run the CommandButton's Click method. The method added to the package would just call the Click method of the CommandButton.
Adding Objects at Runtime Control classes do not have an AddObject() method; therefore, you cannot add controls to the package at runtime.
Subclassing Combination Controls There are a few ramifications regarding subclassing combination controls.
Adding Controls to a Subclass You cannot add controls to the package in a subclass (just as you cannot add a control to a subclass of the CommandButton class).
Accessing Package Contents in a Subclass The objects contained in the combination control's package are not visible when you subclass it. This means that any methods or properties you might want to modify in a subclass must be hooked to the package as opposed to one of the member objects. This requires some care in designing the class hierarchy.
TIP |
You can provide for this situation if you first fully define all the control classes that you will use in the package as classes, with all the necessary code, before you put them in the package. |
If you look at GetAFileControl, you'll notice that all the methods for the TextBox and the CommandButton call methods on the package. In effect, this hedges your bets. You should do a little more work up front to preserve flexibility down the road.
A Final Word As container-type classes go, the control class does have limitations, but the limitations of the control class are its strength. It gives you the opportunity to create tightly controlled classes to use on your forms. However, many developers find control classes limiting because of the lack of capability to add controls at runtime, to add controls to a subclass, or to access the controls within the package can present a restrictive working environment.
The container class, on the other hand, gives you the ability to create composite classes with more flexibility. This is the next topic.
A container class is similar to the control class with one major exception: The controls in a container class's package are visible to the outside world (unless specifically protected in the class).
By the way, there are many classes that support containership. For example, a form is a type of container class (only more specialized). Page frames and grids are also examples of classes that support containership. This discussion focuses on the container class, but many of the discussions are applicable to the other container-type classes as well.
Container classes are wonderful for a whole host of different jobs. In fact, any time multiple objects need to be brought together, a container can do the job. (I usually prefer to combine controls in a container to accomplish a task.)
To illustrate the difference between a control class and a container class, I'll redefine the GetAFileControl class to work off the container class instead of the control class. First, Listing 15.2 shows the exported code.
* Class.............: Getafilecontainer * Author............: Menachem Bazian, CPA * Notes.............: Exported code from Class Browser. ************************************************** *-- Class: getafilecontainer *-- ParentClass: container *-- BaseClass: container *-- A combination of controls that allow a user to select an existing file. * DEFINE CLASS getafilecontainer AS container Width = 358 Height = 26 BackStyle = 0 BorderWidth = 0 cvalue = "" *-- The caption to display on the GETFILE box. cdisplaycaption = "Please select a file." *-- Caption for the OPEN button. See GetFile() for more information on this. copenbuttoncaption = "Open" *-- File extensions to allow for. See GetFile() for more information on this. cfileextensions = "" *-- The type of buttons on the GetFile() dialog. See GetFile() for more information on this. nbuttontype = 0 *-- Should the path shown be a minimum path or not? lminimumpath = .T. Name = "getafilecontainer" ADD OBJECT cmdgetfile AS cmdGetfile WITH ; Top = 0, ; Left = 330, ; Height = 24, ; Width = 24, ; Caption = "...", ; Name = "cmdGetFile" ADD OBJECT cfilenameTextBox AS cFileNameTextBox WITH ; Value = "", ; Format = "!", ; Height = 24, ; Left = 0, ; Top = 0, ; Width = 325, ; Name = "cFileNameTextBox" *-- Accepts a string parameter and validates that it is an existing file. PROCEDURE validatefilename LPARAMETERS tcFileName LOCAL llRetVal llRetVal = EMPTY(tcFileName) OR FILE(ALLTRIM(tcFileName)) IF !llRetVal =MessageBox("File does not exist: " + ALLTRIM(tcFileName)) ENDIF tcFileName = ALLTRIM(tcFileName) IF llRetVal this.cValue = tcFileName ENDIF RETURN llRetVal ENDPROC *-- Display the files for the user to select from with a GetFile() dialog. PROCEDURE displayfiles LOCAL lcValue, lcDialogCaption, lcOpenButtonCaption, lnButtonType lcDialogCaption = this.cDisplayCaption lcOpenButtonCaption = this.cOpenButtonCaption lcFileExtensions = this.cFileExtensions lnButtonType = this.nButtonType lcValue = GETFILE(lcFileExtensions, ; lcDialogCaption, ; lcOpenButtonCaption, ; lnButtonType) IF !EMPTY(lcValue) IF this.lminimumpath lcValue = SYS(2014, lcValue) ENDIF this.cValue = lcValue ENDIF this.refresh() ENDPROC PROCEDURE Refresh this.cFileNameTextBox.Value = this.cValue this.cFileNameTextBox.SetFocus() ENDPROC ENDDEFINE DEFINE CLASS cmdGetFile AS CommandButton PROCEDURE Click This.parent.DisplayFiles ENDPROC ENDDEFINE DEFINE CLASS cFileNameTextBox AS NoNotifyTextBox PROCEDURE Valid RETURN This.parent.validatefilename(this.value) ENDPROC ENDDEFINE DEFINE CLASS nonotifyTextBox AS TextBox Height = 24 Width = 113 Name = "nonotifyTextBox" *-- The setting of SET NOTIFY when the control got focus. PROTECTED cnotifysetting PROCEDURE LostFocus LOCAL lcNotify lcNotify = this.cNotifySetting SET NOTIFY &lcNotify ENDPROC PROCEDURE GotFocus this.cNotifySetting = SET("notify") SET NOTIFY OFF ENDPROC ENDDEFINE * *-- EndDefine: getafilecontainer **************************************************
At first glance, the code in Listing 15.2 does not seem very different than the GetAFileControl class from the previous section. In fact, it isn't. One thing that I changed was to define each of the controls as a class before I put them in the container. Another difference is how the two classes (GetAFileControl and GetaFileContainer) operate within the form.
Figures 15.5 and 15.6 show a key difference between when you are working with control-based classes as opposed to container-based classes. Notice the value of _screen.activeform.activecontrol.name, which is tracked in the Debug window, and how it differs from the top control (an instance of GetAFileControl) and the lower container (an instance of GetaFileContainer).
Figure 15.5 : A form with GetAFileControl active.
Figure 15.6 : A form with GetaFileContainer active.
To the naked eye, the two controls seem the same. Behind the scenes, however, they are very different-the control version shows no ActiveControl for the form regardless of whether the TextBox has the focus or the CommandButton has the focus. The container version, on the other hand, shows the name of the TextBox as the name of the active control.
By the way, this is a good example of the use of _screen to assist in debugging. You might not always know the name of the variable behind a form, but you can usually count on _screen.activeform to give you a reference to the active form.
TIP |
In Visual FoxPro 6, you can use _VFP and _SCREEN interchangeably. |
Order of Inits So, you have a package with multiple objects in it. You put code in one object's Init that references the container. But when you try to instantiate the package, you get an error message in the Init code and the package refuses to instantiate. What did you do wrong?
The answer to this puzzle lies in the order in which the objects are created. As it turns out, the container package is the last object to get created. The objects inside are created first, and the container is created only after all the objects inside have successfully been created.
As to the order in which the internal objects instantiate, the best way to find out is to look at the order in which the objects appear in the property sheet. For best results, try not to put code in any of the Init events that depends on the order in which objects instantiate. You'll be a lot safer that way.
Navigator-Another Container Class A container of CommandButtons used for navigating through a table in a form is a good example of a container class. Listing 15.3 shows an example of this kind of class called Navigator.
* Class.............: Navigator * Author............: Menachem Bazian, CPA * Notes.............: Exported code from Class Browser. ************************************************** *-- Class: navigator *-- ParentClass: container *-- BaseClass: container * DEFINE CLASS navigator AS container Width = 350 Height = 32 BackStyle = 0 BorderWidth = 0 Name = "navigator" ADD OBJECT cmdnext AS cmdNext WITH ; Top = 0, ; Left = 0, ; Height = 31, ; Width = 60, ; Caption = "Next", ; Name = "cmdNext" ADD OBJECT cmdprev AS cmdPrev WITH ; Top = 0, ; Left = 72, ; Height = 31, ; Width = 60, ; Caption = "Prev", ; Name = "cmdPrev" ADD OBJECT cmdtop AS cmdTop WITH ; Top = 0, ; Left = 144, ; Height = 31, ; Width = 60, ; Caption = "Top", ; Name = "cmdTop" ADD OBJECT cmdbottom AS cmdBottom WITH ; Top = 0, ; Left = 216, ; Height = 31, ; Width = 60, ; Caption = "Bottom", ; Name = "cmdBottom" ADD OBJECT cmdok AS okbuttonwithtooltip WITH ; Top = 0, ; Left = 288, ; Height = 31, ; Width = 60, ; Name = "cmdOK" ENDDEFINE DEFINE cmdNext AS CommandButton PROCEDURE Click SKIP 1 IF EOF() =Messagebox("At end of file!", 16) GO BOTTOM ENDIF thisform.refresh() ENDPROC ENDDEFINE DEFINE CLASS cmdPrev AS CommandButton PROCEDURE Click SKIP -1 IF BOF() =Messagebox("At beginning of file!", 16) GO TOP ENDIF thisform.refresh() ENDPROC ENDDEFINE DEFINE CLASS cmdTop AS CommandButton PROCEDURE Click GO TOP thisform.refresh() ENDPROC ENDEFINE DEFINE CLASS cmdBottom AS CommandButton PROCEDURE Click GO BOTTOM thisform.refresh() ENDPROC ENDDEFINE DEFINE CLASS okbuttonwithtooltip AS okbutton Height = 29 Width = 41 ToolTipText = "Releases the form" Name = "okbuttonwithtooltip" ENDDEFINE * *-- EndDefine: navigator **************************************************
Each button on the container executes the necessary navigation instructions and then executes a call to the host form's Refresh method. It might seem strange to put code to work with a form on a class that is not yet physically part of a form, but that's OK. When you document the class, just make sure to specify how it is meant to be used.
In order to use the Navigator class, all you need to do is drop it on a data entry form and you have the ability-without coding one line-to move within the file. Figure 15.7 shows a sample data entry form with this class dropped on it.
Figure 15.7 : A form with a Navigator container.
When this class is dropped on a form, as shown in Figure 15.8, a view of a form's property window and all the controls are available for editing and on-the-fly subclassing. You can even extend the container and add another control to it in the form. The additional control, of course, is not added to the class in the VCX file; it's a special version for that form only.
Figure 15.8 : The property window of a container.
The next bit of code is a representation of a form that adds a New button to the container (it's available as MoreBtns.SCX).
* Form..............: Form1 * Author............: Menachem Bazian, CPA * Notes.............: Exported code from Class Browser. ************************************************** *-- Form: form1 (morebtns.scx) *-- ParentClass: form *-- BaseClass: form * DEFINE CLASS form1 AS form Top = 0 Left = 0 Height = 182 Width = 463 DoCreate = .T. BackColor = RGB(192,192,192) BorderStyle = 2 Caption = "Form1" Name = "Form1" ADD OBJECT text1 AS textbox WITH ; BackColor = RGB(192,192,192), ; ControlSource = "test.cmembname", ; Height = 24, ; Left = 48, ; Top = 24, ; Width = 113, ; Name = "Text1" ADD OBJECT text2 AS textbox WITH ; BackColor = RGB(192,192,192), ; ControlSource = "test.cmembtype", ; Height = 24, ; Left = 48, ; Top = 60, ; Width = 113, ; Name = "Text2" ADD OBJECT navigator1 AS navigator WITH ; Top = 120, ; Left = 12, ; Width = 421, ; Height = 32, ; Name = "Navigator1", ; cmdNext.Name = "cmdNext", ; cmdPrev.Name = "cmdPrev", ; cmdTop.Name = "cmdTop", ; cmdBottom.Name = "cmdBottom", ; cmdOK.Top = 0, ; cmdOK.Left = 360, ; cmdOK.Height = 31, ; cmdOK.Width = 61, ; cmdOK.Name = "cmdOK" ADD OBJECT form1.navigator1.cmdnew AS commandbutton WITH ; Top = 0, ; Left = 288, ; Height = 31, ; Width = 61, ; Caption = "New", ; Name = "cmdNew" PROCEDURE cmdnew.Click APPEND BLANK thisform.refresh() ENDPROC ENDDEFINE * *-- EndDefine: form1 **************************************************
Notice how the additional command button (cmdNew) is added within the code. An ADD OBJECT command adds the CommandButton to the container. This is just one of the powerful capabilities of the container class: You can add objects to it on-the-fly.
Figure 15.9 shows what the form looks like. As you can see, there is no visible indication that the additional CommandButton was not part of the original class.
Figure 15.9 : A form with an additional CommandButton.
The Flexibility of the Container Class The container class is a very versatile class. It has little display baggage and yet has the full capabilities one would expect from a container. You can add controls to it on-the-fly with ADD OBJECT and AddObject(), and you can combine virtually any controls to create combinations that are limited only by your imagination.
Think about two simple controls: a TextBox and a Timer control. Individually, they each have specific duties. Combine them in a container and you can create something totally different: a clock.
A Clock Class A clock, as a class, is simply a combination of a Timer and a TextBox, as shown in Listing 15.4.
* Class.............: Clock * Author............: Menachem Bazian, CPA * Notes.............: Exported code from Class Browser. ************************************************** *-- Class: clock *-- ParentClass: container *-- BaseClass: container * DEFINE CLASS clock AS container Width = 367 Height = 27 BorderWidth = 0 SpecialEffect = 2 ntimeformat = 0 Name = "clock" ADD OBJECT txtdate AS txtDate WITH ; Height = 22, ; Left = 5, ; Top = 3, ; Width = 250, ; Name = "txtDate" ADD OBJECT txttime AS textbox WITH ; Height = 22, ; Left = 268, ; Top = 3, ; Width = 77, ; ADD OBJECT tmrtimer AS trmTimer WITH ; Top = 0, ; Left = 120, ; Height = 24, ; Width = 25, ; Name = "tmrTimer" ENDDEFINE DEFINE CLASS txtDate AS TextBox Alignment = 0 BackColor = RGB(255,255,0) BorderStyle = 0 Value = (CDOW(date())+" "+CMONTH(date())+" "+ ; ALLT(STR(DAY(date())))+", "+ALLT(STR(YEAR(date())))) Enabled = .F. DisabledForeColor = RGB(0,0,0) DisabledBackColor = RGB(255,255,255) ENDEFINE DEFINE CLASS txtTime AS TextBox Alignment = 0 BorderStyle = 0 Value = (IIF(THIS.PARENT.TimeFormat = 0, ; IIF(VAL(SUBSTR(time(),1,2))>12,; ALLT(STR((VAL(SUBSTR(time(),1,2))-12)))+SUBSTR(time(),3,6),; time()),time())) Enabled = .F. DisabledForeColor = RGB(0,0,0) DisabledBackColor = RGB(255,255,255) ENDDEFINE DEFINE CLASS tmrTimer AS Timer Interval = 1000 PROCEDURE Timer this.Parent.txtDate.Value = CDOW(date()) + " " + ; CMONTH(date())+" "+ ; ALLT(STR(DAY(date()))) + ; ", "+ALLT(STR(YEAR(date()))) IF this.Parent.nTimeFormat = 0 this.Parent.txtTime.Value = ; IIF(VAL(SUBSTR(time(),1,2))>12, ; ALLT(STR((VAL(SUBSTR(time(),1,2))-12))) + ; SUBSTR(time(),3,6),time()) ELSE this.Parent.txtTime.Value = time() ENDIF ENDPROC ENDDEFINE * *-- EndDefine: clock **************************************************
The Timer control enables you to run a command or series of commands at timed intervals. When the Interval property is set to a value greater than zero, it controls the amount of time between executions of the timer's Timer event in milliseconds (1/1000 of a second units). When you combine this capability with the display characteristics of the TextBox control to display the time calculated by the timer, you have a clock.
Attached to the container is a custom property, nTimeFormat, that governs whether the time is shown in 12- or 24-hour format. Setting the property to 0 shows the time in AM/PM format, whereas setting the property to 1 shows the time in military format. Figure 15.10 shows what the clock looks like when you drop it on a form (it's on the Web site as a class called ClockForm in CHAP15.VCX).
Once a second the Timer event fires and recalculates the time. The results of the calculation are placed in the Value property of the txtTime text box.
CAUTION |
Remember always that a timer's interval is specified in milliseconds and not seconds. If you want a timer to fire once every second, give it an interval of 1000. Using an interval of 1 will cause your system to appear locked up because the timer is firing 1,000 times each second. |
Providing a Consistent Interface Notice the placement of the nTimeFormat property in the Clock class: It is placed on the container. Why put it there? You could have just as easily put the property on the Timer or even the TextBox.
Whenever you work with a composite class, it is very important to provide a consistent interface for the user of the control. It is crucial not to force the users of your classes to drill down into the member controls in order to accomplish what they need to do, especially when you are working with a container that can have many controls.
You should use the container to communicate with the outside world. Controls within the container can communicate with each other, but users should not have to communicate with the controls inside the container in order to use your classes. Chapter 16 shows some more examples of container-based controls.
There is not much to talk about regarding form-based classes that you have not already come across in the discussions of the Form Designer or other classes. Rather than discuss how to create form classes, I will show you how form classes are different from SCX-based forms.
Bringing Up the Form You are familiar with the DO FORM syntax; however, this syntax is no longer used to work with forms stored as classes in a VCX file. Instead, the familiar CreateObject() syntax, the means by which all objects are instantiated from their classes, is used. Notice that after a form is instantiated with CreateObject(), you must call the Show method to display it.
TIP |
In Visual FoxPro 6, you can use the NewObject() function to create objects from classes. This includes form classes as well as any other class. |
Making a Form Modal There are two ways to make a form modal. One is to set the WindowType property to 1 (0 is modeless). The other is to call the Show method with a parameter of 1. Here is an example:
oForm = CreateObject("Form") oForm.Show(1)
or
oForm = NewObject("Form") oForm.Show(1)
Modeless forms raise two issues when dealing with VCX-based forms: reading systems events and variable scoping.
Reading System Events Consider the following procedure:
*-- ShowForm.Prg SET CLASS TO junkme oForm = CreateObject("Form") oForm.Show()
If you run this program from the command window, you will see the form flash and then disappear. This happens because of the scoping of oForm. Because oForm is a memory variable, the procedure ends and oForm loses scope when the Show method finishes and the form appears. Because the variable is released, the form it references is released, too, and disappears. The main problem is that the program never stops. There is no wait state to tell Visual FoxPro to stop and read system events. This is not an issue with the DO FORM command, though, because it contains its own inherent wait state in which the form operates.
The answer to this problem is to issue a READ EVENTS command, which, in effect, is a replacement for the old foundation read. It tells Visual FoxPro to stop and read system events. Without it, the program does not stop and eventually terminates.
Variable Scoping Revisited The READ EVENTS command does not completely eliminate the issue of variable scoping. The READ EVENTS command stops the program so that it can read system events. You can still instantiate a form in a procedure and have it flash if the variable loses focus.
You need to be careful with the scoping of your form instance variables. Just issuing a Show method does not stop processing. When the Show method completes, control is returned to the calling program. A typical example is a form called from the Click event of a CommandButton. Unless something is done to keep the variable around, the variable loses scope and the form flashes. There are several strategies to deal with this problem, and all the strategies deal with the same issue: keeping the instance variable in scope.
Attaching the Variable to Another Object One way to keep the variable in scope is to attach the instance variable to the object calling the form. For example, consider the class shown in Listing 15.5.
Listing 15.5 15CODE05-A
Command Button Class That Launches Forms
* Class.............: Launchform * Author............: Menachem Bazian, CPA * Notes.............: Exported code from Class Browser. ************************************************** *-- Class: launchform *-- ParentClass: CommandButton *-- BaseClass: CommandButton * DEFINE CLASS launchform AS CommandButton Height = 29 Width = 94 Caption = "LaunchForm" *-- The form to Launch oform = .NULL. Name = "launchform" PROCEDURE Click this.oForm = CreateObject("Form") this.oForm.Show() ENDPROC ENDDEFINE * *-- EndDefine: launchform **************************************************
This CommandButton maintains scope as long as the form it is attached to maintains scope; therefore, any properties of the CommandButton retain scope, too. In this example, the form launched from within the Click event of the CommandButton would stick around.
A similar strategy is to attach the form instance variable to a public object. For example, it could be added to _screen. There is an example of this in Chapter 17.
Making the Variable Global Another way to keep the variable in scope is to make the variable holding the form reference global. This is not the most elegant of solutions, but it does work. Perhaps a more elegant method is to declare an array in the start up program for the application and use the array as the repository for forms you want to keep around.
TIP |
Global variables are not necessarily PUBLIC variables. A variable can be of three scopes, PUBLIC, PRIVATE, or LOCAL. PUBLIC variables exist from the moment they are created until they are either released with the RELEASE command or Visual FoxPro closes. PRIVATE variables exist from the moment they are created until the routine that created them ends (or they are released by the RELEASE command). LOCAL variables have the same scope as PRIVATE variables except that they are not visible to the routine called from the one that created them. |
The problem with the global approach is managing the public variables. Having many public variables in a system that can be used and modified in many programs can be a little hairy at times. It's really the same issue with using public variables in a system in general. Conventional wisdom seems to shy away from this type of solution, but it will work if you need it.
Data Environments One of the more significant differences between forms in an SCX file and form classes is in the area of data environments. You use a data environment to specify what tables, views, and relations are used in the form. When a form class is created, you cannot save a data environment with it. If you save a form as a class that had a data environment attached to it, the data environment is lost.
This might look terribly negative, but it really isn't. Although you lose the graphically created data environment capabilities, you can still create data environment classes in code (this is discussed in greater detail in Chapter 17). Also, there might be many cases in which the data environment for the form is not determined by the form but rather by another class on the form (for example, a business object, which is also discussed in Chapter 17).
If you don't want to get involved with data environment classes, you can still do what you want with relative ease. All you need to do is place code in the Load event to open the tables.
Take the example shown in Listing 15.6, which opens the Customer table in the TESTDATA database and then presents some of the fields for editing (the form is shown in Figure 15.11).
Figure 15.11: The Customer data form.
Listing 15.6 15CODE06-A
Form Class That Opens Data Tables for Editing
* Class.............: Customerdataform * Author............: Menachem Bazian, CPA * Notes.............: Exported code from Class Browser. ************************************************** *-- Class: customerdataform *-- ParentClass: form *-- BaseClass: form * DEFINE CLASS customerdataform AS form DataSession = 2 Height = 238 Width = 356 DoCreate = .T. AutoCenter = .T. BackColor = RGB(192,192,192) Caption = "Sample Customer Data" Name = "dataform" ADD OBJECT txtcust_id AS TextBox WITH ; ControlSource = "customer.cust_id", ; Enabled = .F., ; Height = 24, ; Left = 120, ; Top = 24, ; Width = 205, ; Name = "txtCust_Id" ADD OBJECT txtcompany AS TextBox WITH ; ControlSource = "customer.company", ; Height = 24, ; Left = 120, ; Top = 72, ; Width = 205, ; Name = "txtCompany" ADD OBJECT txtcontact AS TextBox WITH ; ControlSource = "customer.contact", ; Height = 24, ; Left = 120, ; Top = 120, ; Width = 205, ; Name = "txtContact" ADD OBJECT txttitle AS TextBox WITH ; ControlSource = "customer.title", ; Height = 24, ; Left = 120, ; Top = 168, ; Width = 205, ; Name = "txtTitle" ADD OBJECT label1 AS label WITH ; AutoSize = .T., ; BackStyle = 0, ; Caption = "Customer Id:", ; Height = 18, ; Left = 12, ; Top = 24, ; Width = 80, ; Name = "Label1" ADD OBJECT label2 AS label WITH ; AutoSize = .T., ; BackStyle = 0, ; Caption = "Company:", ; Height = 18, ; Left = 12, ; Top = 72, ; Width = 64, ; Name = "Label2" ADD OBJECT label3 AS label WITH ; AutoSize = .T., ; BackStyle = 0, ; Caption = "Contact:", ; Height = 18, ; Left = 12, ; Top = 120, ; Width = 52, ; Name = "Label3" ADD OBJECT label4 AS label WITH ; AutoSize = .T., ; BackStyle = 0, ; Caption = "Title:", ; Height = 18, ; Left = 12, ; Top = 168, ; Width = 32, ; Name = "Label4" PROCEDURE Load OPEN DATA _SAMPLES+" \data\testdata.dbc" USE customer ENDPROC ENDDEFINE * *-- EndDefine: customerdataform **************************************************
TIP |
Visual FoxPro 6 has a new system memory variable named _SAMPLES. This variable holds the path to the Visual FoxPro sample files. This is a big help because with Visual Studio 6, the samples for all of the Visual Studio products are stored in a common directory structure. Visual FoxPro 6 does not put its sample files in the familiar Sample directory under the Visual FoxPro home directory. |
If you combine this form with the Navigator container you saw earlier, you have a complete data entry package. Listing 15.7 shows what it looks like in code; a visual representation is shown in Figure 15.12.
Figure 15.12: The Customer data form with the Navigator container.
* Class.............: Custformwithnavcontainer * Author............: Menachem Bazian, CPA * Notes.............: Exported code from Class Browser. ************************************************** *-- Class: custformwithnavcontainer *-- ParentClass: customerdataform (d:\data\docs\books\vfu\code\oop2.vcx) *-- BaseClass: form * DEFINE CLASS custformwithnavcontainer AS customerdataform Height = 283 Width = 370 DoCreate = .T. Name = "custformwithnavcontainer" txtCust_Id.Name = "txtCust_Id" txtCompany.Name = "txtCompany" txtContact.Name = "txtContact" txtTitle.Name = "txtTitle" Label1.Name = "Label1" Label2.Name = "Label2" Label3.Name = "Label3" Label4.Name = "Label4" ADD OBJECT navigator1 AS navigator WITH ; Top = 240, ; Left = 12, ; Width = 350, ; Height = 32, ; Name = "Navigator1", ; cmdNext.Name = "cmdNext", ; cmdPrev.Name = "cmdPrev", ; cmdTop.Name = "cmdTop", ; cmdBottom.Name = "cmdBottom", ; cmdOK.Name = "cmdOK" ENDDEFINE * *-- EndDefine: custformwithnavcontainer **************************************************
As it turns out, even without the native data environment capabilities, creating forms as classes is a very viable way to create and display forms in Visual FoxPro.
CREATEOBJECT("form") Versus DO FORM Why should you create a form as a class instead of using an SCX file and calling it with DO FORM? This question brings up a few issues you should consider.
The first issue is subclassing. You can subclass forms only if they are stored as classes.
Form classes are also more flexible to use than the DO FORM syntax. I like being able to instantiate a form object with CreateObject() and then play with it a little before issuing a call to the Show() method. Finally, the CreateObject() syntax does provide uniformity among the components of an application. I like the simple elegance of having a similar syntax for all my objects as opposed to using CreateObject() most of the time and DO FORM some of the time.
Having said this, you can handle the situation in any way you feel comfortable. To a degree, it does come down to programmer preference.
Build Forms as Classes or Use the Form Designer? |
Using the Form Designer versus forms as classes is a question that might never have an answer. Both methods work fine in many applications. The key point about the whole issue is that you should use one methodology so that your applications are consistent in how they work. |
Protecting Members on Forms In earlier chapters you learned how to protect members from the outside world. If you recall, the rule of thumb for protecting members is to protect them if the outside world has no use for them.
With forms, the rules are changed on this issue. It is conceivable that you might want to protect something (a method, property, or even an object on the form) from the outside world but you want it accessible to objects within the form. Unfortunately, if you protect something within the form, it is private throughout the form. In other words, if it is private, it is considered private to everything.
In addition, beware of protected members on objects themselves. If you protect a member on an object, it can even be protected from itself. Sound strange? Then get a load of this. Remember the NoNotifyTextBox text box created earlier in this chapter? It had a protected member called cNotifySetting, which held the setting of SET NOTIFY when the control got focus. Now, suppose I dropped it on a form, as shown in Listing 15.8.
* Author............: Menachem Bazian, CPA * Notes.............: Exported code from Class Browser. PUBLIC oform1 oform1=CREATEOBJECT("form1") oform1.Show() RETURN ************************************************** *-- Form: form1 (d:\data\docs\books\vfu\code\protect.scx) *-- ParentClass: form *-- BaseClass: form * DEFINE CLASS form1 AS form Top = 0 Left = 0 Height = 57 Width = 301 DoCreate = .T. Caption = "Form1" Name = "Form1" ADD OBJECT nonotifytextbox1 AS nonotifytextbox WITH ; Left = 84, ; Top = 12, ; Name = "Nonotifytextbox1" PROCEDURE nonotifytextbox1.MouseMove LPARAMETERS nButton, nShift, nXCoord, nYCoord WAIT WINDOW this.cNotifySetting ENDPROC ENDDEFINE * *-- EndDefine: form1 **************************************************
This is an .SCX-type form with a NoNotifyTextBox text box in it. Notice the MouseMove event for NoNotifyTextBox. The code has been entered into the property at the form level (that is, the control was not subclassed in a VCX file and the code added in the subclass).
If you run this form and move the mouse over the TextBox, you will get an error stating that cNotifySetting does not exist (see Figure 15.13). This is because the code was not put in the class itself or in a subclass-it was entered at the form.
Figure 15.13: A form with an error message.
That's the way it works. Keep an eye out for this one.
NOTE |
The property cNotifySetting cannot be seen by the code written in the NoNotifyTextBox instance on the form because the code added in the Form Designer is added to the form and not the text box. Look at the code above and you will notice that the method name for the new code is part of the form's class definition. This means that the reference to the cNoNofitySetting property is being made by the form. Because the form is not the text box, the protected property of the text box is hidden from the form. |
The final type of visual class covered in this chapter is the Toolbar. You can think of toolbars as a special type of form. You create them by dropping objects on them (usually CommandButtons but other objects can be added as well). The toolbar always stays on top, docks itself automatically (by default) when dragged to the top, bottom, right, or left borders of the FoxPro desktop, and resizes just like any other toolbar in Visual FoxPro. I will illustrate this with a simple navigation toolbar.
* Class.............: Simplenavbar * Author............: Menachem Bazian, CPA * Notes.............: Exported code from Class Browser. ************************************************** *-- Class: simplenavbar *-- ParentClass: toolbar *-- BaseClass: toolbar * DEFINE CLASS simplenavbar AS toolbar Caption = "Navigator Buttons" Height = 31 Left = 0 Top = 0 Width = 177 Name = "simplenavbar" ADD OBJECT cmdnext AS CommandButton WITH ; Top = 4, ; Left = 6, ; Height = 25, ; Width = 33, ; FontBold = .F., ; FontSize = 8, ; Caption = "Next", ; Default = .F., ; ToolTipText = "Next record", ; Name = "cmdNext" ADD OBJECT cmdprev AS CommandButton WITH ; Top = 4, ; Left = 38, ; Height = 25, ; Width = 33, ; FontBold = .F., ; FontSize = 8, ; Caption = "Prev", ; Default = .F., ; ToolTipText = "Previous record", ; Name = "cmdPrev" ADD OBJECT cmdtop AS CommandButton WITH ; Top = 4, ; Left = 70, ; Height = 25, ; Width = 33, ; FontBold = .F., ; FontSize = 8, ; Caption = "Top", ; Default = .F., ; ToolTipText = "First record", ; Name = "cmdTop" ADD OBJECT cmdbottom AS CommandButton WITH ; Top = 4, ; Left = 102, ; Height = 25, ; Width = 33, ; FontBold = .F., ; FontSize = 8, ; Caption = "Bott", ; Default = .F., ; ToolTipText = "Last record", ; Name = "cmdBottom" ADD OBJECT separator1 AS separator WITH ; Top = 4, ; Left = 140, ; Height = 0, ; Width = 0, ; Name = "Separator1" ADD OBJECT cmdok AS okbuttonwithtooltip WITH ; Top = 4, ; Left = 140, ; Height = 25, ; Width = 33, ; FontBold = .F., ; FontSize = 8, ; Default = .F., ; Name = "cmdOK" PROCEDURE cmdnext.Click IF TYPE("_screen.activeform") # 'O' ; OR isNull(_screen.activeform) WAIT WINDOW "No form active!" RETURN ENDIF SET DATASESSION TO _screen.activeform.datasessionid SKIP 1 IF EOF() =Messagebox("At end of file!", 16) GO BOTTOM ENDIF _screen.activeform.refresh() ENDPROC PROCEDURE cmdprev.Click IF TYPE("_screen.activeform") # 'O' ; OR isNull(_screen.activeform) WAIT WINDOW "No form active!" RETURN ENDIF SET DATASESSION TO _screen.activeform.datasessionid SKIP -1 IF BOF() =Messagebox("At beginning of file!", 16) GO TOP ENDIF _screen.activeform.refresh() ENDPROC PROCEDURE cmdtop.Click IF TYPE("_screen.activeform") # 'O' ; OR isNull(_screen.activeform) WAIT WINDOW "No form active!" RETURN ENDIF SET DATASESSION TO _screen.activeform.datasessionid GO TOP _screen.activeform.refresh() ENDPROC PROCEDURE cmdbottom.Click IF TYPE("_screen.activeform") # 'O' ; OR isNull(_screen.activeform) WAIT WINDOW "No form active!" RETURN ENDIF SET DATASESSION TO _screen.activeform.datasessionid GO BOTTOM _screen.activeform.refresh() ENDPROC PROCEDURE cmdok.Click IF TYPE("_screen.activeform") # 'O' ; OR isNull(_screen.activeform) WAIT WINDOW "No form active!" RETURN ENDIF SET DATASESSION TO _screen.activeform.datasessionid _screen.activeform.release() ENDPROC ENDDEFINE * *-- EndDefine: simplenavbar **************************************************
Notice the differences between this version of the Navigator buttons and the container version previously shown. In the container-based version, each button had the code to move within the current table. The code was simple and straightforward. The code for the Toolbar version is a bit more obtuse. Instead of working directly with the form, I use _screen.activeform, and there is a SET DATASESSION command thrown in. The OK button does not issue RELEASE thisform but rather calls the Release method. Why the additional work?
The container version is designed to work with a single form; each form that uses the container version has an instance of the container on it. The toolbar, on the other hand, is designed to work generically with many forms.
Because the Toolbar exists independently of any individual form, you have to deal with the issue of data sessions. A form might work in its own data session or in the default one. There is no way at design time to know the one with which you will be dealing (unless you set rules one way or the other).
The solution to this issue is to specifically set the current data session with the SET DATASESSION command to the current form's data session (which is stored in the form's DataSessionId property). One way to get to the current form is to use _screen.activeform. The problem is that _screen.activeform might be null even if a form is visible-hence the check on _screen.activeform in the CommandButton's Click methods.
Furthermore, notice how the form is released. The Release method (which is a built-in method in the Form base class) takes care of this action quite nicely. The reason for not using something like RELEASE thisform is because the CommandButton is not sitting on a form at all; in addition, it has no clue as to the name of the instance variable to release. _screen.activeform.Release handles these issues for you.
One final note: When objects are added to the toolbar they are placed flush against each other. Unlike other classes that support containership, you cannot dictate the placement of the objects in the toolbar-the class handles that for you. All you can dictate is the order in which they are placed.
In order to achieve a separation between objects, you need to add a separator control. The control doesn't really do much of anything, it just adds a space between controls on the toolbar. Figure 15.14 shows an example of a data form with the toolbar. In this case, the data form is the simple Customer form with the SimpleNavBar class; both are instantiated separately.
Figure 15.14: A form with SimpleNavBar.
Internal Characteristics of a Toolbar Toolbars to do not behave like other forms. They have some specific characteristics, including the following:
In addition to these built-in behaviors, there are some additional characteristics that good toolbars should have. Here's a list of them:
Large Objects on a Toolbar If you need to put a lot of data on a toolbar, it is a good idea not to force a large object to exist on the toolbar when it is docked. For the most part, large objects (such as a list box) can be mimicked with other objects that take up less space (a drop-down list object, for example). The idea here is to create a toolbar with both the large versions and small versions of the objects on it and to show or hide the objects based on whether the toolbar is docked.
Consider the following scenario. You have a toolbar that displays a list of customers in the table. (The specific functionality isn't important, only that you need to have a toolbar with a list.) You prefer to use the list box because it is larger, but you cannot afford the space on the desktop when the toolbar is docked.
Listing 15.10 demonstrates how you could handle the situation.
* Class.............: Morphbar * Author............: Menachem Bazian, CPA * Notes.............: Exported code from Class Browser. ************************************************** *-- Class: morphbar *-- ParentClass: toolbar *-- BaseClass: toolbar * DEFINE CLASS morphbar AS toolbar Caption = "Toolbar1" DataSession = 1 Height = 121 Left = 0 Top = 0 Width = 332 Name = "morphbar" ADD OBJECT list1 AS listbox WITH ; RowSourceType = 6, ; RowSource = "customer.company", ; Height = 115, ; Left = 6, ; Top = 4, ; Width = 170, ; Name = "List1" ADD OBJECT combo1 AS combobox WITH ; RowSourceType = 6, ; RowSource = "customer.company", ; Height = 21, ; Left = 175, ; Style = 2, ; Top = 4, ; Width = 153, ; Name = "Combo1" PROCEDURE AfterDock IF this.DockPosition = 1 or this.DockPosition = 2 *-- Note, you cannot use the Dock() method to undock *-- a toolbar... you have to use the Move() method. *-- Using the Dock() method with the -1 parameter results *-- in a totally bogus error message. this.move(1,1) ENDIF ENDPROC PROCEDURE Init IF this.docked this.list1.visible = .F. this.combo1.visible = .T. ELSE this.list1.visible = .T. this.combo1.visible = .F. ENDIF ENDPROC PROCEDURE BeforeDock *-- nLocation shows where the toolbar will be after *-- is complete. -1 means it will be undocked. 0,1,2,3 *-- mean top, left, right and bottom respectively. LPARAMETERS nLocation DO CASE CASE nLocation = -1 && Not DOcked this.List1.Visible = .T. this.list1.Enabled = .T. this.Combo1.visible = .F. CASE nLocation = 0 OR nLocation = 3 && Top or bottom this.List1.Visible = .F. this.Combo1.visible = .T. this.Combo1.Enabled = .T. ENDCASE ENDPROC ENDDEFINE * *-- EndDefine: morphbar **************************************************
Notice that there are two objects on this toolbar. The list box is kept visible whenever the toolbar is not docked. The combo box is made visible whenever the toolbar is not docked. The toolbar automatically adjusts its size based on the visible controls on it. Cool stuff, no?
Neither a combo box nor a list box work well when docked to the side because they just take up too much real estate on the desktop. The solution taken in this example is not to let the user dock the toolbar to the left or right sides.
The BeforeDock event fires when an attempt is made to dock or undock the toolbar. The nLocation parameter tells the event where the toolbar is going. Based on where the toolbar is being docked, the visible properties of the lists are adjusted appropriately.
A few notes are in order here about the data behind the lists. In order to use this class, the TESTDATA database, which is located in the Sample files \DATA directory (_SAMPLES will give you the path to the Visual FoxPro sample files), has to be open and the Customer table has to be in use. Here are the commands to do this:
OPEN DATABASE _SAMPLES+" \data\testdata.dbc" USE customer
You can't really open the data in this class. Unlike its cousin, the Form class, the Toolbar class does not have a LOAD method. The RecordSource properties of both lists look to the Customer file. As a result, when Visual FoxPro tries to instantiate the class, it looks at the properties of the contained objects and validates them. If the Customer table is not open, the object does not instantiate. Even opening the table in the Init method of the lists won't work.
Coordinating Forms with Toolbars Another issue to consider is that of coordinating forms with toolbars. You have already learned how to coordinate data sessions with a form; however, there are many other issues to consider. Here are just a few:
Coordination is merely a matter of sending the appropriate messages back and forth. By placing a custom property on data forms, you can determine whether a form is a data form by the existence of the custom property. When a data form is instantiated, it looks for a global variable (goNavToolBar). If the variable exists, the toolbar is assumed to exist (you make the toolbar nonclosable). Each new instance of a data form also sends a message to goNavToolBar telling it that there is a new data form in town (in the following example the toolbar maintains a count variable and the new instances increase it by one). When an instantiated form is released, it tells the toolbar that there is one less data form. If this is the last data form visible on the desktop, the toolbar releases itself. Listing 15.11 shows the code for the example, Figure 15.15 shows you what it looks like.
Figure 15.15: The Customer data form with NavigationToolBar.
* Class.............: Navigatortoolbar * Author............: Menachem Bazian, CPA * Notes.............: Exported code from Class Browser. ************************************************** *-- Class: navigatortoolbar *-- ParentClass: simplenavbar *-- BaseClass: toolbar * DEFINE CLASS navigatortoolbar AS simplenavbar Height = 31 Left = 0 Top = 0 Width = 171 *-- The number of forms active that use this toolbar. *-- When this property reaches 0, the toolbar will release. PROTECTED nnumforms nnumforms = 0 Name = "navigatortoolbar" cmdNext.Top = 4 cmdNext.Left = 6 cmdNext.Default = .F. cmdNext.Name = "cmdNext" cmdPrev.Top = 4 cmdPrev.Left = 38 cmdPrev.Default = .F. cmdPrev.Name = "cmdPrev" cmdTop.Top = 4 cmdTop.Left = 70 cmdTop.Default = .F. cmdTop.Name = "cmdTop" cmdBottom.Top = 4 cmdBottom.Left = 102 cmdBottom.Default = .F. cmdBottom.Name = "cmdBottom" cmdOK.Top = 4 cmdOK.Left = 134 cmdOK.Default = .F. cmdOK.Name = "cmdOK" *-- Add a form to the toolbar form count. PROCEDURE addform *-- This method should be called by form.init() this.nNumForms = this.nNumForms + 1 ENDPROC *-- Remove a form from the toolbar form count. PROCEDURE removeform *-- This method should be called by form.destroy() this.nNumForms = this.nNumForms - 1 IF this.nNumForms = 0 this.release() ENDIF ENDPROC *-- Release the toolbar. Mimics a form's RELEASE method. PROCEDURE release RELEASE thisform ENDPROC PROCEDURE Refresh IF TYPE("_screen.activeform.lIsDataForm") = 'L' this.cmdNext.Enabled = .T. this.cmdPrev.Enabled = .T. this.cmdTop.Enabled = .T. this.cmdBottom.Enabled = .T. ELSE this.cmdNext.Enabled = .F. this.cmdPrev.Enabled = .F. this.cmdTop.Enabled = .F. this.cmdBottom.Enabled = .F. ENDIF ENDPROC PROCEDURE Destroy this.visible = .f. ENDPROC ENDDEFINE * *-- EndDefine: navigatortoolbar ************************************************** * Class.............: Custformlinkedtotoolbar * Author............: Menachem Bazian, CPA * Notes.............: Exported code from Class Browser. ************************************************** *-- Class: custformlinkedtotoolbar *-- ParentClass: customerdataform *-- BaseClass: form * DEFINE CLASS custformlinkedtotoolbar AS customerdataform DoCreate = .T. Name = "custformlinkedtotoolbar" txtCust_Id.Name = "txtCust_Id" txtCompany.Name = "txtCompany" txtContact.Name = "txtContact" txtTitle.Name = "txtTitle" Label1.Name = "Label1" Label2.Name = "Label2" Label3.Name = "Label3" Label4.Name = "Label4" lisdataform = .F. PROCEDURE Init IF TYPE ("goNavToolBar") # 'O' OR isnull(goNavToolBar) RELEASE goNavToolBar PUBLIC goNavToolBar goNavToolBar = CreateObject("NavigatorToolBar") goNavToolBar.Dock(0,1,0) goNavToolBar.Show() ENDIF goNavToolBar.AddForm() goNavToolBar.Refresh() ENDPROC PROCEDURE Activate goNavToolBar.Refresh() ENDPROC PROCEDURE Deactivate goNavToolBar.Refresh() ENDPROC PROCEDURE Destroy goNavToolBar.RemoveForm() ENDPROC ENDDEFINE * *-- EndDefine: custformlinkedtotoolbar **************************************************
When you create classes in Visual FoxPro, it is not uncommon to think only in terms of visual classes. Visual classes certainly are fun to create-working with GUI elements has an element of art to it. Creating a form that is both functional and pleasing to the eye is something of an achievement.
However, OOP does not stop with visual classes. Unlike some languages that only support GUI classes, Visual FoxPro supports nonvisual classes. What you can do with nonvisual classes is no less remarkable.
A nonvisual class is any class that is not designed primarily to be displayed. For example, CommandButton is a class that is specifically designed to display on a form. The Timer class, on the other hand, does not show on a form at all.
Nonvisual classes in Visual FoxPro are typically descendants of the Custom or Timer class and often have no display component attached to them at all. However, a nonvisual class can have a visual component attached to it. And to make matters more confusing, classes typically thought of as visual classes can be the basis for nonvisual classes, too.
TIP |
Because nonvisual classes have no display component, that is, they are invisible, you can use any base class in Visual FoxPro as the basis for a nonvisual class. I often use the Label base class for the basis of my nonvisual classes for two reasons. One, it takes up less memory than most of the other classes. Two, I can give it a caption that is seen clearly in the form or Class Designer and still set its visible property to .F. so that the user never sees it. |
There are many reasons why you would create a nonvisual class. In fact, there are as many reasons to create nonvisual classes as there are to create visual ones. Here are the main reasons:
What's the difference between nonvisual and visual classes? The basic difference lies in the type of classes you create. Visual classes typically center around the user interface, whereas nonvisual classes play key roles in management functions. Nonvisual classes also incorporate the type of class that is most often neglected when people discuss object orientation in systems development: business classes.
Here are some of the more common types of nonvisual classes that you will typically create and use in your applications.
Wrapper Classes When you create classes written for management roles, you want to consider the many different aspects of management for which you can create a class. One aspect is to manage the interface between one program and another. A good example of this would be the management of the interface between Visual FoxPro code and DLLs, FLLs, or other function libraries. These classes are created for several reasons:
This process is known as wrapping a class around some existing functionality. Appropriately, these classes are called wrapper classes.
Manager Classes Another typical nonvisual class is a class that manages other classes. A good example is a class that handles multiple instances of forms. Such a class enables you to create functions such as Tile All Windows. These classes are known as manager classes.
Business Classes A business class is a class designed to model an entity in a business environment. A good example is the Customer class. These classes are a combination of information and actions designed to do what a business entity needs to do within the context of the problem domain (that is, the environment being modeled and automated).
The responsibilities of a business class are determined after careful analysis and design. Business class responsibilities can be very abstract in nature and require careful modeling before implementation. Some common responsibilities might be the following:
Business classes are a little more difficult to classify as visual or nonvisual. Business classes can, and often do, have visual components. A business class can be based on a visual class (a Form class, for example) with the appropriate properties and methods added to the form. In which category does a business class belong? It depends on how the object is created. In reality, it's all semantics anyway; you can call it what you want.
The purpose of a wrapper class is to create a class that manages and perhaps even enhances the functionality of some other code. Any code can be wrapped into a class. If you have an old procedure library written in earlier versions of FoxPro, you could wrap a class around it if you like. The tough part is deciding when it is appropriate to wrap a class around something.
The best reason to wrap a class around something is to make it easier and better to use. A perfect example of a candidate for a wrapper class is a DLL or FLL. These function libraries can be obscure, their parameters can be difficult to determine, and their error-handling requirements can be rather extensive. For example, if you are using an FLL library (for example, FOXTOOLS), what do you do if someone else's code unloads it accidentally with SET LIBRARY TO? Can you rely on the fact that the library is there all the time? Take the example of calling some of the Windows API functions (the functions to write and read from INI files, for example). These can be difficult to learn to use.
When a class is wrapped around some other piece of code, the class developer has the ability to control which portions of the DLL or FLL are available to the outside world, how they are called, and even what values are returned.
Wrapper classes carry a myriad of benefits with them. First of all, if a DLL or FLL is used with a wrapper class, the developers who use that class do not have to know anything about the DLL or FLL that serves as the basis for the class. They also do not have to be concerned with issues of loading the DLL or FLL or registering its functions. In effect, the result is a much reduced learning curve and coding time for all concerned.
Listing 15.12 shows an example of a wrapper class. This class is a wrapper around a library of functions called FOXTOOLS.FLL that ships with Visual FoxPro.
NOTE |
Although in Visual FoxPro 6 most of the functions contained in FOXTOOLS.FLL are now part of the VFP language, this example of a wrapper class is still a good exercise for seeing how wrapper classes are built. |
Listing 15.12 15CODE12-A
Wrapper Class for FOXTOOLS.FLL
* Class.............: Foxtools * Author............: Menachem Bazian, CPA * Notes.............: Exported code from Class Browser. ************************************************** *-- Class: foxtools *-- ParentClass: custom *-- BaseClass: custom * * DEFINE CLASS foxtools AS custom Name = "foxtools" PROTECTED lloaded PROCEDURE loadlib IF !"FOXTOOLS" $ SET("library") SET LIBRARY TO (SYS(2004)+"FOXTOOLS") this.lLoaded = .T. ENDIF ENDPROC PROCEDURE drivetype LPARAMETERS tcDrive LOCAL lnRetVal lnRetVal = (drivetype(tcDrive)) RETURN lnRetVal ENDPROC PROCEDURE justfname LPARAMETERS tcString LOCAL lcRetVal lcRetVal = (justfname(tcString)) RETURN lcRetVal ENDPROC PROCEDURE juststem LPARAMETERS tcString LOCAL lcRetVal lcRetVal = (juststem(tcString)) RETURN lcRetVal ENDPROC PROCEDURE justpath LPARAMETERS tcString LOCAL lcRetVal lcRetVal = (this.addbs(justpath(tcString))) RETURN lcRetVal ENDPROC PROCEDURE justdrive LPARAMETERS tcString LOCAL lcRetVal lcRetVal = (this.addbs(justpath(tcString))) RETURN lcRetVal ENDPROC PROCEDURE justpathnodrive LPARAMETERS tcString LOCAL lcRetval, ; lnAtPos lcRetVal = this.justpath(tcString) lnAtPos = AT(':', lcRetVal) IF lnAtPos > 0 IF lnAtPos < LEN(lcRetVal) lcRetVal = this.addbs(SUBST(lcRetVal,lnAtPos+1)) ELSE lcRetVal = "" ENDIF ENDIF RETURN (lcRetVal) ENDPROC PROCEDURE addbs LPARAMETERS tcString LOCAL lcRetVal lcRetVal = (addbs(tcString)) RETURN lcRetVal ENDPROC PROCEDURE isdir LPARAMETERS tcString LOCAL llRetVal, lcTestString, laFiles[1] lcTestString = ALLTRIM(this.addbs(tcString)) - "*.*" IF ADIR(laFiles, lcTestString, "DSH") > 0 llRetVal = .t. ELSE llRetVal = .F. ENDIF RETURN (llRetVal) ENDPROC PROCEDURE cleandir LPARAMETERS tcString RETURN(UPPER(sys(2027, tcString))) ENDPROC PROCEDURE cut =_edcut(_wontop()) ENDPROC PROCEDURE copy =_edcopy(_wontop()) ENDPROC PROCEDURE paste =_edpaste(_wontop()) ENDPROC PROCEDURE Error LPARAMETERS tnError, tcMethod, tnLine LOCAL lcMessage tcMethod = UPPER(tcMethod) DO CASE CASE tnError = 1 && File not found -- Cause by the library not loaded this.loadlib() RETRY OTHERWISE ?? CHR(7) lcMessage = "An error has occurred:" + CHR(13) + ; "Error Number: " + PADL(tnError,5) + CHR(13) + ; " Method: " + tcMethod + CHR(13) + ; " Line Number: " + PADL(tnLine,5) =MESSAGEBOX(lcMessage, 48, "Foxtools Error") ENDCASE ENDPROC PROCEDURE Destroy IF this.lLoaded RELEASE LIBRARY (SYS(2004)+"foxtools.fll") ENDIF ENDPROC PROCEDURE Init this.lLoaded = .F. this.loadlib() ENDPROC ENDDEFINE * *-- EndDefine: foxtools **************************************************
Before you go into the theory behind the class, learn all the
methods and properties that make up this wrapper class. Table
15.1 presents the methods and properties for Foxtools.
lLoaded | The lLoaded property is a protected property that keeps track of whether the FOXTOOLS library was loaded by this class or from the outside world. If Foxtools loaded the library, it releases the library when the instance is cleared. |
Addbs([tcPath]) | This function is the equivalent of the AddBs() function in FOXTOOLS. It accepts a path string as a parameter and adds a backslash to it if there isn't one already. |
Cleandir([tcPath]) | This function really doesn't use FOXTOOLS.FLL at all but has related functionality. It accepts a filename with a path attached and cleans it up with SYS(2027). Cleaning up means interpreting back steps in path strings and redrawing them. Here is an example:
oFtools.CleanDir("D:\APPS\FOX\VFP\..\") && Returns "D:\APPS\FOX\" |
copy() | This method copies selected text to the Clipboard using the _edCopy() function. |
Cut() | This method copies selected text to the Clipboard and then deletes it using the _edCut() function. |
Destroy() | This method is called when the object is released. The method releases FOXTOOLS.FLL if the object loaded it. |
drivetype([tcDriveLetter]) This method calls the DriveType() function to get the type of the drive specified. | |
Error() | This method is called when a program error occurs in the class. It takes precedence over ON ERROR. If the error is an error number 1 (that is, file not found), the method assumes it occurred because someone unloaded FOXTOOLS.FLL, in which case the Error() method just loads the library with LoadLib() and then retries the command. If the error is something other than a "File not found" error, an error dialog box is presented. |
Init() | This method is called when the object is instantiated. It calls LoadLib() to load the library if needed. |
Isdir([tcPath]) | This method accepts a path string as a parameter and tests it to make sure it's a directory. This method returns a logical value. |
IsDir() does not really use Foxtools, but it is related to the functionality of the library and is therefore included here. | |
Justdrive([tcPath]) | This method accepts a filename with path string and returns only the drive designation. |
Justfname([tcPath]) | This method is the same as JustDrive(), but it returns the filename. |
Justpath([tcPath]) | This method is the same as JustDrive() except that it returns the path. The drive designator is included in the return value. |
Justpathnodrive | This method is the same as JustPath except that the drive designator is removed from the string. |
Juststem([tcPath]) | This method is the same as JustFName() except that this returns the filename minus the extension. |
Loadlib() | This method loads Foxtools if it is not already loaded. lLoaded is set to .T. if this method loads the library. |
Paste() | This method pastes the contents of the Clipboard at the insertion point. |
This class shows the various purposes of a wrapper:
The ability to create and use wrapper classes is a major benefit to software development. Because the complexity of working with something can be hidden within a class without compromising the class's functionality, developers who use the wrapper will immediately notice an increase in their productivity because they can have the wrapper in their arsenals without the cost of learning its intricacies.
A second type of nonvisual class that is often created is a manager class. This class typically manages instances of another class. A good example of this is the management of multiple instances of a form to ensure that subsequent instances are properly placed on the screen with an identifiable header (for example, Document1, Document2, and so on).
The example shown in Listing 15.13 deals with this issue, showing a manager class that manages a simple form class.
* Program...........: MANAGER.PRG * Author............: Menachem Bazian, CPA * Created...........: 05/03/95 *) Description.......: Sample Manager Class with Managed Form Class * Major change list.: *-- This class is designed to manage a particular form class and make *-- sure that when the forms are run they are "tiled" properly. DEFINE CLASS FormManager AS Custom DECLARE aForms[1] nInstance = 0 PROCEDURE RunForm *-- This method runs the form. The instance of the form class *-- is created in the aForms[] member array. LOCAL lnFormLeft, llnFormTop, lcFormCaption nInstance = ALEN(THIS.aForms) *-- Set the Top and Left Properties to Cascade the new Form IF nInstance > 1 AND TYPE('THIS.aForms[nInstance -1]') = 'O' ; AND NOT ISNULL(THIS.aForms[nInstance -1]) lnFormTop = THIS.aForms[nInstance -1].Top + 20 lnFormLeft = THIS.aForms[nInstance -1].Left + 10 ELSE lnFormTop = 1 lnFormLeft = 1 ENDIF *-- Set the caption to reflect the instance number lcFormCaption = "Instance " + ALLTRIM(STR(nInstance)) *-- Instantiate the form and assign the object variable *-- to the array element THIS.aForms[nInstance] = CreateObject("TestForm") THIS.aForms[nInstance].top = lnFormTop THIS.aForms[nInstance].left = lnFormLeft THIS.aForms[nInstance].caption = lcFormCaption THIS.aForms[nInstance].Show() *-- Redimension the array so that more instances of *-- the form can be launched DIMENSION THIS.aforms[nInstance + 1] ENDPROC ENDDEFINE *-- This class is a form class that is designed to work with *-- the manager class. DEFINE CLASS TestForm AS form Top = 0 Left = 0 Height = 87 Width = 294 DoCreate = .T. BackColor = RGB(192,192,192) BorderStyle = 2 Caption = "Form1" Name = "Form1" ADD OBJECT label1 AS label WITH ; FontName = "Courier New", ; FontSize = 30, ; BackStyle = 0, ; Caption = (time()), ; Height = 61, ; Left = 48, ; Top = 12, ; Width = 205, ; Name = "Label1" ENDDEFINE
Notice that forms are instantiated through the RUNFORM method rather than directly with a CreateObject() function. This enables the manager function to maintain control over the objects it instantiates.
By the way, this can be considered a downside to working with manager classes, too. If a developer is used to working with the familiar CreateObject() function to instantiate classes, working through a method like the RUNFORM method might be a bit confusing at first.
By the way, remember the discussion earlier in this chapter about instance variable scoping (see "Reading System Events" and "Variable Scoping Revisited")? FormManager is an example of how to manage instance variable scoping. The aForm[] property is an array property. Each row in the array holds an instance of the form.
Figure 15.16 shows what the forms look like when they are instantiated. Notice how they are properly tiled (with the exception of a few moved aside to show the contents of the form) and that each one has a different caption and time showing.
Manager functions are very useful. They provide a simple way to encapsulate code that would normally have to be duplicated every time an object is instantiated into one single place.
Business classes are object-oriented representations of business entities (for example, a customer). The responsibilities of these classes vary depending on the behavior of a particular object within the problem domain.
The purpose of a business class is multifold. At an abstract level, it is possible to determine the basic functionality of a business object and then to create a class around it. For example, the basic responsibilities of a business object might be the following:
These functions could be abstracted in a class. Abstracting in this sense means that the functionality can be placed in its own class rather than repeating it in multiple classes. The abstract class is created as a basis for other classes, not to be used in instantiating objects (see Listing 15.14).
DEFINE CLASS BaseBusiness AS custom cAlias = "" oData = .NULL. PROCEDURE INIT IF !EMPTY(this.cAlias) AND !used(this.cAlias) =MessageBox("Alias is not open!", 16) ENDIF ENDPROC PROCEDURE next SELECT (this.cAlias) SKIP 1 IF EOF() GO BOTTOM ELSE this.readrecord() ENDIF ENDPROC *-- Additional methods here for movement would mimic *-- procedure NEXT PROCEDURE readrecord *-- This procedure is initially empty SCATTER NAME this.oData ENDPROC *-- Additional methods for saving would follow mimicking *-- procedure readrecord. ENDDEFINE
In order to create a Customer object, all you need to do is subclass it as follows:
DEFINE CLASS customer AS basebusiness cAlias = "Customer" ENDDEFINE
The fields in the Customer alias are automatically added as members of oData. Thus, if an object called oCust were instantiated from the Customer class, the cName field would be held in oCust.oData.cName.
Of course, the beauty of this method of development is that there is little coding to do from one business class to another. In effect, all you do is code by exception.
This is one way to create business classes. You will learn more about business classes and a sample framework for working with business classes in Chapter 17.
The classes you create in Visual FoxPro, as in any object-oriented language, cover many different areas and serve many different purposes. Some classes will be created to serve a specific need in the software. For example, creating a Customer class might be an example of a class designed specifically to the needs of a software project. However, as you learned previously, classes can be created simply because the functionality represented in the class provides a good basis for further subclassing. For example, NoNotifyTextBox is a generic-type class you can create and reuse and from which you can subclass.
When you are starting out with an object-oriented language, the first step you probably take is to create a series of classes that will be the basis on which you create classes for your applications. These classes, taken together, are known as the application framework. Part of the framework will be generic; that is, they will be based on the FoxPro base classes to provide baseline functionality. For example, a subclass of the controls can be created with default font and size characteristics. A base form class can be created with a default background color, logo, and so on.
In addition, the framework might also call for a methodology for adding business classes to applications. Although working with a framework imposes some limitations, it is well worth it in terms of standardization among applications as well as speed in development. Chapter 17 shows a portion of a framework.
A good framework is the key to success in an object-oriented environment. Creating a solid framework (or purchasing one) saves you hours in the long run. However, if the framework is shoddy your application can suffer greatly. Be careful.
© Copyright, Sams Publishing. All rights reserved.