OpenEdge Development: Progress 4GL Handbook


Table of ContentsPreviousNextIndex
Using Dynamic Graphical Objects

In "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 DEFINE statement 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:

CREATE object-type handle [ IN WIDGET-POOL pool-name ]
  [ASSIGN attribute = attribute-value [attribute = attribute-value ] . . . ]
  [ trigger-phrase ] .

When you create an object dynamically you must associate it with a HANDLE variable (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-POOL phrase 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 ASSIGN phrase allows you to assign one or more attribute values for the object at the time you create it. The attribute-value for each attribute can be a constant or it can be an expression. You can use the same CREATE statement 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:

handle:attribute = attribute-value

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:

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 VARIABLE statement:

DEFINE VARIABLE handle-name AS HANDLE [ NO-UNDO ].

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 CREATE statement 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 the CREATE statement 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 expression bMyButton:LABEL. For dynamic objects you do the same thing with the object’s handle, as shown in this sequence:

DEFINE VARIABLE hButton AS HANDLE     NO-UNDO.
CREATE BUTTON hButton ASSIGN LABEL = "Test Button".
MESSAGE "Label: " hButton:LABEL SKIP
        "Type: " hButton:TYPE SKIP
        "Handle:" hButton:HANDLE SKIP
        "Dynamic?: " hButton:DYNAMIC
         VIEW-AS ALERT-BOX.

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 DEFINE statement 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 HANDLE attribute 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:attribute syntax 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 the DISPLAY statement, then you can reference its handle and other attributes:

DEFINE VARIABLE hButton AS HANDLE      NO-UNDO.
DEFINE BUTTON bStaticButton LABEL "Static Button".
DISPLAY bStaticButton bStaticButton: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 SENSITIVE attribute to YES:

DEFINE VARIABLE hButton AS HANDLE       NO-UNDO.
DEFINE BUTTON bStaticButton LABEL "Static Button".
DISPLAY bStaticButton bStaticButton:HANDLE LABEL "Button Handle:".
hButton = bStaticButton:HANDLE.
ASSIGN hButton:LABEL = "Enabled!"
       hButton:SENSITIVE = YES.
WAIT-FOR CLOSE OF THIS-PROCEDURE.

Figure 19–4 shows the result.

Figure 19–4: Enabled button example result

Managing dynamic objects

When you define a static object with the DEFINE statement, 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:

DEFINE VARIABLE hButton AS HANDLE NO-UNDO.
CREATE BUTTON hButton.

When the procedure terminates, the hButton variable 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:

DEFINE VARIABLE hButton AS HANDLE       NO-UNDO.
DEFINE VARIABLE iCounter AS INTEGER      NO-UNDO.
DO iCounter = 1 TO 10:
    CREATE BUTTON hButton ASSIGN LABEL = "Button #" + STRING(iCounter).
END.
MESSAGE hButton:LABEL.

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 OBJECT statement:

DELETE OBJECT handle.

You can also use the WIDGET keyword in place of OBJECT in this statement. The handle is a variable or temp-table field of type HANDLE, which was previously used in a CREATE statement 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 the VALID-HANDLE function:

IF VALID-HANDLE(hButton) THEN
     DELETE OBJECT hButton.

Alternatively, you can include the NO-ERROR keyword on the DELETE statement to suppress any error message:

DELETE OBJECT hButton NO-ERROR.

In either case, don’t forget the OBJECT keyword. If you do, Progress thinks you’re trying to delete a record from a table:

DEFINE VARIABLE hButton AS HANDLE       NO-UNDO.
       .
       .
       .
DELETE hButton.       /* Don’t do this! */

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-POOL statement:

CREATE WIDGET-POOL [ pool-name [ PERSISTENT ] ] [ NO-ERROR ] .

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:

/* Create an unnamed pool to store all the widgets created
     by this procedure. This is a good default which assures
     that this procedure's triggers and internal procedures
     will execute in this procedure's storage, and that proper
     cleanup will occur on deletion of the procedure. */
CREATE WIDGET-POOL.

In this way, all the dynamic objects you create from within the procedure are deleted when the procedure terminates. The CREATE WIDGET-POOL statement 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/template directory and identified by a set of text files with the .cst extension in the same directory. The AppBuilder reads and parses these .cst files on startup to build its Palette and the contents of its New dialog box. For example, the starting point for a window procedure, such as CustOrderWin, is the window.w file. For a structured procedure, such as h-WinSuper.p, it is procedur.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 CREATE object statement. 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 the DELETE WIDGET-POOL statement:

DELETE WIDGET-POOL [ pool-name ] [ NO-ERROR ] .

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:

DEFINE VARIABLE hButton AS HANDLE       NO-UNDO.
CREATE WIDGET-POOL "ButtonPool".
CREATE BUTTON hButton IN WIDGET-POOL "ButtonPool".
DELETE WIDGET-POOL "ButtonPool".
MESSAGE "What is the button handle value?" hButton SKIP
        "Is the button still there?" VALID-HANDLE(hButton).

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 PERSISTENT keyword 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-ERROR keyword 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:

DEFINE VARIABLE hButton AS HANDLE NO-UNDO.
CREATE WIDGET-POOL "ButtonPool".
CREATE BUTTON hButton IN WIDGET-POOL "ButtonPool".
DELETE WIDGET-POOL.
MESSAGE "What is the button handle value?" hButton SKIP
"Is the button still there?" VALID-HANDLE(hButton).

A DELETE WIDGET-POOL statement 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:

DEFINE VARIABLE hButton AS HANDLE"      NO-UNDO.
CREATE WIDGET-POOL "ButtonPool".
CREATE BUTTON hButton. /* No longer IN WIDGET-POOL "ButtonPool". */
DELETE WIDGET-POOL "ButtonPool".
MESSAGE "What is the button handle value?" hButton SKIP
        "Is the button still there?" VALID-HANDLE(hButton).

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-POOL statement 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:

CREATE WIDGET-POOL.
RUN subprocedure.p.
DELETE WIDGET-POOL.

In this example, the CREATE WIDGET-POOL statement creates a new default pool. Any objects created in the default pool within subprocedure.p are placed in this pool. After subprocedure.p completes, the pool is deleted along with any objects subprocedure.p might 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 CREATE statement to create a pool and a DELETE statement 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 the CREATE statement 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.p showed 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:
  1. Open the version of h-CustOrderWin1.w from Chapter Four as a starting point, as you’ve done before and save it as h-CustOrderWin7.w.
  2. In the main block of h-CustOrderWin7.w, add two lines of code, one to use h-StartSuper.p to start or locate a new super procedure called h-dynsuper.p and the second to run an internal procedure that Progress locates in h-dynsuper.p:
  3. MAIN-BLOCK:
    DO ON ERROR   UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK
       ON END-KEY UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK:
      RUN enable_UI.
      RUN h-StartSuper.p("h-dynsuper.p").
      RUN changeFields.
      IF NOT THIS-PROCEDURE:PERSISTENT THEN
       WAIT-FOR CLOSE OF THIS-PROCEDURE.
    END.

  4. Create a new structured procedure in the AppBuilder.
  5. Create a new internal procedure called changeFields.
  6. Define these variables that the procedure needs:
  7. /*---------------------------------------------------------------------
      Procedure changeFields:
      Purpose:     Locates the fields and browse in the target frame
                   and changes some of their attributes.
      Parameters:  <none>
      Notes:
    ---------------------------------------------------------------------*/
    DEFINE VARIABLE hWindow AS HANDLE     NO-UNDO.
    DEFINE VARIABLE hFrame AS HANDLE     NO-UNDO.
    DEFINE VARIABLE hObject AS HANDLE     NO-UNDO.
    DEFINE VARIABLE hColumn AS HANDLE     NO-UNDO.

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, called CURRENT-WINDOW, that Progress uses as the default for parenting frames, dialog boxes, and messages. Initially, the CURRENT-WINDOW is the same as the DEFAULT-WINDOW.

There is no DEFINE WINDOW statement in Progress. All other windows in a session are dynamic windows that you create with the CREATE WINDOW statement. When you start a procedure, you can set the CURRENT-WINDOW system handle to be a window created by your procedure. There is also a CURRENT-WINDOW attribute for the THIS-PROCEDURE system handle, which holds the procedure handle of the procedure containing the reference to THIS-PROCEDURE. Setting the CURRENT-WINDOW attribute 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-WINDOW system handle to the dynamic window the AppBuilder code creates, as you saw in "Examining the Code the AppBuilder Generates." It also sets the CURRENT-WINDOW attribute of THIS-PROCEDURE to 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-WINDOW attribute. Therefore, the first thing the changeFields procedure does is obtain that window handle. Because it is a super procedure of h-CustOrderWin7.w, the TARGET-PROCEDURE system handle within h-dynsuper.p has the same value as THIS-PROCEDURE does within h-CustOrderWin7.w:

hWindow = TARGET-PROCEDURE:CURRENT-WINDOW.

If the h-CustOrderWin7.w procedure did not set its CURRENT-WINDOW attribute, 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’s PRIVATE-DATA attribute. 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-CHILD attribute:

hFrame = hWindow:FIRST-CHILD.

If a window has more than one frame, you can access the other frames by following the NEXT-SIBLING of 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-CHILD attribute. 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 DOWN frame, 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 the FIRST-CHILD attribute of a frame, you are accessing its first field group:

hObject = hFrame:FIRST-CHILD. /* This is the field group */

Unless your frame has a DOWN attribute greater than one, the field group is not very interesting in itself. If the frame has more than one field group, you can use the NEXT-SIBLING chain to get at each of them. Otherwise, you just proceed down another level to get to the first object in the frame:

hObject = hObject:FIRST-CHILD.

In changeFields, you can combine all these steps into a single ASSIGN statement:

ASSIGN hWindow = TARGET-PROCEDURE:CURRENT-WINDOW
hFrame = hWindow:FIRST-CHILD
hObject = hFrame:FIRST-CHILD /* This is the field group */
hObject = hObject:FIRST-CHILD.

The ASSIGN statement 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 the hObject variable 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 the hObject variable, and walk through the chain of NEXT-SIBLING objects 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, changeFields looks at the TYPE attribute of each object to see if it is a FILL-IN. If it is, then it checks the DATA-TYPE attribute to see if the field is an INTEGER. If it is, then it sets its SENSITIVE attribute to false and its BGCOLOR attribute to 8, which represents the color gray. Otherwise, if the field is not an integer, it sets the BGCOLOR attribute to 10, which is the color green:

DO WHILE VALID-HANDLE(hObject):
    IF hObject:TYPE = "FILL-IN" THEN
    DO:
        IF hObject:DATA-TYPE = "INTEGER" THEN
            ASSIGN hObject:SENSITIVE = NO
                   hObject: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-COLUMN attribute. The chain of columns is linked by the NEXT-COLUMN attribute 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 COLUMN attribute, and widens it by six characters by setting the WIDTH-CHARS attribute. 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 same DO WHILE VALID-HANDLE block header as for the frame itself to walk through all the columns in the browse:

ELSE IF hObject:TYPE = "Browse" THEN
    DO:
        ASSIGN hObject:COLUMN = hObject:COLUMN - 3
               hObject:WIDTH-CHARS = hObject:WIDTH-CHARS + 6.
        hColumn = hObject:FIRST-COLUMN.
        DO WHILE VALID-HANDLE(hColumn):
            IF hColumn:DATA-TYPE = "DATE" THEN
               hColumn:WIDTH-CHARS = hColumn:WIDTH-CHARS + 4.
            hColumn = hColumn:NEXT-COLUMN.
        END.
    END.

Finally, you need to remember to move on to the next object in the frame before ending the original DO block:

    hObject = hObject:NEXT-SIBLING.
END.
END PROCEDURE.

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 original TYPE check wasn’t there:

IF hObject:DATA-TYPE = "INTEGER" THEN
   ASSIGN hObject:SENSITIVE = NO
          hObject:BGCOLOR = 8.
ELSE hObject:BGCOLOR = 10.

When Progress tries to retrieve the DATA-TYPE of 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-QUERY function to check whether something is a readable attribute before your code does so. CAN-QUERY takes two arguments, a valid object handle and a character expression that evaluates to an attribute name. This code example eliminates the error:

IF CAN-QUERY(hObject, "DATA-TYPE") AND
          hObject:DATA-TYPE = "INTEGER" THEN
       .
       .
       .

Likewise, you can check in advance whether something is a writable attribute using the CAN-SET function, which also takes an object handle and attribute name as arguments:

IF CAN-SET (hObject, "SENSITIVE") AND
   CAN-SET(hObject, "BGCOLOR") THEN
       ASSIGN hObject:SENSITIVE = NO
              hObject: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-ATTRS and LIST-SET-ATTRS functions. Each function takes a valid object handle as an argument:

IF hObject:TYPE = "FILL-IN" THEN
          MESSAGE LIST-QUERY-ATTRS(hObject).

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:
  1. Open the h-CustOrderWin7.w test window in the AppBuilder. Widen it somewhat to make room for some additional objects.
  2. Pick the Selection List object from the Palette:
  3. Drop the Selection List onto the window to the right of the Customer fields.
  4. Double-click on the Selection List to bring up its property sheet:
  5. Name the object OlineFields.
  6. Check on the Multiple-Selection toggle box. This option allows the user to select more than one entry from the object at run time.
  7. 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.
  8. Choose OK to save your changes to the property sheet.
  9. 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:
  1. Create a new internal procedure called initSelection.
  2. Begin the internal procedure with these definitions:
  3. /*---------------------------------------------------------------------
      Purpose:     Set the selection list to a list of all the fields in the
                   OrderLine table.
      Parameters:  <none>
    ---------------------------------------------------------------------*/
    DEFINE VARIABLE hBuffer AS HANDLE      NO-UNDO.
    DEFINE VARIABLE iField AS INTEGER     NO-UNDO.

    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.
  4. To do this, you first need to use the HANDLE attribute to get the buffer handle:
  5. hBuffer = BUFFER OrderLine:HANDLE.

    Notice that you need to include the BUFFER keyword 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 called NUM-FIELDS that conveniently tells you how many fields there are in the buffer.
  6. Use this code to start a block that walks through all those fields:
  7. DO iField = 1 TO hBuffer:NUM-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’s BUFFER-FIELD attribute. BUFFER-FIELD takes a single argument, which can be either the sequential field position within the buffer or the field name.
  8. In this procedure, since you just want to walk through all the fields to build up a list, use its position to identify it:
  9. hBuffer:BUFFER-FIELD(iField)

  10. Finally, you need to know that, like other objects, the BUFFER-FIELD has various attributes you can query or set. In this case you want the NAME attribute. The Progress 4GL (beginning in Progress Version 9.1D) lets you chain multiple colon-separated references together in a single expression, such as this:
  11. hBuffer:BUFFER-FIELD(iField):NAME

    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 position iField. Then retrieve the NAME attribute 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:

    BUFFER OrderLine:BUFFER-FIELD(iField):NAME

    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-FIRST and ADD-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-FIRST to add items to the beginning of the list and ADD-LAST to add them to the end. If the object uses the simple List-Items form, in which the actual object values are displayed, then use the item-list form 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 single label followed by the single value it represents.

These types of objects have a DELIMITER attribute 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 SORT attribute, which initially is false. If you set SORT to true, then displayed items are sorted by their label. In this case, there is no meaningful difference between using ADD-FIRST and ADD-LAST.

The methods return true if the operation succeeded, and false if for any reason it failed.

To use the ADD-LAST method to add each of the OrderLine field names to the end of the selection list:
  1. Enter this code to complete the initSelection procedure:
  2. /*---------------------------------------------------------------------
      Purpose:     Set the selection list to a list of all the fields in the
                   OrderLine table.
      Parameters:  <none>
    ---------------------------------------------------------------------*/
    DEFINE VARIABLE hBuffer AS HANDLE     NO-UNDO.
    DEFINE VARIABLE iFields AS INTEGER    NO-UNDO.
        hBuffer = BUFFER OrderLine:HANDLE.
        DO WITH FRAME CustQuery:
           DO iFields = 1 TO hBuffer:NUM-FIELDS:
             OLineFields:ADD-LAST(hBuffer:BUFFER-FIELD(iFields):NAME).
           END.
        END.
    END PROCEDURE.

  3. To display these values in the list when the window is viewed, add a RUN statement to the procedure’s main block:
  4. MAIN-BLOCK:
    DO ON ERROR   UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK
       ON END-KEY UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK:
      RUN enable_UI.
      RUN h-StartSuper.p("h-dynsuper.p").
      RUN changeFields.
      RUN initSelection.
      IF NOT THIS-PROCEDURE:PERSISTENT THEN
        WAIT-FOR CLOSE OF THIS-PROCEDURE.
    END.

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 FRAME attribute of the object. For a field-level object, there is also a PARENT attribute. 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. The PARENT attribute points to the field group, not the frame. Progress automatically puts the object into a field group when you assign its FRAME attribute 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 (ROW or Y) and horizontal (COLUMN or X) 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 FORMAT attribute. 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 a FORMAT of “X(20)” is not necessarily the same width in terms of screen real estate as a WIDTH-CHARS of 20. 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 from WIDTH-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-TABLE to 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-HANDLE attribute 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. The SIDE-LABEL-HANDLE attribute 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-VALUE attribute 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. The SCREEN-VALUE attribute 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’s SCREEN-VALUE and 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.w to 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 HANDLE variable array that has an EXTENT, 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 FIND on 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:
  1. Define the cFieldHandles variable in the Definitions section of h-CustOrderWin7.w, which scopes the variable definition to the whole procedure:
  2. /* Local Variable Definitions --- */
    DEFINE VARIABLE cFieldHandles AS CHARACTER  NO-UNDO.

  3. Write a block of code to execute whenever a new Order is selected. This is the VALUE-CHANGED event for the browse, which you’ve used in an earlier variation of this procedure.
  4. The code in the VALUE-CHANGED trigger 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 their SCREEN-VALUE to blank:

    /* ON VALUE-CHANGED OF OrderBrowse */
    DO:
      DEFINE VARIABLE iField AS INTEGER    NO-UNDO.
      DEFINE VARIABLE hField AS HANDLE     NO-UNDO.
      FIND FIRST OrderLine OF Order.
      DO iField = 2 TO NUM-ENTRIES(cFieldHandles) BY 2:
          hField = WIDGET-HANDLE(ENTRY(iField, cFieldHandles)).
          IF VALID-HANDLE(hField) THEN
              hField:SCREEN-VALUE = "".
      END.
    END.

  5. Define a LEAVE trigger for the OLineFields selection list. The trigger uses these variables:
  6.   DEFINE VARIABLE iField    AS INTEGER    NO-UNDO.
      DEFINE VARIABLE hField    AS HANDLE     NO-UNDO.
      DEFINE VARIABLE hLabel    AS HANDLE     NO-UNDO.
      DEFINE VARIABLE cFields   AS CHARACTER  NO-UNDO.
      DEFINE VARIABLE cField    AS CHARACTER  NO-UNDO.
      DEFINE VARIABLE hBufField AS HANDLE     NO-UNDO.
      DEFINE VARIABLE dRow      AS DECIMAL    NO-UNDO INIT 8.0.

  7. 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 cFieldsHandle variable:
  8.   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. The NO-ERROR qualifier on the DELETE OBJECT statement 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 the LEAVE trigger for the selection list, the field’s SCREEN-VALUE attribute 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.
  9. Save this value in a variable to keep the rest of the code from having to refer to the SCREEN-VALUE attribute over and over again:
  10. cFields = OLineFields:SCREEN-VALUE.

  11. Add a block to iterate through all the selections. You saw earlier how the BUFFER-FIELD attribute 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:
  12.   DO iField = 1 TO NUM-ENTRIES(cFields):
          ASSIGN cField = ENTRY(iField, cFields)
                 hBufField = BUFFER OrderLine:BUFFER-FIELD(cField).

  13. Create the text label for the fill-in. As you learned earlier, the label must be a separate text object:
  14.   CREATE TEXT hLabel
             ASSIGN FRAME = FRAME CustQuery:HANDLE
                    DATA-TYPE = "CHARACTER"
                    FORMAT = "X(" + STRING(LENGTH(hBufField:LABEL) + 1) + ")"
                    SCREEN-VALUE = hBufField:LABEL + ":"
                    HEIGHT-CHARS = 1
                    ROW = dRow
                    COLUMN = 85.0.

    The CREATE statement parents it to the frame, sets its data type, calculates a format and value for it using the LABEL attribute of the current buffer field, and positions it in the frame. The HEIGHT-CHARS of 1 makes the label text align properly with the value displayed next to it. The COLUMN positions it next to the browse, and the row is incremented each time through the loop to define a distinct position for each field.
  15. Create the fill-in object itself:
  16. CREATE FILL-IN hField
              ASSIGN DATA-TYPE = hBufField:DATA-TYPE
                     FORMAT = hBufField:FORMAT
                     FRAME = FRAME CustQuery:HANDLE
                     SIDE-LABEL-HANDLE = hLabel
                     COLUMN = 85.0 + LENGTH(hBufField:LABEL) + 4
                     ROW = dRow
                     SCREEN-VALUE = hBufField:BUFFER-VALUE
                     HIDDEN = NO.

    The data type, format, and value all come from the buffer field object handle. The SIDE-LABEL-HANDLE attribute connects this fill-in to its handle object. The COLUMN setting allows room for the label before displaying the field value. The SCREEN-VALUE assigns the value from the buffer field’s BUFFER-VALUE attribute. The HIDDEN attribute makes sure the field is viewed along with the frame that contains it.
  17. 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:
  18. ASSIGN dRow = dRow + 1.0
               cFieldHandles = cFieldHandles +
                  (IF cFieldHandles = "" THEN "" ELSE ",") +
                     STRING(hLabel) + "," + STRING(hField).
    END.   /* END DO iField */

  19. Make sure that the VALUE-CHANGED trigger 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:
  20. MAIN-BLOCK:
    DO ON ERROR   UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK
       ON END-KEY UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK:
      RUN enable_UI. RUN h-StartSuper.p("h-dynsuper.p").
      RUN changeFields.
      APPLY "VALUE-CHANGED" TO OrderBrowse.
      RUN initSelection.
      IF NOT THIS-PROCEDURE:PERSISTENT THEN
        WAIT-FOR CLOSE OF THIS-PROCEDURE.
    END.

  21. 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."
  22. 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:
  23. 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, and GET-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:
  1. Change the column setting in the CREATE TEXT statement for the label to this:
  2. /* 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 the FONT-TABLE. This gives the right starting position for the label object.
  3. Change the column assignment for the fill-in to be fixed at column 102:
  4. /* COLUMN = 85.0 + LENGTH(hBufField:LABEL) + 4 -- changed for colon-aligned */
    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 accurate WIDTH-CHARS attribute 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.
  5. Use the same FONT-TABLE method to calculate the WIDTH-CHARS of the text label object, adding this assignment to the CREATE TEXT statement:
  6. WIDTH-CHARS = FONT-TABLE:GET-TEXT-WIDTH-CHARS(hBufField:LABEL + ":")

  7. Run the window. With these changes, you see a different display where the labels of the dynamic fields are colon-aligned:
  8. Select another row in the browse, and the field values are set to blank by the VALUE-CHANGED trigger on the browse. What you see, however, is that because the displayed fields are not all CHARACTER fields, 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:
  9. 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:

Using multiple windows

As you’ve already learned, there is no DEFINE WINDOW statement 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 WINDOW statement, which has the same syntax as every other CREATE statement 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-WINDOW system handle, which holds the handle of the window used by default for parenting frames, dialog boxes, and message alert boxes. The CURRENT-WINDOW attribute of a procedure overrides CURRENT-WINDOW for 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 both CURRENT-WINDOW and the CURRENT-WINDOW procedure 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.

Table 19–1: Window attributes, methods, and events
Type
Name
Description
Attribute
TITLE
Specifies the window title.
HEIGHT-CHARS WIDTH-CHARS HEIGHT-PIXELS WIDTH-PIXELS
Specifies the standard height and width of the window.
MIN-HEIGHT-CHARS MIN-WIDTH-CHARS MIN-HEIGHT-PIXELS MIN-WIDTH-PIXELS
Specifies the minimum height and width of the window.
MAX-HEIGHT-CHARS MAX-WIDTH-CHARS MAX-HEIGHT-PIXELS MAX-WIDTH-PIXELS
Specifies the maximum height and width of the window.
VIRTUAL-HEIGHT-CHARS VIRTUAL-WIDTH-CHARS VIRTUAL-HEIGHT-PIXELS VIRTUAL-WIDTH-PIXELS
Specifies the maximum display area of the window.
MESSAGE-AREA MESSAGE-AREA-FONT
Defines a message area and its font at the bottom of the window, where messages that are not qualified with the VIEW-AS ALERT-BOX phrase are displayed.
STATUS-AREA STATUS-AREA-FONT
Defines a status area and its font at the bottom of the window.
SCROLL-BARS
Defines whether scroll bars appear when the window is resized.
MENUBAR
POPUP-MENU
Associates a menu or pop-up menu with a window.
PARENT
Establishes parent-child relationships between windows.
Method
LOAD-ICON()
Takes the name of an image file with the .ico extension and displays the image in the title bar of the window, in the task bar when the window is minimized, and when the user selects the window using ALT-TAB.
LOAD-SMALL-ICON()
Takes the name of an image file for an icon in the title bar or the task bar. The .ico file for either method can be an image of 16x16 or 32x32 pixels.
Event
WINDOW-MINIMIZED
Fires when a window is minimized.
WINDOW-MAXIMIZED
Fires when a window is maximized.
WINDOW-RESTORED
Fires when a window is restored after being minimized.
WINDOW-RESIZED
Fires when a keyboard or mouse event starts to change the window’s size. The window’s RESIZE attribute must be true for this to occur.
WINDOW-CLOSE
Fires when the user selects the standard icon used to close the window. In fact, Progress does not take any action automatically when the WINDOW-CLOSE event occurs—it does not even close the window! For this reason, you need to define a trigger for the WINDOW-CLOSE event, as described in the next section.

WINDOW-CLOSE event example

The simplest example of an action on the WINDOW-CLOSE event is this:

WAIT-FOR WINDOW-CLOSE OF CURRENT-WINDOW.

If the WAIT-FOR statement is the last executable statement in a nonpersistent procedure, the event satisfies the WAIT-FOR and the procedure terminates.

The AppBuilder window template uses indirection to direct the WINDOW-CLOSE event for the window to the procedure itself, using this standard ON WINDOW-CLOSE trigger:

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 CLOSE event on the procedure running disable_UI and 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 PARENT attribute 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:

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 BROWSE statement:

CREATE BROWSE browse-handle ASSIGN attribute = value . . .

In addition, you can specify some attribute values individually following the CREATE statement:

browse-handle:attribute = value

Here are some of the principal attributes you can set either in the CREATE BROWSE statement or in a separate assignment on the browse handle:

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 BROWSE statement, the attributes of the browse in the ASSIGN list are assigned in sequence. So, as this single statement is executed, if Progress encounters the VISIBLE = TRUE or HIDDEN = FALSE phrases (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 the ROW-MARKERS attribute to FALSE. If you put this assignment into the CREATE statement 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 BROWSE statement. 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 BROWSE statement 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-FROM method:

browse-handle:ADD-COLUMNS-FROM (buffer-name [ , except-list ] ).

If you specify a comma-separated except-list expression, the columns in the list are not included in the browse. All other columns from the buffer-name are included. If the browse’s query uses more than one buffer, you can invoke ADD-COLUMNS-FROM more 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-COLUMN method:

browse-handle:ADD-LIKE-COLUMN(field-name-expr | buffer-field-handle)

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-COLUMN method 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-COLUMN options:

Notes on dynamic browses and browse columns

Here are some facts to consider when you create and use dynamic browses and browse columns:

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:
  1. In the Definitions section, add these definitions:
  2. /* These variables are used just by the dynamic browse variation
        of this trigger. */
      DEFINE VARIABLE hBrowse   AS HANDLE      NO-UNDO.
      DEFINE VARIABLE hCalcCol  AS HANDLE      NO-UNDO.
      DEFINE QUERY qOrderLine   FOR OrderLine SCROLLING.

    The hBrowse variable will hold the handle of the dynamic browse you create, and hCalcCol will 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.
  3. Add this statement to the end of the initSelection internal procedure, which sets the list of OrderLine fields in the selection list:
  4. OlineFields:ADD-LAST("Price B4 Disc").

    The new entry represents a calculated column that holds the OrderLine price before the discount is applied.
  5. In the LEAVE trigger for the selection list called OlineFields, comment out all of the code.
  6. This code created dynamic fields to display an OrderLine. Now you’ll display a browse in the same place.
  7. 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:
  8. /* This block of code is the second version of the trigger, which creates
        a dynamic browse to show the selected fields. */
      IF VALID-HANDLE(hBrowse) THEN
        DELETE OBJECT hBrowse.

  9. Add new code to create a dynamic browse. This complex statement defines the browse and its attributes:
  10. CREATE BROWSE hBrowse
        ASSIGN FRAME = FRAME CustQuery:HANDLE
               WIDTH = 50
               DOWN  = 6
               ROW   = 8
               COL   = 82
               ROW-MARKERS = NO
               SENSITIVE   = TRUE
               SEPARATORS  = TRUE
               READ-ONLY   = FALSE
               NO-VALIDATE = YES
               VISIBLE     = TRUE
               QUERY = QUERY qOrderLine:HANDLE.

  11. If necessary, adjust the size and position of the browse according to the layout of your own window and Order browse.
  12. Add code that walks through the selected fields from the OlineFields selection list as the dynamic fill-in code does:
  13. cFields = OLineFields:SCREEN-VALUE.
      DO iField = 1 TO NUM-ENTRIES(cFields):
         cField = ENTRY(iField, cFields).

  14. If the selected field is the calculated field, you must add a calculated column to the browse to display it:
  15. IF cField = "Price B4 Disc" THEN
              hCalcCol = hBrowse:ADD-CALC-COLUMN
              ("Decimal",          /* Data type */
              ">,>>>,>>9.99",      /* Format */
              "0",                 /* Initial value */
              "Price B4 Disc").    /* column label */

  16. Otherwise, add a column like the selected field in the OrderLine table:
  17.   ELSE hBrowse:ADD-LIKE-COLUMN("OrderLine." + cField).
      END.     /* END DO iField… */

  18. Add this ROW-DISPLAY trigger, which executes each time a row is displayed in the browse:
  19. ON ROW-DISPLAY OF hBrowse
         PERSISTENT RUN calcPriceB4Disc.

    Because the code in the trigger block for LEAVE of 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 syntax PERSISTENT RUN calcPriceB4Disc to make Progress keep track of what to run when the ROW-DISPLAY event fires.
  20. Add this LEAVE trigger to open the OrderLine query from the Definitions section:
  21. OPEN QUERY qOrderLine FOR EACH OrderLine
         WHERE OrderLine.OrderNum = Order.OrderNum.

    You attached this query to the browse in the CREATE BROWSE statement.
  22. Add this code for the calcPriceB4Disc procedure:
  23. /*---------------------------------------------------------------------
      Procedure:  calcPriceB4Disc
      Purpose:    Calculates the value of the calculated column PriceB4Disc.
      Parameters: <none>
    ---------------------------------------------------------------------*/
    IF VALID-HANDLE(hCalcCol) THEN
             hCalcCol:SCREEN-VALUE = STRING(OrderLine.Price * OrderLine.Qty).
    END PROCEDURE.

    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.
  24. In the VALUE-CHANGED trigger 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:
  25. /* This is the second version of this trigger, for the dynamic browse
         example. */
      OPEN QUERY qOrderLine FOR EACH OrderLine WHERE OrderLine.OrderNum
          = Order.OrderNum.

    This code refreshes the OrderLine browse each time an Order is selected.
  26. Add this statement to each of the four navigation button triggers to apply the Order browse’s VALUE-CHANGED trigger:
  27. DO:
      GET NEXT CustQuery.
      IF AVAILABLE Customer THEN
       DISPLAY Customer.CustNum Customer.Name Customer.Address Customer.City
               Customer.State
          WITH FRAME CustQuery IN WINDOW CustWin.
      {&OPEN-BROWSERS-IN-QUERY-CustQuery}
      APPLY "VALUE-CHANGED" TO BROWSE OrderBrowse.
    END.

    The query opens when a new Customer is selected and its Orders first displayed.
  28. 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 its READ-ONLY attribute to false. Browse columns do not use the SENSITIVE attribute.

The browse FIRST-COLUMN attribute returns the handle to the first (leftmost) column in the browse. Each column’s NEXT-COLUMN attribute returns the handle of the column next to it. For example, you can add this block of code to the OlineFields LEAVE trigger:

/* You can use code such as this to navigate through
      the columns of a browse dynamically. */
  DEFINE VARIABLE hCol AS HANDLE     NO-UNDO.
  DEFINE VARIABLE cCols AS CHARACTER NO-UNDO.
  hCol = hBrowse:FIRST-COLUMN.
  DO WHILE VALID-HANDLE(hCol):
      cCols = cCols + CHR(10) + hCol:LABEL.  /* CHR(10) is a line feed. */
      hCol = hCol:NEXT-COLUMN.
  END.
  MESSAGE cCols.

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 CREATE statements rather than DEFINE statements to create a menu, submenu, or menu item. You parent these objects together in a hierarchy by setting their PARENT attribute. If you need to delete a dynamic menu before its widget pool is deleted, you use the DELETE OBJECT statement 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 MENU statement. 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:

CREATE MENU menu-handle [ ASSIGN attribute = value [ . . .] ] .

As with other dynamic objects, you can assign one or more attributes as part of the CREATE statement or in separate statements that use the menu handle. Typical menu attributes include:

To associate a menu with a window, set the MENUBAR attribute of the window handle to the menu handle.

Creating a submenu

To create a submenu, use the CREATE SUB-MENU statement:

CREATE SUB-MENU submenu-handle [ ASSIGN attribute = value [ . . .]]       [trigger-block ].

Typical attributes you can assign for a submenu include:

You can also define a trigger block (typically for a CHOOSE trigger) as part of the CREATE statement.

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:

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-CHILD attribute of a menu or submenu returns the handle of its first menu item or child submenu. The NEXT-SIBLING attribute 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 the LAST-CHILD attribute and following the PREV-SIBLING chain. 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 CHILD and SIBLING attributes 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.
  1. Add a new fill-in field to the window to display the selected SalesRep. Name it cSalesRep.
  2. 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:
  3. DEFINE VARIABLE hMenu     AS HANDLE     NO-UNDO.
    DEFINE VARIABLE hSubMenu  AS HANDLE     NO-UNDO.
    DEFINE VARIABLE hMenuItem AS HANDLE     NO-UNDO.

  4. Create the menu bar and its submenu:
  5. CREATE MENU hMenu.
    CREATE SUB-MENU hSubMenu
       ASSIGN PARENT = hMenu
              LABEL = "SalesReps".

  6. Create the list of SalesReps as menu items:
  7. FOR EACH SalesRep:
      CREATE MENU-ITEM hMenuItem
          ASSIGN PARENT = hSubMenu
                LABEL = SalesRep.SalesRep + " " + SalesRep.RepName
          TRIGGERS:
             ON CHOOSE PERSISTENT RUN filterCust IN THIS-PROCEDURE.
          END TRIGGERS.
    END.

    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 special PERSISTENT RUN statement to tell Progress to run a separate procedure when the event occurs. You’ll write this filterCust procedure in a moment.
  8. Add a couple of special menu items to the end of the list. Create a RULE to separate the SalesReps from the final menu item:
  9.   CREATE MENU-ITEM hMenuItem
          ASSIGN SUBTYPE = "RULE"
                 PARENT = hSubMenu.

  10. Add a menu item that closes the window and its procedure:
  11.   CREATE MENU-ITEM hMenuItem
          ASSIGN PARENT = hSubMenu
                 LABEL = "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.
  12. Parent the menu bar to the window:
  13.   CURRENT-WINDOW:MENUBAR = hMenu.

  14. Create the filterCust procedure for the SalesRep items:
  15. /*---------------------------------------------------------------------
      Procedure:  filterCust
      Purpose:    Filters customers by the selected SalesRep menu item.
    ---------------------------------------------------------------------*/
        OPEN QUERY CustQuery FOR EACH Customer
                   WHERE Customer.SalesRep = ENTRY (1, SELF:LABEL, " ").
        cSalesRep:SCREEN-VALUE IN FRAME CustQuery = SELF:LABEL.
        APPLY "CHOOSE" TO BtnFirst IN FRAME CustQuery.
    END PROCEDURE.

    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 handle SELF always 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.
  16. Define the internal procedure leaveProc to handle the Exit button:
  17. /*---------------------------------------------------------------------
      Procedure:   leaveProc
      Purpose:     Handles the Exit menu item
    ---------------------------------------------------------------------*/
        APPLY "CLOSE" TO THIS-PROCEDURE.
    END PROCEDURE.

    This code simply invokes the CLOSE event already defined as a standard part of the AppBuilder-generated code for the window.
  18. Add a statement to the procedure’s main block to run createMenu:
  19.       .
          .
          .
      RUN enable_UI.
      RUN createMenu.
      RUN h-StartSuper.p("h-dynsuper.p").
      RUN changeFields.
          .
          .
          .

  20. Run the procedure again. Now you can drop down a list of all the SalesReps:
  21. Select one. Now Customers are selected for that SalesRep only:

Summary

In this chapter, you learned how to use the CREATE statement 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
Table of ContentsPreviousNextIndex