OpenEdge Development: Progress 4GL Handbook
![]() ![]() ![]()
|
Using Dynamic Graphical ObjectsIn "Defining Graphical Objects," you learned about visual objects you can define in Progress, such as fill-in fields, buttons, and images. In other chapters, you have studied how to define and use record buffers, queries, and temp-tables. For all of these, both visual objects and data management objects, you use the
DEFINEstatement to describe the object to Progress. The compiler then builds a structure to support the object and makes that structure part of the compiled r-code.In addition to defining these objects at compile time, you can create them and define their attributes programmatically at run time. This adds great flexibility to your application, as you can create objects specifically to respond to the needs of a procedure under particular circumstances. This might include creating dynamic data representation objects to deal with a variety of different kinds of fields that you can’t determine in advance. Or it could mean creating a temp-table whose fields aren’t known until run time and then creating a query to manage the data in that temp-table.
This chapter discusses:
This chapter includes the following sections:
Creating visual objects
You can create all the kinds of visual objects you’ve seen in this book. There is a uniform syntax for all of them:
CREATEobject-typehandle[ IN WIDGET-POOLpool-name][ASSIGNattribute=attribute-value[attribute=attribute-value] . . . ][trigger-phrase] .
When you create an object dynamically you must associate it with a
HANDLEvariable (or possibly a handle field in a temp-table). This is the only way to reference the object after you create it. Unlike a static object, it has no name.The
WIDGET-POOLphrase lets you define a special storage area in memory where you want the object’s description to be located. The "Using named widget pools" describes this phrase in more detail.Assigning object attributes
The optional
ASSIGNphrase allows you to assign one or more attribute values for the object at the time you create it. Theattribute-valuefor eachattributecan be a constant or it can be an expression. You can use the sameCREATEstatement for multiple objects of the same type that need to have different attribute values. Alternatively, you can assign attribute values after you create the object by using this syntax:
The attributes you can assign to a dynamic object are largely the same as those you can assign in a static definition. You can find a complete list of all the attributes supported by each object in OpenEdge Development: Progress 4GL Reference material or in the online help, under the object-type Widget topic (for example, Button Widget). In the description of many of these attributes, there are one or more special restrictions attached to the attribute:
- Readable only — These attributes can be read and used in expressions using the
handle:attributesyntax, but their values cannot be assigned. In some cases, this is because the attribute value is part of the definition of the object and it would not make sense to change it. Examples of these attributes are:
DYNAMIC— Always true for dynamic objects and always false for static objects.HANDLE— Holds the value of the object’s handle.TYPE— Evaluates to theobject-type.In other cases, an attribute value cannot be assigned directly because it is set indirectly using some other method or statement. For example, most visual objects have aTAB-POSITIONobject, which holds the sequential position of the object within its frame. You cannot set this directly, but rather you must use theMOVE-BEFORE-TAB-ITEMorMOVE-AFTER-TAB-ITEMmethods on an object to change its tab position, which is then reflected in the value of itsTAB-POSITIONattribute.- Can be set only before the widget is realized — You learned about realized objects in "Defining Graphical Objects." Some attribute values cannot be changed after the object has been realized. An example of this is the
DEFAULTattribute for a button. The default button for a frame is the button that receives aRETURNevent for its frame when the user presses RETURN or ENTER, which in turn executes itsCHOOSEtrigger. Once a button has been established as the default button for the frame and the button has been realized, when the frame is viewed, this attribute can’t be changed.- Graphical interfaces only — Progress supports most visual objects in character environments as well as GUI, but some object attributes can only be supported in graphical interfaces. An example is the
BGCOLOR(background color) of a fill-in field. A fill-in field in a character environment does not use this attribute.- Character interfaces only — Likewise, some attributes are supported only in character interfaces. An example is the
DCOLORof a button or other visual object, which is its display color.- Windows only — Progress today supports a graphical interface only for the Microsoft Windows platform, so effectively any attributes marked as Windows only are the same as graphical interfaces only.
Assigning triggers to a dynamic object
You can assign one or more triggers to the object using the optional
trigger-phrase, in the same way as you did for static objects in "Using Graphical Objects in Your Interface."Object handles
You’ve seen a few examples of the use of handles in earlier chapters. You can define a variable to hold the handle of an object with the
DEFINE VARIABLEstatement:
You can also define temp-table fields as type
HANDLE, so it would be possible to assign the handle of an object to a field in a temp-table record.As you can see from the
CREATEstatement syntax, the only way to identify a dynamic object is to associate it with a handle. It does not have a name as a static object does. Progress builds a data structure to the object when it executes theCREATEstatement and the handle becomes a pointer to that structure. You retrieve or set attribute values through the handle, and execute methods on the object through the handle. In "Defining Graphical Objects," you learned how to use attributes and methods by appending a colon and the attribute or method name to the object name, such as in the expressionbMyButton:LABEL. For dynamic objects you do the same thing with the object’s handle, as shown in this sequence:
Figure 19–1 shows the result.
Figure 19–1: Test button message
![]()
As you can see, the handle can be represented as an integer value, but you cannot do any kind of arithmetic with object handles or manipulate handle values in any other way.
Each handle value represents a unique instance of the object you create. As you learned in "Defining Graphical Objects,"when you use the
DEFINEstatement to define a static object, it does not have a unique identity until it is realized. Depending on how it is realized, the same object definition can have multiple distinct run-time instances. A handle always points to a single unique instance of an object.Static object handles
Static objects have handles just as dynamic objects do. As soon as a statically defined object is realized, it is given a handle just like a dynamic object. You can access this handle value using the
HANDLEattribute of the static object. In this way, you can access an object’s attributes and methods using its handle just as you can for dynamic objects, as an alternative to using the object name. For example, the following code defines a handle variable and a static button. It then tries to display the button’s handle in the procedure’s default frame:
DEFINE VARIABLE hButton AS HANDLE NO-UNDO.DEFINE BUTTON bStaticButton LABEL "Static Button".DISPLAY bStaticButton:HANDLE LABEL "Button handle:".WAIT-FOR CLOSE OF THIS-PROCEDURE.
However, this code doesn’t compile, as shown in Figure 19–2, because it tries to reference the button handle without the button being realized.
Figure 19–2: Button handle error message
![]()
The button itself hasn’t been realized in a frame and therefore has no handle. You can’t display its handle, or for that matter any of its other attributes, using the
widget:attributesyntax because Progress has no way of identifying what the attributes of the object are until it can attach a handle to it. If you add the button itself to theDISPLAYstatement, then you can reference its handle and other attributes:
DEFINE VARIABLE hButton AS HANDLE NO-UNDO.DEFINE BUTTON bStaticButton LABEL "Static Button".DISPLAYbStaticButtonbStaticButton:HANDLE LABEL "Button Handle:".WAIT-FOR CLOSE OF THIS-PROCEDURE.
Figure 19–3 shows the result.
Figure 19–3: Static button example result
![]()
Now that the static object has been realized, you can assign its handle to a variable and access its attributes through it. Here the code changes the button label and enables the button by setting its
SENSITIVEattribute toYES:
Figure 19–4 shows the result.
Figure 19–4: Enabled button example result
![]()
Managing dynamic objects
When you define a static object with the
DEFINEstatement, Progress knows everything it needs to know about the object at compile time. In addition to setting up the object’s description in r-code at compile time, Progress defines a scope for the object. "Defining Graphical Objects," discussed object scope. Any defined object has a name, and a reference to that object has to be within its scope. Outside its scope the object name has no meaning and causes a compile-time error.By contrast, a dynamic object has no particular scope in terms of procedure blocks. It can be referenced and used by any part of the application that has access to its handle from the time it is created until it is deleted. You can explicitly delete an individual dynamic object or you can place that object into a pool of memory where its storage is allocated and then delete the object by deleting its memory pool. It’s especially important to keep in mind that the existence of a dynamic object has nothing to do with the scope of the handle variable you associate it with. For example, look at the following two-line procedure:
When the procedure terminates, the
hButtonvariable goes out of scope and you can no longer refer to it. Does this mean that the dynamic button is gone as well? No! The memory Progress allocates for the button is still there, but you have no way of referring to it (or deleting it). It occupies memory until your session ends.Likewise, you can lose your access to an object by resetting the handle. For example:
DEFINE VARIABLE hButton AS HANDLE NO-UNDO.CREATE BUTTON hButton ASSIGN LABEL = "Lost Button!".hButton = ?.
Oops! You’ve reset your handle variable but the object is still there. You just can’t find it anymore. Or consider this example:
Figure 19–5 shows the result.
Figure 19–5: Example button message
![]()
You’ve just created ten buttons and lost contact with all but the last of them. This is not a good idea. Thus, the first rule of managing dynamic objects is to make sure that you don’t lose track of them. Once you lose a valid handle to a dynamic object you can’t access it and it just sits in memory taking up space.
The next sections describe ways to manage memory and clean up dynamic objects so that this kind of memory leak doesn’t happen in your application.
Deleting dynamic objects
You can delete a dynamic object with the
DELETE OBJECTstatement:
You can also use the
WIDGETkeyword in place ofOBJECTin this statement. Thehandleis a variable or temp-table field of typeHANDLE, which was previously used in aCREATEstatement to create the object. If there is no object currently associated with the handle, Progress returns an error when it tries to execute the statement. In cases where your statement might be attempting to delete an object that has not been created or which has already been deleted, and you don’t want to be informed of this at run time, you can avoid such an error by checking the handle in advance with theVALID-HANDLEfunction:
Alternatively, you can include the
NO-ERRORkeyword on theDELETEstatement to suppress any error message:
In either case, don’t forget the
OBJECTkeyword. If you do, Progress thinks you’re trying to delete a record from a table:
Figure 19–6 shows the result.
Figure 19–6: Unknown table error message
![]()
It’s also important that you understand that you cannot delete a static object using its handle. Consider, for example, this sequence of statements:
DEFINE VARIABLE hButton AS HANDLE NO-UNDO.DEFINE BUTTON MyButton.DISPLAY MyButton.hButton = MyButton:HANDLE.DELETE OBJECT hButton.
If you run this procedure you get the error shown in Figure 19–7 at run time.
Figure 19–7: Static handle error message
![]()
Progress deletes static objects for you when they go out of scope. You cannot delete them yourself.
Using widget pools
Another way to manage the memory dynamic objects use is through widget pools. A widget pool is a pool of run-time memory that dynamic objects are created in. Every dynamic object you create is assigned to a widget pool.
Progress creates a single unnamed widget pool for each client session. This session pool is the default pool for all dynamic objects created during the session. It is automatically deleted when the session ends.
You can also create your own widget pools with the
CREATE WIDGET-POOLstatement:
If you don’t specify a
pool-name, then you create an unnamed widget pool. Any dynamic objects you create are then assigned, by default, to the most recently created unnamed widget pool. The scope of the pool is the scope of the procedure that created it. When that procedure terminates, the widget pool is deleted along with all the dynamic objects that were created in it. This can be a very simple and powerful way of handling much of your memory management of dynamic objects. In fact, the standard template the AppBuilder uses for a window procedure puts this statement at the top of every window procedure you create:
In this way, all the dynamic objects you create from within the procedure are deleted when the procedure terminates. The
CREATE WIDGET-POOLstatement is in the Definitions section, which is editable, so if you need some other treatment of your dynamic objects, you are free to change it.Note that this and other standard code in a procedure you create using the AppBuilder isn’t actually generated by the AppBuilder. It is part of the template procedure file the AppBuilder uses as a starting point for a procedure of that type. These template procedure files are located in the
src/templatedirectory and identified by a set of text files with the.cstextension in the same directory. The AppBuilder reads and parses these.cstfiles on startup to build its Palette and the contents of its New dialog box. For example, the starting point for a window procedure, such asCustOrderWin, is thewindow.wfile. For a structured procedure, such ash-WinSuper.p, it isprocedur.p. If you want to learn more about how to define your own template procedure files, you should consult OpenEdge Development: AppBuilder.Sometimes you might need to manage objects more explicitly than just with an unnamed widget pool. In this case, you can give a name to a pool. Once you do this, you can then create objects and allocate them explicitly to that pool, as you saw in the syntax for the
CREATEobjectstatement. For example, this sequence of statements creates a named widget pool and then a button in that pool:
DEFINE VARIABLE hButton AS HANDLE NO-UNDO.CREATE WIDGET-POOL "ButtonPool".CREATE BUTTON hButton IN WIDGET-POOL "ButtonPool".
Note, first of all, that the pool name is a character expression. You can specify either a quoted string literal or a variable or other character expression that holds the name.
When you create the button, you allocate it to the specific widget pool named
ButtonPool. You can then later delete this pool using theDELETE WIDGET-POOLstatement:
The memory for the button and any other dynamic objects you allocated to the pool goes away without disturbing other dynamic objects in other pools, including the unnamed pool.
You can only assign dynamic objects to a named widget pool, so if you want something other than the default allocation, you need to name your widget pools.
Using named widget pools
Here’s a simple example of using a named widget pool:
The code creates a widget pool named
ButtonPool. Then it creates a button in that pool. It then deletes the pool. The handle variable itself is still defined, of course, but its value now points to an invalid object because the memory is gone, as indicated by the message shown in Figure 19–8.Figure 19–8: Button handle message
![]()
By default, a widget pool is deleted when the procedure that creates it terminates. If you create a named widget pool, you can use the
PERSISTENTkeyword to keep the pool around after the procedure terminates. This creates another level of memory management responsibility for you, because now you need to remember to delete the widget pool when you’re done with it. Otherwise, it lasts until the end of the session just like the default pool does.The
NO-ERRORkeyword can prevent an error message if you try to create a pool with a name that is already in use or if you try to delete one that has already been deleted.Now look at some variations on this theme. In this example, the code creates a named widget pool and a button in that pool, but then deletes the unnamed pool:
A
DELETE WIDGET-POOLstatement without a pool name deletes the unnamed widget pool created most recently in that routine, where routine means a main procedure block, internal procedure, or trigger block. Progress does not display an error if there is no unnamed pool to delete, as you can see from this example. When you run the code, the button is still there because its pool wasn’t deleted, as shown in Figure 19–9.Figure 19–9: Button handle message
![]()
The default session pool is never deleted until the session ends.
In this next variation, you create the button in the unnamed pool and then delete the named pool:
Is the button still there after you delete the named pool? Yes, as shown in Figure 19–10, because it wasn’t allocated in the named pool you deleted.
Figure 19–10: Another button handle message
![]()
Remember that a nonpersistent widget pool, whether named or unnamed, is automatically deleted when its procedure goes out of scope. Thus, a
DELETE WIDGET-POOLstatement at the end of such a procedure is optional. But it’s certainly not a bad idea to include the statement to confirm that the pool is going away with the procedure.Using unnamed widget pools
Any unnamed widget pool you create becomes the default pool until it is deleted or until you create another unnamed pool. Any unnamed pools you create are scoped to the routine in which they are created. This routine can be a main procedure block, an internal procedure, or trigger block. A subprocedure or trigger inherits, as its default pool, the most recent unnamed widget pool created in the calling procedure unless it creates an unnamed pool of its own. When execution of a routine ends, or it goes out of scope, any unnamed pools created in the routine are automatically deleted. Because a persistent procedure goes out of scope only when it is explicitly deleted, an unnamed widget pool created in one can persist as long as the procedure does.
You might use an unnamed pool to ensure that all objects created in the default pool in a procedure you run are deleted when that procedure returns or goes out of scope, as in this example:
In this example, the
CREATE WIDGET-POOLstatement creates a new default pool. Any objects created in the default pool withinsubprocedure.pare placed in this pool. Aftersubprocedure.pcompletes, the pool is deleted along with any objectssubprocedure.pmight have created.On the other hand, in a persistent procedure, you can use an unnamed pool to ensure that dynamic objects are not deleted after the procedure returns from its main block. Otherwise, if the calling procedure deletes the pool that was current when it ran the persistent procedure, it also deletes any dynamic objects for the persistent context.
Are widget pools static or dynamic objects?
It may strike you as you read through this discussion that there is a bit of an inconsistency in how Progress manages widget pools as opposed to other kinds of objects. A widget pool seems to be a dynamic object in its own right, but it doesn’t have a handle. You use a
CREATEstatement to create a pool and aDELETEstatement to get rid of it, but you reference it only by name and never by a handle. This is a valid observation, and one that you simply need to accept. A widget pool is definitely a dynamic object. It is created only when theCREATEstatement is executed, just like other dynamic objects. It has no definition that the compiler is aware of as true static objects do. But it is true that you refer to it by name and not by a handle.Manipulating the objects in a window
In this section, you’ll learn how to locate and identify the objects in a window by their handles. Because both static and dynamic objects have handles, and attributes and methods you can manipulate through those handles, this material applies to both static and dynamic objects. Later in this chapter, you’ll add new dynamic objects to the sample window.
As discussed in the "Defining Functions and Building Super Procedures," one of the principal reasons why you might need to locate and manipulate objects by their handles is to write generic code to adjust the appearance or behavior of objects from another procedure, such as a super procedure. The sample super procedure
h-WinSuper.pshowed a very simple example of that.Next, you’ll write another super procedure to make some more substantial adjustments to the objects in the test window by “walking the widget tree” of the window.
To write another super procedure:
- Open the version of
h-CustOrderWin1.wfrom Chapter Four as a starting point, as you’ve done before and save it ash-CustOrderWin7.w.- In the main block of
h-CustOrderWin7.w, add two lines of code, one to useh-StartSuper.pto start or locate a new super procedure calledh-dynsuper.pand the second to run an internal procedure that Progress locates inh-dynsuper.p:
- Create a new structured procedure in the AppBuilder.
- Create a new internal procedure called changeFields.
- Define these variables that the procedure needs:
These handles allow you to identify the procedure’s current window, its frame, each field or other object inside the frame, and finally the columns of the order browse in the frame. To understand how to walk down through the frame to locate objects, you need to look at the hierarchy of the objects the handles point to.
First, look at how a procedure’s window handles are established. Progress defines a single default window for a session, and uses this window to display objects in when no other window is specified. There is a built-in system handle, called
DEFAULT-WINDOW, that holds the handle of this window. There is also a system handle, calledCURRENT-WINDOW, that Progress uses as the default for parenting frames, dialog boxes, and messages. Initially, theCURRENT-WINDOWis the same as theDEFAULT-WINDOW.There is no
DEFINE WINDOWstatement in Progress. All other windows in a session are dynamic windows that you create with theCREATE WINDOWstatement. When you start a procedure, you can set theCURRENT-WINDOWsystem handle to be a window created by your procedure. There is also aCURRENT-WINDOWattribute for theTHIS-PROCEDUREsystem handle, which holds the procedure handle of the procedure containing the reference toTHIS-PROCEDURE. Setting theCURRENT-WINDOWattribute sets the default parenting for all frames, dialog boxes, and messages used in that procedure without affecting windows defined by other procedures.The standard code the AppBuilder uses for a window includes statements to set the
CURRENT-WINDOWsystem handle to the dynamic window the AppBuilder code creates, as you saw in "Examining the Code the AppBuilder Generates." It also sets theCURRENT-WINDOWattribute ofTHIS-PROCEDUREto the same window:
/* Set CURRENT-WINDOW: this will parent dialog-boxes and frames. */ASSIGN CURRENT-WINDOW = {&WINDOW-NAME}THIS-PROCEDURE:CURRENT-WINDOW = {&WINDOW-NAME}.
Other procedures can access the window of a procedure that adheres to this convention by referencing its
CURRENT-WINDOWattribute. Therefore, the first thing thechangeFieldsprocedure does is obtain that window handle. Because it is a super procedure ofh-CustOrderWin7.w, theTARGET-PROCEDUREsystem handle withinh-dynsuper.phas the same value asTHIS-PROCEDUREdoes withinh-CustOrderWin7.w:
If the
h-CustOrderWin7.wprocedure did not set itsCURRENT-WINDOWattribute, you would need to use some other mechanism to identify it. Because this is the most straightforward way to associate a window with a procedure, you should adopt this convention, whether you use this standard AppBuilder template or not.A procedure can create more than one window. Only one window can be the procedure’s
CURRENT-WINDOW. If your procedures use more than one window, then you need to use some other convention to locate other windows in the procedure. For example, you could store a character string representing a list of other window handles in the window’sPRIVATE-DATAattribute. The standard for procedures you build in the AppBuilder, as well as in the SmartObjects that are the components you can build applications from, is to create only one window in a procedure.The next level down from a window is the frame or frames that are parented to the window. To identify the first frame in a window, you use its
FIRST-CHILDattribute:
If a window has more than one frame, you can access the other frames by following the
NEXT-SIBLINGof the first frame, which chains the frames together.As you might expect, you go down to the objects in a frame by accessing its
FIRST-CHILDattribute. But there is another level of object in between the frame and the fields and other objects in the frame. This level is called a field group.Remember that Progress supports the notion of a
DOWNframe, which displays multiple instances of the same fields to show multiple records from a result set in a manner similar to what a browse does. This visualization is used primarily by older character-mode applications where there is no browse. For this reason most graphical applications use only one-down frames. Regardless of this, the notion of this iteration through multiple instances of the same fields is still present in all frames, and each instance of the fields in the frame is a field group. Therefore, when you reference theFIRST-CHILDattribute of a frame, you are accessing its first field group:
Unless your frame has a
DOWNattribute greater than one, the field group is not very interesting in itself. If the frame has more than one field group, you can use theNEXT-SIBLINGchain to get at each of them. Otherwise, you just proceed down another level to get to the first object in the frame:
In
changeFields, you can combine all these steps into a singleASSIGNstatement:
ASSIGN hWindow = TARGET-PROCEDURE:CURRENT-WINDOWhFrame = hWindow:FIRST-CHILDhObject = hFrame:FIRST-CHILD /* This is the field group */hObject = hObject:FIRST-CHILD.
The
ASSIGNstatement is more efficient than a sequence of individual assignments. The steps in the sequence are executed in order, just as they appear. For instance, the value of thehObjectvariable assigned in the third step (to the field group) can then be used in the fourth step to assign the same variable a new value equal to the first object in the group.Figure 19–11 is a pictorial representation of how these different objects and their handles are related.
Figure 19–11: Relationships between objects in a window
![]()
Reading and writing object attributes
Once you have the handle to an object, you can change its appearance and behavior through its handle. To locate all the objects in the sample procedure’s frame, you start with the field group’s
FIRST-CHILD, which is now in thehObjectvariable, and walk through the chain ofNEXT-SIBLINGobjects as long as the object handle remains valid.For example, assume you want to identify each fill-in field in the frame. For each one that is an integer field, you want to disable the field and set its background color to a dark gray. For each other fill-in field, you want to set the background color to green to highlight the field for data entry.
To start with,
changeFieldslooks at theTYPEattribute of each object to see if it is aFILL-IN. If it is, then it checks theDATA-TYPEattribute to see if the field is anINTEGER. If it is, then it sets itsSENSITIVEattribute to false and itsBGCOLORattribute to 8, which represents the color gray. Otherwise, if the field is not an integer, it sets theBGCOLORattribute to 10, which is the color green:
DO WHILE VALID-HANDLE(hObject):IF hObject:TYPE = "FILL-IN" THENDO:IF hObject:DATA-TYPE = "INTEGER" THENASSIGN hObject:SENSITIVE = NOhObject:BGCOLOR = 8.ELSE hObject:BGCOLOR = 10.END.
Identifying the columns of a browse
Next, you need to know how to identify the columns in the Order browse. The browse is a single Progress object with its own handle, but the columns in the browse have handles as well. The browse acts as a container for those columns much as a frame does for the fields and other objects it contains.
To get the handle to the first column in a browse, you use its
FIRST-COLUMNattribute. The chain of columns is linked by theNEXT-COLUMNattribute of each column.This next block of code checks to see if the current object in the frame is a browse. If it is, then it moves the browse to the left by changing its
COLUMNattribute, and widens it by six characters by setting theWIDTH-CHARSattribute. It then walks through the columns, checking each one’s data type. If a column is a date, it widens it by four characters. You use the sameDO WHILE VALID-HANDLEblock header as for the frame itself to walk through all the columns in the browse:
Finally, you need to remember to move on to the next object in the frame before ending the original
DOblock:
If you forget this step, your procedure goes into an infinite loop when you run it, and you’ll need to press CTRL-BREAK to end it.
Figure 19–12 shows what you see when you run the window.
Figure 19–12: Updated sample window
![]()
Using the CAN-QUERY and CAN-SET functions
In this kind of code, where you are walking through a frame that might contain many different kinds of objects, you might need to verify not only whether the current object handle is valid, but also whether it is valid to set or query a particular attribute. If you don’t, you might get an error at run time. The code you’ve been looking at checks that an object is a fill-in before it checks the
DATA-TYPE, but suppose for a moment that the originalTYPEcheck wasn’t there:
IF hObject:DATA-TYPE = "INTEGER" THENASSIGN hObject:SENSITIVE = NOhObject:BGCOLOR = 8.ELSE hObject:BGCOLOR = 10.
When Progress tries to retrieve the
DATA-TYPEof an object that doesn’t have this attribute, such as the browse, you get the error shown in Figure 19–13.Figure 19–13: DATA-TYPE error message
![]()
To avoid this error, in cases where you can’t be sure whether the attribute matches the object type, you can use the
CAN-QUERYfunction to check whether something is a readable attribute before your code does so.CAN-QUERYtakes two arguments, a valid object handle and a character expression that evaluates to an attribute name. This code example eliminates the error:
Likewise, you can check in advance whether something is a writable attribute using the
CAN-SETfunction, which also takes an object handle and attribute name as arguments:
IF CAN-SET (hObject, "SENSITIVE") ANDCAN-SET(hObject, "BGCOLOR") THENASSIGN hObject:SENSITIVE = NOhObject:BGCOLOR = 8.
You’ll find these functions useful especially in cases where the attribute name itself is a variable, so that you can’t be sure when you write the code whether all possible values will be valid.
To see a list of all the valid attributes you can set or query for an object, use the
LIST-QUERY-ATTRSandLIST-SET-ATTRSfunctions. Each function takes a valid object handle as an argument:
As you can see in Figure 19–14, the list of attributes for most objects is quite large. You can find out about all of them in the online help, as well as in the third volume of OpenEdge Development: Progress 4GL Reference.
Figure 19–14: Result of LIST-QUERY-ATTRS function example
![]()
Adding dynamic objects to a window
To see dynamic objects in action, you can add a few to the test window. The goal is to let the user select one or more fields from the OrderLine table to display alongside the Order browse. Because these fields are the user’s choice at run time, they are dynamic fill-ins.
To add dynamic objects to the sample window:
- Open the
h-CustOrderWin7.wtest window in the AppBuilder. Widen it somewhat to make room for some additional objects.- Pick the Selection List object from the Palette:
![]()
- Drop the Selection List onto the window to the right of the Customer fields.
- Double-click on the Selection List to bring up its property sheet:
![]()
- Name the object OlineFields.
- Check on the Multiple-Selection toggle box. This option allows the user to select more than one entry from the object at run time.
Notice that there is a choice between List-Items and List-Item-Pairs in the property sheet. Selection lists, combo boxes, and radio sets all provide this choice. If you set up the object to use List-Items, then the values displayed and selected in the object are the actual values stored in the field in the underlying variable or database field. In this case, the initial value of the object, which establishes the list of choices, is a simple comma-separated list of those values. If you set up the object to use List-Item-Pairs, then the values displayed are paired with another set of values that are the ones actually stored in the variable or field. In this case, the initial value of the object is a comma-separated list of alternating displayed and stored values. You use this option if the value stored is a coded value that is not meaningful to the user, and the user should instead choose from a more meaningful set of labels for those values. In this case, the default choice of List-Items is appropriate. Instead of setting the list as its initial value in its property sheet, you’ll establish the list of OrderLine fields at run time.- Choose OK to save your changes to the property sheet.
- Select the Text object from the Palette, drop it onto the window above the selection list, and give it a value of Show OrderLine Fields:
![]()
Using a buffer handle and buffer field handles
In the next chapter, you’ll learn how to define dynamic data management objects such as buffers, and queries. In the meantime, it is useful to know that these objects, whether static or dynamic, have handles and attributes just like any other Progress object.
To see how to use buffer handle and buffer field handles:
- Create a new internal procedure called initSelection.
- Begin the internal procedure with these definitions:
To build a list of all the fields in the OrderLine table at run time, you need to be able to walk through a list of those fields in the OrderLine record buffer.- To do this, you first need to use the
HANDLEattribute to get the buffer handle:
Notice that you need to include theBUFFERkeyword in the statement so that Progress knows how to identify the literal value OrderLine.Next, you need to know that the buffer object has an attribute calledNUM-FIELDSthat conveniently tells you how many fields there are in the buffer.- Use this code to start a block that walks through all those fields:
There is another Progress object that represents a single field in a buffer. You get the handle of a particular field object using the buffer’sBUFFER-FIELDattribute.BUFFER-FIELDtakes a single argument, which can be either the sequential field position within the buffer or the field name.- In this procedure, since you just want to walk through all the fields to build up a list, use its position to identify it:
- Finally, you need to know that, like other objects, the
BUFFER-FIELDhas various attributes you can query or set. In this case you want theNAMEattribute. The Progress 4GL (beginning in Progress Version 9.1D) lets you chain multiple colon-separated references together in a single expression, such as this:
The only requirement is that each of the elements in the expression until the last one must be a handle, since each element is in turn an attribute of the handle that the expression yields at that point in its evaluation. Thus the expression above represents this sequence:Starting with the handle to the buffer, retrieve the handle of the buffer field that is in positioniField. Then retrieve theNAMEattribute of that field handle.You could even leave out the earlier step of saving off the buffer handle in a variable and put that into the expression as well, as in:
In this example, you did not to do this because the buffer handle is referenced in several different statements in this little procedure, so it’s more efficient and makes the code a bit more readable to save the value off once and reuse it.Populating a list at run time
There are two methods you can use on a selection list, combo box, or radio set to set the value list at run time:
ADD-FIRSTandADD-LAST. Each has the same two forms:
object-handle:ADD-FIRST(item-list)object-handle:ADD-LAST(item-list)object-handle:ADD-FIRST(label,value)object-handle:ADD-LAST(label,value)
Use
ADD-FIRSTto add items to the beginning of the list andADD-LASTto add them to the end. If the object uses the simple List-Items form, in which the actual object values are displayed, then use theitem-listform of the method to add one or more items to the list. If the object uses the List-Item-Pairs form, then use the second form of the method to specify a singlelabelfollowed by the singlevalueit represents.These types of objects have a
DELIMITERattribute to allow you to set a delimiter between items other than the default comma, in case one of the values or labels contains a comma.The objects also support a logical
SORTattribute, which initially is false. If you setSORTto true, then displayed items are sorted by their label. In this case, there is no meaningful difference between usingADD-FIRSTandADD-LAST.The methods return true if the operation succeeded, and false if for any reason it failed.
To use the
ADD-LASTmethod to add each of the OrderLine field names to the end of the selection list:
- Enter this code to complete the
initSelectionprocedure:
- To display these values in the list when the window is viewed, add a
RUNstatement to the procedure’s main block:
Creating dynamic fields
Before you look at the code for this example that actually creates and manages the dynamic objects, this section summarizes some basic principles of setting up dynamic field-level objects.
Frame parenting
You can place a dynamic field-level object in either a static or a dynamic frame. To do this, assign the frame handle to the
FRAMEattribute of the object. For a field-level object, there is also aPARENTattribute. Remember that there is a field group object that acts as the immediate container for field-level objects, in effect in between the frame and the individual objects. ThePARENTattribute points to the field group, not the frame. Progress automatically puts the object into a field group when you assign itsFRAMEattribute and also when you assign the object a default tab position, if it can receive input.Object positioning
To arrange objects in a frame, you must explicitly position each one by setting the appropriate vertical (
ROWorY) and horizontal (COLUMNorX) attributes. However, Progress does assume the topmost and leftmost position in the frame if you do not set a placement attribute for the object. This means that, if you place multiple dynamic objects into a frame without positioning them properly, they all wind up on top of one another.Object sizing
You can size an object, depending on its object type and data type, using either the various height and width attributes or the
FORMATattribute. Because of the imprecise nature of width calculations for values displayed in variable-width fonts, you might have to adjust either the format or the width to be appropriate for the values you’re displaying. This is no different than for static objects in a graphical environment. Be aware that aFORMATof“X(20)”is not necessarily the same width in terms of screen real estate as aWIDTH-CHARSof20. Generally, Progress uses a formula for calculating a format that is slightly more generous (that is, yields a slightly greater width) than the formula for calculating the width fromWIDTH-CHARS. Some amount of trial and error might be necessary to arrive at the right format or width for the type of data typically displayed in a field. Capital letters, for example, are on average much wider than lowercase letters, so a field that is displayed all in capitals likely needs a greater width than a lowercase or mixed-case field.The sample code in the "Using the FONT-TABLE to make the labels colon-aligned" provides an example of using a built-in system handle called
FONT-TABLEto calculate the display width of a specific string in a specific font, which you can also use to assign an appropriate width.Label handling
You must provide a separate text object as a label for dynamic data representation objects, including fill-ins, combo boxes, editors, radio sets, selection lists, sliders, and text fields. When you drop fill-ins into the AppBuilder’s design window, for example, the AppBuilder is actually generating separate, dynamic label objects so that you see how the label will look at run time, since it is creating dynamic objects at design time to build up the contents of what will become a static frame and static field-level objects when you save it and it generates code for the frame and object definitions. When you create your own dynamic objects, you have to supply the dynamic text label yourself.
If you want a side label for a fill-in field, or one of the other dynamic data representation object types, you must create a separate text object and then assign its handle to the
SIDE-LABEL-HANDLEattribute of the object it is a label for. For any other type of label, such as vertical columns, you must create and manage the text object completely separately. You must also position text objects used as labels explicitly, even for side labels. Progress assigns no positioning information for dynamic side labels, as it does for button or toggle box labels. TheSIDE-LABEL-HANDLEattribute on the fill-in does not actually provide any automatic services such as moving the label together with the field. It is simply a useful way to help you navigate between the field object and its label object when you need to.The example code described in the "Adding dynamic fields to the test window" shows you how to define side labels for dynamic fill-ins.
Data handling
Unlike static data representation objects, dynamic objects have no field or variable implicitly associated with them. You must explicitly assign data between an object’s
SCREEN-VALUEattribute and the field or variable you use for data storage. This allows you to use a single object to represent multiple fields or variables at different times, if you wish, limited only by the object and data type.Data typing
Some dynamic objects support entry and display data types other than
CHARACTER. In particular, fill-ins and combo boxes support the full range of Progress entry and display data types. It’s important to understand that, for dynamic objects, this support is for entry validation and display formatting purposes only. TheSCREEN-VALUEattribute always holds the data in character format, no matter what the object’s data type. You must make all necessary data type conversions using the appropriate functions (STRING,INTEGER,DECIMAL, etc.) when assigning data between the object’sSCREEN-VALUEand the field or variable you use for data storage.Adding dynamic fields to the test window
The first thing you need to add to
h-CustOrderWin7.wto create dynamic fields is a variable or other storage to hold their handles. Because the whole purpose of the exercise is to allow the user to select a variable number of fields to display, there is no reasonable way to store each one’s handle in a separate variable.Storing a list of handles
You could store the object handles in a
HANDLEvariable array that has anEXTENT, but this is almost certainly a bad idea. The first rule of using a variable with an extent is that you should do it only when the proper value for the extent is clear, based on the nature of the data it is holding, such as values for the seven days in a week or the twelve months in a year. If you just try to pick a value that seems big enough, you will often regret it later when that number turns out to be too small for some case you hadn’t anticipated.The method used in the example is just to store the handles in a list, in character form. For a modest number of values, this is quite reasonable, and the conversion effort back and forth between a handle and its character representation is not significant.
Always keep in mind the alternative of using a temp-table to store a set of values during program execution. Although the overhead of having to perform a
FINDon what amounts to a special database table may seem significant, in fact temp-tables are extremely fast. Most or all of the records you need to work with will likely be in memory anyway and, with the ability to index fields that you need to retrieve or filter on, even a large temp-table should provide very good performance. A temp-table is well suited to situations where the number of possible values you need to keep track of can grow large. How large is large? There’s no precise answer to this, but it is probably a good rule of thumb that if you’re storing more than a few dozen values, it is cleaner and possibly faster to use a temp-table. A temp-table is also the right choice when you need to store several related pieces of information for each item, each of which can become a field in the temp-table definition.For this example you simply use a character variable. Its value needs to persist for the life of the procedure, because the handles are saved off by one internal procedure or trigger block and used by another.
To create dynamic fields in the sample window:
- Define the
cFieldHandlesvariable in the Definitions section ofh-CustOrderWin7.w, which scopes the variable definition to the whole procedure:
- Write a block of code to execute whenever a new Order is selected. This is the
VALUE-CHANGEDevent for the browse, which you’ve used in an earlier variation of this procedure.The code in theVALUE-CHANGEDtrigger needs to find the first OrderLine for the Order. For the sake of simplicity, the example does not navigate through all the OrderLines, but you could easily extend it to do this. Then, it looks at the existing list of dynamic field handles (if any) and clears them out by setting theirSCREEN-VALUEto blank:
- Define a
LEAVEtrigger for the OLineFields selection list. The trigger uses these variables:
- To allow for the case where this is not the first time the user has selected a list of fields, add code that first deletes the existing fields using their object handles, which are stored in a list in the
cFieldsHandlevariable:
DO iField = 1 TO NUM-ENTRIES(cFieldHandles):hField = WIDGET-HANDLE(ENTRY(iField,cFieldHandles)).DELETE OBJECT hField NO-ERROR.END.Remember that if you neglect to do this, each new request would add more objects to the session that aren’t being used anymore. TheNO-ERRORqualifier on theDELETE OBJECTstatement simply suppresses any error message in the event that the object has already been deleted in some other way.How about when the procedure is terminated? Do you need code to delete the dynamic fields that are around at that time to prevent a memory leak? The answer is no, but only because of the widget pool created in the Definitions section, which cleans up all dynamic objects created by the procedure when the procedure terminates. That’s why the widget pool convention is so valuable. Without the widget pool created for the procedure, you could leave dynamic objects in memory for the duration of the session, even after the procedure exits.Since this code is theLEAVEtrigger for the selection list, the field’sSCREEN-VALUEattribute holds the value the user selected. In the case of a multiple-selection list such as this, the value is actually a comma-separated list of all the entries the user selected.- Save this value in a variable to keep the rest of the code from having to refer to the
SCREEN-VALUEattribute over and over again:
- Add a block to iterate through all the selections. You saw earlier how the
BUFFER-FIELDattribute on a buffer handle can take the ordinal position of the field in the buffer as an identifier. You can also pass the field name, as the code does here. Once you’ve retrieved the handle of the selected field, the code can query a number of different field attributes through that handle:
DO iField = 1 TO NUM-ENTRIES(cFields):ASSIGN cField = ENTRY(iField, cFields)hBufField = BUFFER OrderLine:BUFFER-FIELD(cField).- Create the text label for the fill-in. As you learned earlier, the label must be a separate text object:
TheCREATEstatement parents it to the frame, sets its data type, calculates a format and value for it using theLABELattribute of the current buffer field, and positions it in the frame. TheHEIGHT-CHARSof 1 makes the label text align properly with the value displayed next to it. TheCOLUMNpositions it next to the browse, and the row is incremented each time through the loop to define a distinct position for each field.- Create the fill-in object itself:
The data type, format, and value all come from the buffer field object handle. TheSIDE-LABEL-HANDLEattribute connects this fill-in to its handle object. TheCOLUMNsetting allows room for the label before displaying the field value. TheSCREEN-VALUEassigns the value from the buffer field’sBUFFER-VALUEattribute. TheHIDDENattribute makes sure the field is viewed along with the frame that contains it.- Increment the row counter to set the position of the next field, and save off the handles of the labels and fill-ins in a list:
ASSIGN dRow = dRow + 1.0cFieldHandles = cFieldHandles +(IF cFieldHandles = "" THEN "" ELSE ",") +STRING(hLabel) + "," + STRING(hField).END. /* END DO iField */- Make sure that the
VALUE-CHANGEDtrigger for the Order browse fires whenever a different record is displayed. This includes when the procedure first starts up, so make this addition to the main block:
- Make the same addition to each of the navigation button triggers, as you have done to another version of the procedure in "Record Buffers and Record Scope."
- Run the window. Now you can select one or more fields from the selection list, tab out of it, and see those fields displayed as dynamic fill-ins with dynamic labels next to the browse:
If a few of the fields seem to be positioned rather far to the right (the Order Num for instance), it’s because they are right-justified numeric fields with overly generous display formats as defined in the Data Dictionary. Specifically, the OrderNum and ItemNum fields are defined in the schema with a long format that uses the Z character to format leading zeros. The Z tells Progress to replace leading zeroes with spaces, which pushes the displayed value out to the right. Others, such as the Price, are formatted with the > character, which tells Progress to suppress leading zeroes, effectively left-justifying the value. This is just a result of the formatting choices made by the database designer and has nothing to do with the display of dynamic values.Using the FONT-TABLE to make the labels colon-aligned
This display looks all right as far as it goes, but in many cases you want your labels to appear right-justified rather than left-justified. In other words, you want the colons that end each label to be vertically aligned, so that all the field values can begin at the same column position to the right of that. How can you do this?
Progress provides a built-in system handle, called
FONT-TABLE, which is an object representing the current font. There are four useful methods you can apply to this handle to calculate the actual size of a value when it’s displayed:GET-TEXT-WIDTH-CHARS,GET-TEXT-HEIGHT-CHARS,GET-TEXT-WIDTH-PIXELS, andGET-TEXT-HEIGHT-PIXELS. In an alternative version of the trigger code for the selection list, you can use this function to align the labels on their colons.To colon-align your labels:
- Change the column setting in the
CREATE TEXTstatement for the label to this:
/* COLUMN = 85.0. -- modified to do colon-aligned labels */COLUMN = 100.0 - FONT-TABLE:GET-TEXT-WIDTH-CHARS(hBufField:LABEL + ":").Instead of starting at column 85 and positioning the label to the right, the statement starts where the labels should end (column position 100), and subtracts the label width as calculated by the method on theFONT-TABLE. This gives the right starting position for the label object.- Change the column assignment for the fill-in to be fixed at column 102:
This places each displayed value at the same position, two positions to the right of the label.There’s a third change you have to make as well. As discussed in the "Object sizing" , the format calculation for the label is likely to provide a format somewhat larger than the actual display width. This is a deliberate adjustment Progress makes to provide a format for a field that is large enough to display most values without truncation. In the case of your labels, however, you might find that if you just use the label format to determine the width, the display width is a bit too large and overwrites the beginning of some of the displayed values with blanks. To correct this, you need to specify an accurateWIDTH-CHARSattribute value for the label, so that it is just large enough to display itself without truncation but not so large that it overwrites the value that follows it.- Use the same
FONT-TABLEmethod to calculate theWIDTH-CHARSof the text label object, adding this assignment to theCREATE TEXTstatement:
- Run the window. With these changes, you see a different display where the labels of the dynamic fields are colon-aligned:
![]()
- Select another row in the browse, and the field values are set to blank by the
VALUE-CHANGEDtrigger on the browse. What you see, however, is that because the displayed fields are not allCHARACTERfields, Progress applies the field’s format to the blank value, which in the case of a numeric field is the value zero, and is displayed in various ways by the different field formats:
Clearly this procedure is an interesting example of using dynamic fields but not a terribly useful application window. You would probably want to be able to select a list of fields once and use them to display OrderLine values for any Order you select, rather than having the fields blanked out after one OrderLine is displayed. And there must be a way to navigate through the OrderLines of an Order rather than just seeing the first one. Feel free to extend the test procedure to provide these abilities.Since there’s room in the window to display all the OrderLine fields, the whole notion of making them dynamic fill-ins is also of limited use. A more realistic example of how you can use this technique in an application would be for a table with a great many fields, only a few of which each user needs to work with. After you learn about dynamic data management objects in the next chapter, you will also be able to allow the user to select a completely different table to display in place of the OrderLines.Field format versus width
The relationship between format and width that the example highlights might seem confusing. Keep these basic guidelines in mind:
- The
FORMATis the maximum number of characters the user is allowed to type into the field at run time.- The
WIDTH-CHARS(orWIDTH-PIXELSif you measure your objects that way) is the actual screen real estate allotted to the object, whether it’s for display or for data entry purposes. You can use theFONT-TABLEmethods to calculate a precise width for a single displayed value, but you must estimate an appropriate width for a field that can be used to display many values, for example as you navigate through many records in a table.- If you assign a format but not a width, Progress estimates a width based on the format. This normally is slightly greater that the width as based on
WIDTH-CHARS, because Progress tries to provide additional space to prevent truncation of, in particular, short values with wide characters (such as a two-character capitalized state abbreviation, for instance). Therefore, you must assign an explicit width (usingWIDTH-CHARSorWIDTH-PIXELS) if you want to make sure the display space isn’t bigger than it needs to be, for example to prevent overwriting an object displayed next to the object you’re sizing.- If, on the other hand, you assign a width but not a format, Progress uses a default display format for the data type. For a
CHARACTERfield, for example, this is“X(8)”. This is not likely to be appropriate, so you almost always need to assign a format.- If the format is larger than the display width for a
CHARACTERfill-in field that is enabled for input, the user can type as many characters as the format allows. If the display space runs out, the fill-in automatically scrolls to allow the user to enter more data. This is very often a desirable user interface design, which displays a reasonable number of characters for a field but allows the user to type more when necessary (and have it all stored in the underlying field).- If the format for a
CHARACTERfill-in is smaller than the display width and the user tries to type more characters than the format allows, Progress prevents this. The user gets only a warning bell as he continues to type, even though there is still visible display space available that was allocated by the width attribute. This is a very bad user interface design, which leads to the following basic guiding principle.- When in doubt, make the format generously large and the width appropriate for the display. Limit the format size for enabled fields only when it is important to limit the number of characters stored in the underlying variable or database field. Remember that all Progress
CHARACTERvariables and fields are inherently variable width, so the format of a database field does not allocate a fixed storage size, but only what a particular field value uses for each record.Using multiple windows
As you’ve already learned, there is no
DEFINE WINDOWstatement in Progress. Any windows you build for your application are dynamic windows. This section summarizes some of the window handles and attributes that can be useful to you in designing an application with multiple windows.You create a window with the
CREATE WINDOWstatement, which has the same syntax as every otherCREATEstatement you’ve seen.Window system handles
The simple examples in this book that don’t use a window you create (or that is created for you in code the AppBuilder generates) use the default window that is part of every session. This has a system handle called
DEFAULT-WINDOW. This handle is not something you would ordinarily use in a real application.You’ve seen the
CURRENT-WINDOWsystem handle, which holds the handle of the window used by default for parenting frames, dialog boxes, and message alert boxes. TheCURRENT-WINDOWattribute of a procedure overridesCURRENT-WINDOWfor the context of that procedure only, without changing the value of the session-wide system handle. The statements in the standard AppBuilder window template that set bothCURRENT-WINDOWand theCURRENT-WINDOWprocedure attribute to the procedure’s window provide a good default for parenting of objects created and used in that procedure.Another useful system handle is
ACTIVE-WINDOW, which holds the handle of the window that has received the most recent input focus in the application. This handle can help you assure that a dialog box or message alert box appears parented to the window where the user is currently working, even if it is not the current window of the procedure that executes the code to display the dialog box or message.Useful window attributes, methods, and events
Table 19–1 describes the numerous attributes, methods, and events that you can use to control the appearance and behavior of windows in your application.
WINDOW-CLOSE event example
The simplest example of an action on the
WINDOW-CLOSEevent is this:
If the
WAIT-FORstatement is the last executable statement in a nonpersistent procedure, the event satisfies theWAIT-FORand the procedure terminates.The AppBuilder window template uses indirection to direct the
WINDOW-CLOSEevent for the window to the procedure itself, using this standardON WINDOW-CLOSEtrigger:
DO:/* This event will close the window and terminate the procedure. */APPLY "CLOSE":U TO THIS-PROCEDURE.RETURN NO-APPLY.END.
You’ve already seen the rest of the steps in this sequence, with the
CLOSEevent on the procedure runningdisable_UIand deleting the procedure itself.Creating window families
By default, when you create a window, Progress parents that window transparently to the window system. In this way, windows you create are siblings of each other. You can also parent a window to another window by setting the one window’s
PARENTattribute to the handle of the other. Windows that are parented to another window form a window family. A window parented directly to the window system can be called the root window of a window family. Windows parented by any child window, in turn, form a child window family. A window can be parented to only one other window at a time, but can have multiple child windows.Window families share a number of properties that make them convenient for both applications and users to manage:
- Coordinated viewing and hiding — When you view any member of a window family (by setting its
VISIBLEattribute to true, setting itsHIDDENattribute to false, or using theVIEWstatement), the whole window family is viewed unless theHIDDENattribute is true for at least one other member. IfHIDDENis true for a parent or other ancestor of the window that is viewed, no windows in the family are viewed. The only effect is that theHIDDENattribute for the window you try to view is set to false. If, on the other hand, any child or descendent window hasHIDDENset to true, then all the windows in the family are viewed, except the descendent window and its descendents. When you hide a member of a window family by setting itsVISIBLEattribute to false, that window and all its descendents are hidden, but theirHIDDENattributes are not changed. In this way, you can hide and view an entire family of windows by changing theVISIBLEattribute of the top window. You can leave any window or part of a hierarchy of windows out of the hiding and viewing by setting theHIDDENattribute of the window at the head of that part of the hierarchy to true.- Coordinated minimizing and restoring — When you minimize a window, all of its descendents disappear from view, unless they are already minimized. Any minimized descendents appear separately in the taskbar and can be restored individually. When you restore a parent window, any of its hidden descendents are redisplayed.
- Coordinated close events — If a parent window receives a
WINDOW-CLOSEevent, it propagates thePARENT-WINDOW-CLOSEevent to all of its descendent windows. Progress supports these as two separate events so that a procedure can react in a special way to the parent of its window being closed. TheWINDOW-CLOSEevent does not propagate any events upward to its parent window.Creating a dynamic browse
The most complex graphical object is the browse object. You can create a browse dynamically and specify programmatically all its attributes, including what table and query’s records are displayed, what columns it displays, which columns are enabled for input, and its visible attributes such as size and position.
You can assign most of a dynamic browse’s attributes in the
CREATE BROWSEstatement:
In addition, you can specify some attribute values individually following the
CREATEstatement:
Here are some of the principal attributes you can set either in the
CREATE BROWSEstatement or in a separate assignment on the browse handle:
FRAME— You must associate the browse with a frame it is visualized in.- X and Y, or ROW and COLUMN — You should specify a position for the browse. Otherwise, it is positioned in the upper-left corner of the frame. As for other objects,
XandYare in pixels,ROWandCOLUMNare in character rows.- WIDTH or WIDTH-PIXELS — You should specify a width for the browse using the
WIDTHattribute in characters or theWIDTH-PIXELSattribute in pixels.- HEIGHT, HEIGHT-PIXELS or DOWN — You should specify a height for the browse using either the character
HEIGHTorHEIGHT-PIXELSattribute or theDOWNattribute.DOWNspecifies the number of rows to display. Keep in mind that if you use theHEIGHTorHEIGHT-PIXELSattribute, then any horizontal scrollbar uses part of that height. The overall height of the browse remains the same whether there is a horizontal scrollbar or not. If you specifyDOWN, that determines the number of rows of data to display, and any horizontal scrollbar is added to that height.ROW-HEIGHT— Set this decimal attribute if you want each row to be different from the default, which is calculated based on the font.SENSITIVE— You should generally make a browse sensitive so that the user can use the scroll bar.QUERY— You must provide the browse with the handle of a query that provides its data to display.VISIBLEorHIDDEN— You must set eitherHIDDENto false, if you want the browse to be viewed when its parent frame if viewed, orVISIBLEto true to force the frame and the browse to be viewed.READ-ONLY— Set this option to true if you are not going to enable columns in the browse.SEPARATORS— Set this option to true if you want lines between the columns and rows of the browse.ROW-MARKERS— Set this option to false if you do not want row markers at the beginning of each row.NO-VALIDATE— Set this option to true to prevent Progress from compiling field-level validation phrases from the schema into the browse.TITLE— Set this string if you want the browse to have a title bar.MULTIPLE— Set this option to true if you want to enable multiple selection of rows.Some attributes can only be assigned before the browse is realized. In the case of a dynamic browse, this is generally when the browse is made visible, which you can do in one of two ways:
In a
CREATE BROWSEstatement, the attributes of the browse in theASSIGNlist are assigned in sequence. So, as this single statement is executed, if Progress encounters theVISIBLE = TRUEorHIDDEN = FALSEphrases (and its parent frame is visible), the browse is realized at that time. If you try to assign an attribute later in the statement, or in a separate statement, which has to be assigned before the browse is realized, you get an error at run time. For example, if you don’t want the browse to have row markers at the beginning of each row, you set theROW-MARKERSattribute toFALSE. If you put this assignment into theCREATEstatement after an attribute that realizes the browse, you get the error shown in Figure 19–15.Figure 19–15: ROW-MARKERS error message
![]()
If this occurs, you have to reorder the assignments in the
CREATE BROWSEstatement. Generally, you should put the assignment that realizes the browse at the end of the statement or in a separate attribute assignment statement.In the same way, you can modify most attributes in separate statements after the browse has been realized, except for attributes such as
ROW-MARKERS.The one thing you cannot set within the
CREATE BROWSEstatement is the list of columns to display. There are three methods on the browse handle that set the column list after the browse has been created.ADD-COLUMNS-FROM method
If you want all or most of the columns from the query’s buffer to be displayed in the browse, use the
ADD-COLUMNS-FROMmethod:
If you specify a comma-separated
except-listexpression, the columns in the list are not included in the browse. All other columns from thebuffer-nameare included. If the browse’s query uses more than one buffer, you can invokeADD-COLUMNS-FROMmore than once on different buffers used by the query.ADD-LIKE-COLUMN method
If you want to add columns individually, you can use the
ADD-LIKE-COLUMNmethod:
This method adds one column at a time to a browse, based on a field name string expression or field handle in a buffer. You can add any number of columns to a browse by making successive calls to this method.
ADD-CALC-COLUMN method
The
ADD-CALC-COLUMNmethod creates a single column based on a list of specified properties rather than deriving it from a specific field in a buffer. This is typically used as a placeholder column for a calculated value:
[column-handle= ]buffer-handle:ADD-CALC-COLUMN(datatype-exp,format-exp,initial-value-exp,label-exp[ ,pos] )
Here are descriptions of the
ADD-CALC-COLUMNoptions:
column-handle— The handle of the column object returned from the method. You might want to capture this value to associate the column with aROW-DISPLAYtrigger that populates it.datatype-exp— A literal string or character expression evaluating to the data type for the column.format-exp— A string or character expression evaluating to the format for the column.initial-value-exp— A string or expression evaluating to the initial value for the column.label-exp— A string or expression evaluating to the column label for the column.pos— The integer position of the new column within the browse display list. If you do specifypos, the column goes at the end of any columns already defined.Notes on dynamic browses and browse columns
Here are some facts to consider when you create and use dynamic browses and browse columns:
- You can set attributes on any of the columns in a dynamic browse after you have added them.
- You can use all three of the
ADDmethods to add more columns to a static browse, as well as a dynamic browse. You can take advantage of this to add columns to a browse that already has some columns statically defined, or you can define a static browse with no columns at all as a placeholder for a browse whose query and columns are defined dynamically at run time. In this way, you can define some of the visual attributes of the browse statically and then fill in the definition at run time.- You cannot specify the
CAN-FINDfunction in the validation expression for a dynamic browse column. This is a principal reason why you normally want to set theNO-VALIDATEbrowse attribute toYESfor a dynamic browse, in case there are any such expressions in the field-level validation inherited from the schema.- You can set the
VISIBLEattribute for a browse column as well as for the browse as a whole.- If you use the
ADD-CALC-COLUMNmethod to create one or more columns to hold calculated values, you must define theROW-DISPLAYtrigger where the value of the calculated column is set prior to adding the column, so that the trigger is already established before the column is added. This is to ensure that the initial viewport of the calculated column is correctly populated.- A dynamic browse, as well as a static browse to which you add columns dynamically, automatically becomes a
NO-ASSIGNbrowse. This means that you do not get any default update handling from Progress. You must take care of capturing changes to the browse data and applying those changes in procedure code.- You can change the query of either a dynamic or a static browse, even if the underlying fields are not the same as those of the original query. However, if the underlying fields are not the same, all browse columns are removed and you have to specify the columns again using the
ADD-COLUMNS-FROM,ADD-LIKE-COLUMN, andADD-CALC-COLUMNmethods. If you set theQUERYattribute of a browse to the Unknown value, all browse columns are removed.Extending the sample procedure with a dynamic browse
If you start with
h-CustOrderWin7.w, you can add a dynamic browse to it to illustrate some of the capabilities of this object.To show all the OrderLines for the current Order in a browse:
- In the Definitions section, add these definitions:
ThehBrowsevariable will hold the handle of the dynamic browse you create, andhCalcColwill hold the handle of a dynamic calculated column for it. The query definition will be attached to the browse when you create it. These definitions need to be here at the top level of the procedure so that their values persist beyond the end of the trigger where they are assigned.- Add this statement to the end of the
initSelectioninternal procedure, which sets the list of OrderLine fields in the selection list:
The new entry represents a calculated column that holds the OrderLine price before the discount is applied.- In the
LEAVEtrigger for the selection list called OlineFields, comment out all of the code.This code created dynamic fields to display an OrderLine. Now you’ll display a browse in the same place.- Because you can create a browse over and over again with different lists of columns, you should first check to see if there’s already a dynamic browse and delete it:
/* This block of code is the second version of the trigger, which createsa dynamic browse to show the selected fields. */IF VALID-HANDLE(hBrowse) THENDELETE OBJECT hBrowse.- Add new code to create a dynamic browse. This complex statement defines the browse and its attributes:
- If necessary, adjust the size and position of the browse according to the layout of your own window and Order browse.
- Add code that walks through the selected fields from the OlineFields selection list as the dynamic fill-in code does:
cFields = OLineFields:SCREEN-VALUE.DO iField = 1 TO NUM-ENTRIES(cFields):cField = ENTRY(iField, cFields).- If the selected field is the calculated field, you must add a calculated column to the browse to display it:
IF cField = "Price B4 Disc" THENhCalcCol = hBrowse:ADD-CALC-COLUMN("Decimal", /* Data type */">,>>>,>>9.99", /* Format */"0", /* Initial value */"Price B4 Disc"). /* column label */- Otherwise, add a column like the selected field in the OrderLine table:
- Add this
ROW-DISPLAYtrigger, which executes each time a row is displayed in the browse:
Because the code in the trigger block forLEAVEof OlineFields goes out of scope as soon as the trigger block ends, you must put the code for this nested trigger definition in a separate procedure, and use the special syntaxPERSISTENT RUN calcPriceB4Discto make Progress keep track of what to run when theROW-DISPLAYevent fires.- Add this
LEAVEtrigger to open the OrderLine query from the Definitions section:
You attached this query to the browse in theCREATE BROWSEstatement.- Add this code for the
calcPriceB4Discprocedure:
A trigger such as this should always check first that its column handle is valid in case it is fired once before the column is created. In this case, the column is optional, so it must always check first to see if it’s there.- In the
VALUE-CHANGEDtrigger for the OrderBrowse, comment out the existing code that blanks out the dynamic fill-ins and replace it with a statement that re-opens the OrderLine query:
/* This is the second version of this trigger, for the dynamic browseexample. */OPEN QUERY qOrderLine FOR EACH OrderLine WHERE OrderLine.OrderNum= Order.OrderNum.This code refreshes the OrderLine browse each time an Order is selected.- Add this statement to each of the four navigation button triggers to apply the Order browse’s
VALUE-CHANGEDtrigger:
The query opens when a new Customer is selected and its Orders first displayed.- Run
h-CustOrderWin7.w. You can select some OrderLine fields along with the calculated field and see the results of your work:
![]()
Accessing the browse columns
Each of the browse columns is an object in its own right, with its own handle. If you want to set individual column attributes at run time (for example, to enable columns for update), you set those attributes through the handle. Static browse columns are objects with handles as well, but you can also access those by name. Dynamic browse columns, like other dynamic objects, have no name, so you must use their handle.
Note: To make a dynamic browse column updateable, set itsREAD-ONLYattribute to false. Browse columns do not use theSENSITIVEattribute.The browse
FIRST-COLUMNattribute returns the handle to the first (leftmost) column in the browse. Each column’sNEXT-COLUMNattribute returns the handle of the column next to it. For example, you can add this block of code to theOlineFieldsLEAVEtrigger:
When you run the procedure again and select fields, Figure 19–16 shows what you see.
Figure 19–16: Result of COLUMN example
![]()
Creating a dynamic menu
This final section shows you how to create a dynamic menu for a window. Dynamic menus share the same characteristics of static menus. As with other dynamic objects, you use
CREATEstatements rather thanDEFINEstatements to create a menu, submenu, or menu item. You parent these objects together in a hierarchy by setting theirPARENTattribute. If you need to delete a dynamic menu before its widget pool is deleted, you use theDELETE OBJECTstatement to delete the top-level menu object. Progress automatically deletes all its child submenus and menu items.When you define a static menu, you need to define its elements in reverse order, so that you define the submenus before you reference them in the static
DEFINE MENUstatement. With dynamic menus you do the opposite. You first define the top-level menu, then its submenus, and then each submenu’s menu items. As you create each one, you parent it to the next level up to establish the menu hierarchy.Creating a menu
To create a menu, use this statement:
As with other dynamic objects, you can assign one or more attributes as part of the
CREATEstatement or in separate statements that use the menu handle. Typical menu attributes include:To associate a menu with a window, set the
MENUBARattribute of the window handle to the menu handle.Creating a submenu
To create a submenu, use the
CREATE SUB-MENUstatement:
Typical attributes you can assign for a submenu include:
You can also define a trigger block (typically for a
CHOOSEtrigger) as part of theCREATEstatement.Creating menu items
In a dynamic menu, menu items are individually created objects with their own handles. Because you can delete an entire menu by deleting the top-level menu, or by deleting the procedure that controls the widget pool for the menu, you can normally use the same object handle for all your menu items. Each one must be parented to the submenu that contains it. Therefore, each menu item can be used in only one menu or submenu. Typical menu item attributes include:
LABEL— The text label for the menu item.PARENT— The parent submenu handle.SENSITIVE— Set this to false to disable the menu item.SUBTYPE— The defaultSUBTYPEisNORMAL, for standard selectable menu items. Other valid values areREAD-ONLY(for a read-only menu item),SKIP(for a blank line in the menu), orRULE(to create a line between menu items).Navigating the hierarchy of menu handles
For both menus and submenus, your procedure code can traverse all the menu items and nested submenus. The
FIRST-CHILDattribute of a menu or submenu returns the handle of its first menu item or child submenu. TheNEXT-SIBLINGattribute of each menu item or submenu returns the handle of the next object at that level. Object handles are returned in left-to-right order for sibling submenus, and top-to-bottom order for menu items. You can navigate in reverse by starting with theLAST-CHILDattribute and following thePREV-SIBLINGchain. These attributes are readable only. Their values are established when you assign parents to each submenu and menu item.You can also add dynamic submenus and menu items to an existing static menu. To do this, navigate the menu using these
CHILDandSIBLINGattributes to locate the handle of the object in the menu where you want to attach another submenu or menu item. Create the new objects and parent them to the existing object. You can parent a new submenu to the menu bar or to another submenu. You can parent a new menu item to an existing submenu. New objects are added to the end of the list of current objects at that level (that is, to the right of existing submenus in a parent menu or submenu) or to the bottom of the list of menu items for a submenu.Adding a dynamic menu to the test window
In this section, you’ll add a dynamic menu to the test window. The menu displays all SalesReps and lets the user filter the list of Customers by a selected SalesRep. This example demonstrates one typical use of dynamic menus—to create a list of menu items that are data-driven. In other cases, a list of dynamic menu items might be based on currently available windows or other elements of an application that can vary at run time.
To add a dynamic menu bar to the h-CustOrderWin7.w procedure.
- Add a new fill-in field to the window to display the selected SalesRep. Name it cSalesRep.
- Create a new internal procedure called
createMenu. The procedure needs handle variables for the menu bar, its submenu, and one handle for all the menu items you create:
DEFINE VARIABLE hMenu AS HANDLE NO-UNDO.DEFINE VARIABLE hSubMenu AS HANDLE NO-UNDO.DEFINE VARIABLE hMenuItem AS HANDLE NO-UNDO.- Create the menu bar and its submenu:
- Create the list of SalesReps as menu items:
Each one is parented to the submenu. You can reuse the same object handle for each one because you won’t need to reference those objects individually. Each menu item has the SalesRep initials followed by the SalesRep’s full name as its label.When the user chooses one of these items, you want to reopen the Customer query for just Customers of that SalesRep. Remember that when you define a trigger on an object inside an internal procedure or another trigger block, the trigger definition doesn’t persist beyond the end of that procedure or block. Therefore, you have to use the specialPERSISTENT RUNstatement to tell Progress to run a separate procedure when the event occurs. You’ll write thisfilterCustprocedure in a moment.- Add a couple of special menu items to the end of the list. Create a
RULEto separate the SalesReps from the final menu item:
- Add a menu item that closes the window and its procedure:
CREATE MENU-ITEM hMenuItemASSIGN PARENT = hSubMenuLABEL = "E&xit"TRIGGERS:ON CHOOSE PERSISTENT RUN leaveProc IN THIS-PROCEDURE.END TRIGGERS.Once again, the trigger code has to be in a separate procedure.- Parent the menu bar to the window:
- Create the
filterCustprocedure for the SalesRep items:
This code reopens the Customer query for just those Customers whose initials match the first part of the menu item label. Remember that the built-in handleSELFalways evaluates to the object handle that triggered the event.The procedure displays the selected SalesRep in the new fill-in field, and then resyncs the display to the first record in the new query by programmatically choosing the First button.- Define the internal procedure
leaveProcto handle the Exit button:
This code simply invokes theCLOSEevent already defined as a standard part of the AppBuilder-generated code for the window.- Add a statement to the procedure’s main block to run
createMenu:
- Run the procedure again. Now you can drop down a list of all the SalesReps:
![]()
- Select one. Now Customers are selected for that SalesRep only:
![]()
Summary
In this chapter, you learned how to use the
CREATEstatement and handles to create and manage visual objects your application needs at run time. The next two chapters extend the discussion to cover dynamic forms of the data management objects as well: queries, buffers, and temp-tables. You’ll also look at how to create a dynamic browse to display the contents of a dynamic temp-table.
|
Copyright © 2005 Progress Software Corporation www.progress.com Voice: (781) 280-4000 Fax: (781) 280-4095 |
![]() ![]() ![]()
|