OpenEdge Development: Progress 4GL Handbook


Table of ContentsPreviousNextIndex
Using the Browse Object

This chapter describes how to design and interact with static browse objects and the queries they represent. In "Creating and Using Dynamic Temp-tables and Browses,"you’ll learn how to create a dynamic browse at run time.

Like most other Progress graphical objects, the browse is also supported on character terminals, though with somewhat reduced functionality. Although this book concentrates on graphical interfaces, this chapter includes notes, along with a section at the end, that describe some of the differences between using the browse in a Windows GUI and in character mode.

A browse is a visual representation of a query. You already know from "Using Queries," that one of the principal uses of a query in Progress is to define a set of rows that you can navigate visually using a browse. The browse displays data from the query in rows and columns. A row represents the data of a single row in the query’s result list. A column represents the value of a particular field for each row. A row and column intersection, called a cell, represents the value of the field (column name) in that particular row. A user can scroll up and down the rows, and left and right through the columns.

You’ve already learned how to use the AppBuilder to generate a browse definition for you, and also how to define a browse on your own. In this chapter, you’ll learn about some of the many features that make the browse the most flexible and powerful visual object the 4GL supports. Even an entire chapter isn’t sufficient to cover every aspect of using the browse. For a description of all browse functionality, including attributes and methods not covered here, see the DEFINE BROWSE Statement reference entry and the Browse Widget reference entry, in OpenEdge Development: Progress 4GL Reference or the online help.

This chapter includes the following sections:

Defining a query for a browse

By default, a query against database tables uses SHARE-LOCK, so that other users can see the same records but not update them. A query is not scrollable by default. That is, the query does not have the ability to move backward as well as forward in the query. When you associate a browse with a query (by way of the DEFINE BROWSE statement), Progress automatically changes the query to use the NO-LOCK and SCROLLABLE options. You can also associate a query with a browse at run time. In this case, you should make the query SCROLLABLE when you define it. Since it’s important for you to begin to separate user interface procedures from database access procedures even in these first exercises, this chapter extends the temp-table-based OrderLine browse from "Defining and Using Temp-tables," so that you think in terms of browsing temp-tables in a client procedure that might be in a separate session from where the database is located. In this case, locking the temp-table records is not an issue since those records are always used only within your local session.

Once you define the query, define the browse, and open the query, the browse and the query become tightly bound. The currently selected row and the result list cursor are in sync and remain so. When the user manipulates the browse, the user is also performing the same manipulation on the cursor of the result list. Many programmatic actions performed on the result list or the browse automatically update the other, although this is not universally true. You could say that learning all the subtleties of the browse involves learning what occurs by default, what you have to manage, and what behaviors you can override.

As a rule, a query associated with a browse should be used exclusively by that browse. While you can use GET statements on the query to manipulate the query’s cursor, the result list, and the associated buffers, you run the risk of putting the browse out of sync with the query. If you do mix browse widgets and GET statements with the same query, you must use the REPOSITION statement to manually keep the browse in sync with the query.

Planning for the size of the result set

The number of records in your result list can have a serious impact on the performance of your browse, especially when you consider that you typically have to load those records into a temp-table and ship them across a network connection before they are seen in the browse. You might want to optimize your query definition to take advantage of features that work well with small and large sets of data, and design your user interface to encourage or require users to provide selection criteria to reduce the number of records that need to go into the data they’re browsing.

Defining a browse

This section describes some of the major functionality and style choices available when you define a browse, and the issues involved with those choices.

You can browse records by defining a browse for the query and opening the query. If you do not specifically enable the browse columns, the result is a read-only browse. Once the user finds and selects a record, your application can use the selected record, which the associated query puts in one or more associated buffers. This is the general syntax for a browse definition:

DEFINE BROWSE browse-name QUERY query-name
   [ SHARE-LOCK | EXCLUSIVE-LOCK | NO-LOCK ]
   DISPLAY { column-list | record [ EXCEPT field ... ] }
   [ browse-enable-phrase ] browse-options-phrase .

As with all objects you define, you first give the browse a name. Next, you associate it with a query that you have previously defined. You can later associate the browse with a different query at run time by setting its QUERY attribute, but the query it’s defined for is the source of initial information about the buffers and fields you use to define columns in the browse.

Once again, NO-LOCK is the only reasonable locking option even for a browse on a database table and the only possible one for a browse on a temp-table, since temp-table don’t support record locking. It’s also the default, so you can leave out the NO-LOCK keyword altogether.

The DISPLAY phrase tells Progress which fields from the buffers in the query to display as columns in the browse. You can specify either an explicit space-delimited column-list or a list of one or more record buffers from the query. In the latter case, you can use the EXCEPT phrase to remove one or more fields from each record when it is displayed. The default is not to display any fields at all, so you must always include a DISPLAY phrase in your browse definition unless you want to define all the columns at run time, as you will learn to do in "Creating and Using Dynamic Temp-tables and Browses."

If you just specify the DISPLAY list, the browse is read-only. None of its cells are enabled for input. The browse-enable-phrase lets you enable one or more cells for input:

ENABLE { column . . . | ALL [ EXCEPT column . . .] }

Each column in the ENABLE phrase must be a column in the DISPLAY list. If you want to enable all or almost all the columns, you can use the ALL keyword optionally followed by an EXCEPT list.

There are many options you can specify in the browse-options-phrase to customize the appearance and behavior of your browse. The phrase begins with the WITH keyword. Here are some of the more important options:

Changing the test window for the OrderLine browse

You can try out some of the browse options using the browse you defined for a temp-table based on the OrderLine table in "Defining and Using Temp-tables."

To change the test window:
  1. Open the CustBrowseWin4.w procedure and save it as CustBrowseWin5.w.
  2. Remove the Save Position and Restore Position buttons, and the New State and Number of Matches fill-ins.
  3. Remove these lines from the Main Block:
  4. ASSIGN cState = Customer.State
           iMatches = NUM-RESULTS("CustQuery").
      DISPLAY cState iMatches WITH FRAME CustQuery.

    The default position for the OrderLine browse was to the right of the Order browse.
  5. To keep the window from being so wide, change its position to be explicitly below the OrderBrowse in the VALUE-CHANGED trigger block for the OrderBrowse:
  6. DO:
      RUN h-fetchOlines.p (INPUT Order.OrderNum, OUTPUT TABLE ttOline).
      OPEN QUERY OlineQuery FOR EACH ttOline.
      DISPLAY OlineBrowse AT ROW 14 COLUMN 5 WITH FRAME CustQuery.
      ENABLE OlineBrowse WITH FRAME CustQuery.
    END.

  7. Resize the window so that it is somewhat narrower but taller than it was before. Experiment with it so that it is large enough to display the OrderLine browse when you run it:
  8. Now that you better understand the complete syntax for defining a browse, take another look at the browse definition you created for Order Lines in the Definitions section of your window procedure:
  9. DEFINE BROWSE OlineBrowse
      QUERY OlineQuery NO-LOCK DISPLAY
          ttOline.Ordernum
          ttOline.LineNum LABEL "Line"
          ttOline.ItemNum LABEL "Item" FORMAT "ZZZ9"
          ttOline.ItemName FORMAT "x(20)"
          ttOline.TotalWeight
          ttOline.Price FORMAT "ZZ,ZZ9.99"
          ttOline.Qty
          ttOline.Discount
          ttOline.ExtendedPrice LABEL "Ext.Price" FORMAT "ZZZ,ZZ9.99"
        WITH NO-ROW-MARKERS SEPARATORS 7 DOWN ROW-HEIGHT-CHARS .57.

    The definition has the necessary browse name and query name, and the optional NO-LOCK keyword, which could have been left off because it’s the default. This is followed by the list of temp-table fields to display as browse columns. A browse column definition has a long list of available display options, including the LABEL and FORMAT that you specified for some of them. Because there is no ENABLE phrase, the browse is read-only.
    The browse options phrase specifies:
    • NO-ROW-MARKERS — Provides the default when there are no enabled columns.
    • SEPARATORS — Provides the lines between columns and rows.
    • 7 DOWN — Provides the height of the browse in terms of rows displayed (because there is no WIDTH phrase all columns are displayed in full).
    • ROW-HEIGHT-CHARS — Specifies the precise height of each row. The value 57 is the same value Progress would provide for the default font if you left this option out.
    • You can experiment with changing some of these options to see how they affect the appearance of the browse.

Enabling columns in the browse

In this section, you experiment with browse columns.

To enable some of the columns in the OrderLine browse:
  1. Add this ENABLE phrase to the browse definition:
  2. DEFINE BROWSE OlineBrowse
      QUERY OlineQuery NO-LOCK DISPLAY
            ttOline.Ordernum
          .
          .
          .
        ENABLE ttOline.Price ttOline.Qty ttOline.Discount
        WITH SEPARATORS 7 DOWN ROW-HEIGHT-CHARS .57.

  3. You can also remove the NO-ROW-MARKERS keyword to see the effect of row markers in your updateable browse:

Now that the browse is updateable there is a row marker at the beginning of each row, if you don’t specify the NO-ROW-MARKERS keyword, to indicate which row is selected. This is useful because the way a row is highlighted is very different depending on whether the user happens to click in an enabled or read-only column:

The user should always click on the row marker to select a row to avoid inadvertently selecting an enabled column when this is not the user’s intent.

If the user selects the Price column for an OrderLine, or the Qty or Discount, which are all enabled, the browse cell for that column behaves in much the same manner as a fill-in field. Tabbing or clicking moves the user from one updateable cell to the next. If the user changes the value, then that value is assigned when the user leaves the row. But assigned where? Because this is a browse of a temp-table, the value is assigned only back to the row in the temp-table. If the user browses an actual database table, then the update goes back to the database unless you included the NO-ASSIGN keyword in your browse definition. You’ll learn more about updating records in "Updating Your Database and Writing Triggers," so you won’t do any more with enabled columns for now.

Note: In character interfaces, a key sequence (ESC-R) toggles between two modes, to select rows or edit cells. By default, the selected row is the highlighted row that you change with CURSOR-DOWN and CURSOR-UP. Character row markers appear as an asterisk (*). Also, the EDITOR-TAB and BACK-TAB key functions (usually CTRL-G and CTRL-U) tab from cell to cell.

One very important fact to remember is that you cannot enable, or turn on, a read-only browse after you have defined it. If you expect to need any of the capabilities of the updateable browser, enable the appropriate fields in the definition and then use the READ-ONLY logical attribute to temporarily turn them off at run time.

Defining a single- or multiple-select browse

A browse can support single and multiple selections of rows. By default, a browse is single-select. The MULTIPLE option sets up a multiple-select browse. Multiple-selection lets you select any combination of rows from the browse, which you can then access using the appropriate browse attributes and methods.

In a multiple-select browse, the user first clicks in a row to select it. To select additional rows without deselecting the first, the user holds down the CTRL key and clicks in each row. Alternatively, the user can click in one row, hold down the mouse key, and drag the cursor through multiple rows to select them all. Clicking on a selected row while pressing CTRL deselects it. Clicking on a row without pressing CTRL selects it and deselects all other rows. In a multiple-select updateable browse, the user must click on one row marker, hold down the mouse button and drag down through the row markers to select multiple contiguous rows.

Note that an updateable browse can only have one selected row when it is in edit mode. So, if the user selects five rows and then an updateable cell, the four rows that do not contain the active cell are deselected.

Note: For a multiple-select browse in character interfaces, selection of each row is switched by the space bar. The close angle bracket (>) indicates each selected row. The same technique applies to both read-only and updateable multiple-select browses.

There are several attributes and methods you can use in conjunction with selected rows in a single-selection or multiple-selection browse. You can learn about these in OpenEdge Development: Progress 4GL Reference.

Next, you modify your test procedure to use the NUM-SELECTED-ROWS attribute and the FETCH-SELECTED-ROW method to gather information about a set of selected rows in a multiple-selection browse

To modify your test procedure:
  1. Add a button named BtnTotal with the label Total to your window.
  2. Add a fill-in called dTotal to the window with no label.
  3. In its property sheet, change its data type to Decimal.
  4. Define this trigger block for the CHOOSE event for BtnTotal:
  5. DO:
        DEFINE VARIABLE iRow    AS INTEGER    NO-UNDO.
    DEFINE VARIABLE hBrowse AS HANDLE     NO-UNDO.
        hBrowse = BROWSE OlineBrowse:HANDLE.
        DO iRow = 1 TO hBrowse:NUM-SELECTED-ROWS:
          hBrowse:FETCH-SELECTED-ROW(iRow).
          dTotal = dTotal + ttOline.ExtendedPrice.
        END.
        DISPLAY dtotal WITH FRAME CustQuery.
    END.

The NUM-SELECTED-ROWS attribute returns the number of rows the user has selected in the browse. You can then use the FETCH-SELECTED-ROW method, which takes the sequence within the list of selected rows as an argument, to position the query to each selected row in turn, and add up the ExtendedPrice for each of those rows.

One question you might have is why is it necessary to define a handle variable to hold the handle of the browse, rather than just referring to it in each statement as BROWSE OlineBrowse? The browse is not part of the initial frame definition for the window’s frame. It is displayed after the window is initialized. If you refer to BROWSE OlineBrowse in the statements with the NUM-SELECTED-ROWS attribute or the FETCH-SELECTED-ROW method, Progress insists that you qualify the reference with a frame name so that it can identify the browse. Normally, you would qualify each reference to make it explicit or else simply put the entire set of references inside a DO block that defines the context:

DO WITH FRAME CustQuery:
DO iRow = 1 TO BROWSE OlineBrowse:NUM-SELECTED-ROWS:
      .
      .
      .

But because the browse is not part of the frame at compile time, Progress gives you an error message when you compile the procedure (or when you run it, which compiles it first) to the effect that it can’t find OlineBrowse in frame CustQuery. To get around this, you need to fool Progress into accepting the reference at compile time. To do this, you first set the handle hBrowse variable to the HANDLE attribute of the browse, and then use the handle in place of the name. Progress accepts this because handle references are always difficult or impossible to check at compile time, so Progress has to assume that the reference will be valid when the code is executed, which it is.

To see a total displayed, Run the window, select some OrderLines, and then choose the Total button:

Browse selection and query interaction

When a browse is initialized, the buffer for its query contains the first record in the query, since the user has not yet selected a record. To keep the browse in sync with the query, the buffer contains the first record in the browse viewport while there is no selected record. Whenever the user selects a row, that row becomes the current row in the query’s buffer.

In a multiple-select browse, if you deselect the current row, the query is repositioned to the previously selected row. If no rows are selected, the query is repositioned to the first record in the viewport.

Table 13–1 summarizes how actions on the browse affect the associated query.

Table 13–1: Browse and query interaction
Browse action
Effect on query
Vertical keyboard navigation.
Moves the result list cursor for single-select browse widgets.
Select row.
Puts records for that row into the record buffers.
Deselect current row.
If other records are selected in the browse, put records for the most recently selected of those rows into the record buffers. Otherwise, repositions to the first row in the viewport.
Deselect noncurrent row.
None.

Using the GET statement (such as GET NEXT) to navigate within the result list of the query has no effect on the browse. However, the REPOSITION statement does update the current position of the browse. If you use GET statements for a query on which a browse is defined, you should use the REPOSITION statement to keep the browse synchronized with the query. Also, when you open or reopen the query with the OPEN QUERY statement, the browse is automatically refreshed and positioned to the first record.

Using calculated columns

A browse can show calculated results involving other browse columns and other available data values as separate columns in the browse. You accomplish this by adding the expression for each calculated column to the DEFINE BROWSE statement. All valid browse format phrase options are legal extensions to the expression (for example, a LABEL option). When the browse is opened, the calculated columns are displayed for each row.

For an updateable browse, however, you need to refresh the calculation if one of the values in the expression changes. The browse displays the appropriate calculations only when the query refreshes the data. It is more appropriate to refresh the calculated data programmatically when the user finishes editing the affected row. To accomplish this, use a base field as a placeholder for the expression. You then can reference the calculation and refresh the result as needed.

To see an example, you can add a column to the Order browse in your test procedure to display the number of days between the OrderDate and the PromiseDate

To add a column to the Order browse that displays the number of days between the OrderDate and the PromiseDate:
  1. Open h-CustOrderWin4.w and save it as h-CustOrderWin5.w.
  2. Define a variable in the Definitions section to act as the placeholder for the calculated field. Call this Integer variable iPromiseDays:
  3. DEFINE VARIABLE iPromiseDays AS INTEGER     NO-UNDO.

  4. Define the calculation as a column in the browse that is effectively displayed at the placeholder variable. You do this by including the expression for the calculation in the DISPLAY list followed by the at-sign (@) followed by the name of the placeholder variable that is used to store the value and represent its display format. Give the calculated field a COLUMN-LABEL of “Promise!Days” to see the effect of creating a stacked label:
  5. DEFINE BROWSE OrderBrowse
      QUERY OrderBrowse NO-LOCK DISPLAY
          Order.Ordernum FORMAT "zzzzzzzzz9":U WIDTH 10.2
          Order.OrderDate FORMAT "99/99/99":U
          Order.PromiseDate FORMAT "99/99/99":U
          Order.ShipDate FORMAT "99/99/9999":U
          Order.PromiseDate - Order.OrderDate @ iPromiseDays
              COLUMN-LABEL "Promise!Days"
          Order.PO FORMAT "x(20)":U WIDTH 17.2
        WITH NO-ROW-MARKERS SEPARATORS SIZE 65 BY 6.19 ROW-HEIGHT-CHARS .57 EXPANDABLE.

  6. Run h-CustOrderWin5.w. You see the calculation along with the other columns (it just happens that the number of days is always 5 in the test data in the Sports2000 database):
  7. Note: When there are calculated fields in the DISPLAY phrase, you cannot use the ENABLE ALL option unless you use the EXCEPT option to exclude the calculated field, since calculated fields cannot be enabled.

Sizing a browse and browse columns

Normally, the browse calculates its size based on the column width of each field and the number of display rows requested with the DOWN option. If the resultant browse is too big for the frame, Progress displays an error message at compile time.

Use the DOWN option to specify the number of rows to display and the WIDTH option to specify the width of the browse. If the total column width is greater than the specified browse width, the browse displays a horizontal scrollbar to access the columns that don’t display initially and you avoid the compilation error.

Note: In character interfaces, there is no horizontal scrollbar for a wide browse but the user can scroll the browse one column at a time, using CURSOR-LEFT and CURSOR-RIGHT.

The horizontal scrollbar can work in two different ways. By default, the browse scrolls whole columns in and out of the viewport. To change this behavior to pixel scrolling, specify the NO-COLUMN-SCROLLING option of the DEFINE BROWSE statement or set the COLUMN-SCROLLING attribute to No at run time.

You can also use the SIZE phrase to set an absolute outer size of the browse in pixel or character units, and Progress determines how many rows and columns to display. The AppBuilder always uses the SIZE phrase to specify the size of a browse that you lay out visually.

The browse also supports a WIDTH attribute for each column. Use this attribute to set the width of individual columns. When a column WIDTH sets the width of an updateable column smaller than the size specified by the FORMAT string, the browse cell scrolls to accommodate extra input up to the size specified by the FORMAT. You can take advantage of this behavior to allow the display of longer character fields. For example, you can use it where the WIDTH limits the display space the column takes up, but the user can scroll left and right through the entire text of the string up to the size of the FORMAT, using the left and right arrow keys.

The width of each column label is also a determining factor in the width of the displayed column. If the column label is wider than the data for that column, then the column is made wide enough to accommodate the label. To reduce this width, you can specify a shorter label in the browse definition (as you did in the OrderLine browse, for example). As you’ve seen, you can also create stacked labels by inserting an exclamation mark (!) when the label should be divided between two lines. For more details, see the "Using stacked labels" .

Programming with the browse

The following sections discuss important programming techniques involving the browse, including browse events, managing rows in the browse, and manipulating the browse as a visual object.

Browse events

This section covers the essential events supported by the browse, including:

Basic events

The basic browse events include:

Typically, you use the VALUE-CHANGED and DEFAULT-ACTION events to link your browse to other parts of your application. Your current test window contains an example of the use of VALUE-CHANGED. Whenever the current row in the Order browse is changed, the VALUE-CHANGED event occurs and the trigger executes to retrieve its OrderLines and to display them in the second browse.

The DEFAULT-ACTION event occurs when the user explicitly selects a row not just by positioning to it, but by either double-clicking it or pressing ENTER.

To define a DEFAULT-ACTION trigger for the Order browse:
  1. In the Section Editor, select the Triggers section and the OrderBrowse object.
  2. Choose the New button and select DEFAULT-ACTION from the list of browse events.
  3. Enter this MESSAGE statement for the event:
  4. DO:
      MESSAGE "This Order's SalesRep is " Order.SalesRep SKIP
              "and the terms are " Order.Terms VIEW-AS ALERT-BOX.
    END.

  5. Run the window procedure and double-click on one of the Order rows to see the message:

The SCROLL-NOTIFY event is important if you plan to overlay other botches on browse cells, because it notifies you whenever the position of the rows of data within the browse viewport is changed by a scrolling action. SCROLL-NOTIFY gives you the ability to move the overlay widget to keep pace with the user’s scrolling. See the "Overlaying objects on browse cells" for more information on this technique.

You could use the OFF-END or OFF-HOME event to retrieve more data to all the rows in the browse’s query, for example, when there are too many rows to retrieve at once. Note that if the user scrolls by clicking and holding the up or down arrows at the bottom and top of the vertical scrollbar, the OFF-HOME or OFF-END event does not occur until the user releases the mouse key.

Row events

The browse supports three row-specific events:

You can find examples of how ROW-ENTRY and ROW-LEAVE work in the sections on updating and creating browse rows later in the "Manipulating rows in the browse" .

The ROW-DISPLAY event lets you change the color and font attributes of a row or individual cells or to reference field values in the row. The event also lets you change the format of a browse-cell by changing the value of its FORMAT attribute. You can also use the ROW-DISPLAY event to change the SCREEN-VALUE of one or more cells within the row.

For example, you can create a ROW-DISPLAY event for the OrderBrowse in your test window with this code:

DO:
  Order.PO:SCREEN-VALUE IN BROWSE orderBrowse =
      "PO" + STRING (Order.OrderNum).
  IF Order.ShipDate = ? THEN
      ShipDate:BGCOLOR IN BROWSE OrderBrowse = 12.
END.

This changes the displayed value for the PO column and also checks the value of the ShipDate and changes its background color to signal that the ShipDate has not been entered, as shown in Figure 13–1.

Figure 13–1: Result of ROW-DISPLAY event example
Notes: If you want to use this technique to change the colors of an array extent field, you must do so by directly referencing each member.

In character interfaces, the DCOLOR attribute specifies the color of an individual cell.
Column events

There are LEAVE and ENTRY events that reference the browse object itself. For example:

On ENTRY OF OrderBrowse
DO:
   .
   .
   .
END.

You can also write LEAVE and ENTRY triggers for individual columns. These triggers can refer to fields in the current row and can set and get attributes of the current cell by using either the name of the column in the browse or the SELF keyword. For example, you can enable the PO column in the OrderBrowse and then define this LEAVE trigger for it:

DO:
  IF Order.PO:SCREEN-VALUE IN BROWSE OrderBrowse BEGINS "X" then
     Order.PO:BGCOLOR IN BROWSE OrderBrowse = 12. /* RED */
END.

Or more simply, you can use the SELF keyword to refer to the cell:

DO:
  IF SELF:SCREEN-VALUE BEGINS "X" then
     SELF:BGCOLOR = 12. /* RED */
END.

Either way, you see the result shown in Figure 13–2 when you run the procedure.

Figure 13–2: Result of column event example
Searching columns

There are also two events that let you interact with search mode. START-SEARCH occurs when the user chooses a column label. END-SEARCH occurs when the user enters edit mode on a cell or selects a row. You can apply both of these events to columns to force the start and end of search mode.

When a read-only browse has focus, typing any printable key (an alphanumeric character) forces Progress to reposition the browse to the first row where the first character of the first browse column matches the typed character. Thus, the read-only browse allows searching, but based only on the data in the first column. If the browse finds no match, it wraps to the top of the column and continues searching.

In an updateable browse, the user can select the column that is to be the basis of the character search. First, the user clicks a column label. The column label depresses. When the user types any printable key, Progress searches for the first row in that column that matches and focuses that row. Column searches also wrap to the top if necessary.

Note: A column search on a browse associated with a query with the INDEXED-REPOSITION option does not wrap to the top of the column if it cannot find a record to satisfy the search. This behavior is a side effect of the reposition optimization. To work around this, you can apply HOME to the query before starting the search.

There are two ways to extend this basic behavior.:

As noted, the column-searching feature is exclusive to the updateable browse. The read-only browse can do one-character deep searches on the initial column of the browse. You can work around this by defining your browse with one field enabled for input and the NO-ROW-MARKERS option specified. After the definition, disable the enabled column by setting the column’s READ-ONLY attribute to false. This provides an updateable browse that looks like a read-only browse. The updateable browse retains the ability for the user to select individual columns and perform searches on them.

Note: The column searching capability is not available in character interfaces.

Manipulating rows in the browse

This section describes different ways you can change browse rows.

Refreshing browse rows

If you use a read-only browse and a user updates a record in some way outside the browse, you need a way to refresh the currently focused browse row with the new data. Use the following statement form at the end of the update code to refresh the browse:

DISPLAY column-name ... WITH BROWSE browse-name.

Repositioning focus

The browse and its query must remain in sync. At times, you might have to do some behind-the-scenes manipulation of the result set to keep them in sync. Generally speaking, whenever the user repositions the browse by selecting a new row, its query repositions automatically to that same row. Likewise, if the query is programmatically repositioned to a different row, the browse automatically coordinates with it and the new current row within the query becomes the currently selected row in the browse. However, if you simply FIND a row using the query’s buffer, this does not reposition either the query or its browse. This is why you might need to resync the query in your code in order to properly refresh the browse. To do this, you can use the REPOSITION statement. The REPOSITION statement moves the database cursor to the specified position and adjusts the browse viewport to display the new row.

To avoid display flashing when doing programmatic repositions, you can set the REFRESHABLE browse attribute to FALSE, do the REPOSITION, and then set REFRESHABLE to TRUE. This suspends any redisplay of the browse until after the operation is complete.

In addition, the SET-REPOSITIONED-ROW( ) method gives you control over the position in the viewport where the browse displays the repositioned row. The method takes two arguments:

  1. Its first Integer argument tells Progress which row (that is within the browse viewport) to position to. For example, if your browse displays seven rows at a time, you could use the SET-REPOSITIONED-ROW method to show a newly positioned row in the middle of the viewport by using an argument value of 4.
  2. The second Character argument to the method can be ALWAYS or CONDITIONAL. If you specify ALWAYS, the browse is always adjusted to show the repositioned row in the specified position. If you specify CONDITIONAL, then the browse adjusts only if the repositioned row is not already in the viewport.

Note that normally you set SET-REPOSITIONED-ROW( ) once for the session for a browse to establish its behavior wherever it is used. You can also use the GET-REPOSITIONED-ROW() method to return as an Integer the current target viewport row for repositions.

To reposition the query and the browse along with it:
  1. Define a new fill-in called iOrder of INTEGER data type.
  2. Define this LEAVE trigger for the fill-in field:
  3. DO:
      BROWSE OrderBrowse:SET-REPOSITIONED-ROW(3, "CONDITIONAL").
      ASSIGN iOrder.
      FIND Order WHERE order.orderNum = iOrder NO-ERROR.
      IF AVAILABLE (Order) THEN
          REPOSITION OrderBrowse TO ROWID ROWID(Order) NO-ERROR.
    END.

When you enter a value into the field and press TAB, the query and the browse are both repositioned to that row. If you enter a row that Progress can’t find or that isn’t an Order for the current Customer, Progress suppresses these errors and nothing changes in the browse. The SET-REPOSITIONED-ROW method makes the new row the third row in the viewport unless it’s already displayed.

For a multiple-select browse, the concepts of focus and selection separate. Selection indicates that the user has selected a record. The user can select many records. The last selected record is the one in the record buffer. Focus indicates that the record has input focus. There can be only one focused row, and it might or might not be a selected row. In a single-select browse, selection and focus are one and the same.

Updating browse rows

By default, Progress handles the process of updating data in the records managed by an updateable browse’s query. Because you should normally create applications that do not have a client-side dependency on a direct database connection, it is not advisable for you to define browses directly against database tables or to update database tables directly through a browse. Therefore, the default update behavior is described here to make you aware of what Progress does when you are browsing a database table, not to recommend that you take advantage of it. Some of these steps might not be completely clear to you until you read "Managing Transactions," but it gives you an overview of the steps.

Assuming that the browse starts with the record in NO-LOCK state, Progress follows these steps:

You also have the option to disable this default behavior and programmatically commit the changes by way of a trigger on the ROW-LEAVE event. To do this, you must supply the NO-ASSIGN option in the DEFINE BROWSE statement.

If you define your browses against temp-tables, as you are doing in the OlineBrowse of the test procedure, then there are no record locks and no possibility of contention with other users for the records in the temp-table. Therefore, you can let Progress update the temp-table by defining the browse without the NO-ASSIGN keyword and then take care of updating the database from the temp-table yourself. You learn how to do this in "Creating and Using Dynamic Temp-tables and Browses."

Creating browse rows

In an updateable browser, you might want to allow the user to add new records to the underlying temp-table or database table. Programmatically, this requires three separate steps:

  1. Create a blank line in the browse viewport with the INSERT-ROW() method and populate it with new data. INSERT-ROW takes a single optional argument, which is the string “BEFORE” or “AFTER”. This argument tells Progress whether to insert the new row before or after the currently selected row in the browse. The default is “BEFORE”. You can use the INSERT-ROW() browse method in an empty browse. It places a new row at the top of the viewport.
  2. Use the CREATE statement and ASSIGN statement to update the database or the underlying temp-table.
  3. Add a reference to the result list with the CREATE-RESULT-LIST-ENTRY( ) method. This is the list of record identifiers that Progress uses to keep track of the set of rows in the query. This step is only necessary if you do not plan to reopen the query after the update, because reopening the query completely refreshes the list. However, this method makes reopening the query unnecessary for most applications.

All three steps are required to create the record and keep the database, query, and browse in sync. Also, there are several possible side effects to allowing the user to add a record through an updateable browse. They include placing new records out of order and adding records that do not match the query. To eliminate these side effects, you can reopen the query after each new record is added.

To add a button to the test window that lets you add new OrderLines to the temp-table through the OlineBrowse:
  1. In the Definitions section of the h-CustOrderWin5.w procedure, beneath the browse define for OlineBrowse, define a handle variable called hBrowse. You will use this variable to hold the handle of the OrderLine browse because it is needed in several places.
  2. Define a new buffer for the OrderLine temp-table called ttOline2. You’ll use this buffer to hold onto the values in the current temp-table record as you create a new record, and to copy some of the values from the old record to the new one.
  3. Assign the OlineBrowse handle to the new hBrowse variable:
  4. DEFINE VARIABLE hBrowse AS HANDLE     NO-UNDO.
    DEFINE BUFFER ttOline2 FOR ttOline.
    hBrowse = BROWSE OlineBrowse:HANDLE.

  5. Drop a new button onto the window where it will not be overwritten by the OrderLine browse when it is displayed.
  6. Name the new button btnNew and give it a label of New OrderLine.
  7. Define a CHOOSE trigger for the button:
  8. DO:
        IF hBrowse:NUM-SELECTED-ROWS = 0 THEN
        DO:
           APPLY "END" TO hBrowse.
           hBrowse:SELECT-FOCUSED-ROW().
        END.
        hBrowse:INSERT-ROW("AFTER").
    END.

    This trigger code uses the handle of the OlineBrowse that you just assigned up in the Definitions section and inserts a new row in the browse after the currently selected row. First, it checks to make sure that there is a selected row using the browse’s NUM-SELECTED-ROWS attribute. If the value of this attribute is zero, then the user has not selected any row. In this case, the trigger positions to the end of the browse by applying the END event to it and then selects that row using the SELECT-FOCUSED-ROW method.
  9. Back in the procedure’s Definitions section, add this ROW-LEAVE trigger block for the OlineBrowse following the statement that assigns its handle to hBrowse:
  10. ON "ROW-LEAVE" OF BROWSE OlineBrowse
    DO:
        IF hBrowse:NEW-ROW THEN
        DO:
            FIND LAST ttOline2.
            CREATE ttOline.
            BUFFER-COPY ttOline2 TO ttOline
                ASSIGN ttOline.LineNum = ttOline2.LineNum + 1.
            ASSIGN INPUT BROWSE OlineBrowse ttOline.Qty
                    ttOline.Price ttOline.Discount.
            DISPLAY ttOline.OrderNum ttOline.LineNum
                    ttOline.ItemNum ttOline.ItemName
                    WITH BROWSE OlineBrowse.
            hBrowse:CREATE-RESULT-LIST-ENTRY().
        END.
    END.

    This code first checks the NEW-ROW attribute of the browse to see if the row the user left is one newly created by the New OrderLine button.
    Because you’re not prepared to do a full-fledged database update from the browse yet, the example simply creates a new row in the temp-table. To simplify the example, you start by copying the fields from the previous record into the new one. To do this, you FIND the LAST record in the temp-table using the second buffer ttOline2. This gives you the highest LineNum value so that you can increment it for the new record. Note that this means that other initial field values are also assigned from the last record regardless of where in the viewport the user inserted the new record.
    You create a new temp-table record and BUFFER-COPY the last record into the new one, incrementing the LineNum field to give it a distinct value. This is important because the temp-table has the same unique index on the OrderNum and LineNum fields as the underlying OrderLine table in the database, so changing the LineNum has to happen in the BUFFER-COPY statement to avoid a unique index violation.
    Next, the code assigns values to those columns that are enabled for input. The INPUT keyword on the ASSIGN statement tells Progress to take the values from the screen buffer for each column. Then the new values are displayed in the browse.
    Finally, you create a result list entry for the new record. If you didn’t create a result list entry the row would be lost if you were to scroll it out of the browse’s viewport. Reopening the query would also rebuild the result list, and also assure that all rows are in the proper order, so you can use either technique for getting the browse and the query back in sync.
  11. Run the window. Now you can add a record to the OrderLine browse:
  12. Once again, you’ll learn how to get new records into the database in "Updating Your Database and Writing Triggers."
Deleting browse rows

Deleting a record by way of a browse is a two-step process. First, you need to delete the record from the underlying temp-table or database table, and then you need to remove the record from the browse itself, along with the query’s result list. If you are browsing a database table directly and the user indicates a deletion, you should again get the records by EXCLUSIVE-LOCK NO-WAIT and then use the DELETE statement to remove the records from the database. In the case of a temp-table, you can simply use the DELETE statement to remove the records.

Next, you use the DELETE-SELECTED-ROWS( ) method to delete one or more selected records from both the browse widget and the associated query result list.

To add a Delete button to the test window and use it to remove rows from the OlineBrowse and its temp-table:
  1. Drop another button onto the window called btnDelete and give it a label of Delete OrderLines.
  2. Write this CHOOSE trigger for the new button:
  3. DO:
      DEFINE VARIABLE iRow AS INTEGER NO-UNDO.
      DO iRow = 1 TO hBrowse:NUM-SELECTED-ROWS:
         hBrowse:FETCH-SELECTED-ROW(iRow).
         DELETE ttOline.
      END.
      hBrowse:DELETE-SELECTED-ROWS().
    END.

Because the OlineBrowse is a multiple-selection browse, you can use it to delete one or more rows at once. This code walks through the set of selected rows and retrieves each one in turn using the FETCH-SELECTED-ROW method. This method repositions the temp-table query to that row, so that it can be deleted. The code then uses the DELETE-SELECTED-ROWS method to delete all the rows from the browse itself, along with the query’s result list entries for them.

If you run the test window, you can now delete one or more rows from the list of OrderLines for an Order. Remember that these records are deleted only from the temp-table. You would have to make a call to a procedure connected to the database to pass the list of rows to delete from the database table.

Manipulating the browse itself

There are various ways in which you can allow users to change the appearance of the browse at run time. In some cases, you can provide for these changes programmatically so that they are fully under your control. In other cases, you can just let users use the mouse to change the size and shape of the browse to their liking. This section describes some of those capabilities.

Setting the query attribute for the browse

When you define a browse, you must associate it with a previously defined query. This association allows Progress to identify the source for the browse columns and other information. However, at run time you can associate a browse with any query that supplies the columns the browse needs by setting the browse’s QUERY attribute to the handle of the query. The query must supply the same columns as the one you defined the browse with.

When you set the QUERY attribute, Progress immediately refreshes the browse with the results of the new query. If the query is not open then Progress empties the browse.

You can use this capability to define a placeholder query for a browse simply to satisfy the definition, and then to associate the actual query with it at run time. Or you can use this to alternate between two or more queries to display different useful sets of data in the same browse.

To define a different query on the Order table to alternate with the display of Customers of the current Order:

(This alternative shows all the Orders in the database that have the same OrderDate as the currently displayed Order.)

  1. In the Definitions section of h-CustOrderWin5.w, add these lines:
  2. DEFINE BUFFER   bOrder2    FOR Order.
    DEFINE QUERY    qOrderDate FOR bOrder2 SCROLLING.
    DEFINE VARIABLE lOrderDate AS LOGICAL    NO-UNDO.

    Buffer bOrder2 is a second buffer for the Order table and query qOrderDate is a query on this buffer. You’ll use this to define a query for other Orders that have the same OrderDate as the currently displayed Order. The lOrderDate flag tells the program whether the user is currently seeing the results of the query on the OrderDate or not.
  3. Add a button to the window named btnOrderDate with the label Show all on this Date.
  4. Define this CHOOSE trigger for the button:
  5. DO:
        IF NOT lOrderDate THEN
        DO:
            /* If the standard query is displayed, open the query for
               Orderdates and make that the browse's query.
               Hide the OrderLine browse while we're
               doing this, and adjust the button label accordingly. */
            OPEN QUERY qOrderDate FOR EACH bOrder2
               WHERE bOrder2.OrderDate = Order.OrderDate.
            ASSIGN BROWSE OrderBrowse:QUERY = QUERY qOrderDate:HANDLE
                   /* Signal that we're showing the OrderDate query. */
                   lOrderDate = TRUE
                   hBrowse:HIDDEN = TRUE
                   SELF:LABEL = "Show Customer's Orders".
        END.
        ELSE
           /* If we're showing the OrderDate query, switch back to the
                   regular query for the current Customer's Orders. */
           ASSIGN BROWSE OrderBrowse:QUERY = QUERY OrderBrowse:HANDLE
                         lOrderDate = FALSE
                         hBrowse:HIDDEN = FALSE
                         SELF:LABEL = "Show all on this Date".
    END.

    If the lOrderDate flag is not set, then the user sees the default query of Orders for the current Customer. In this case the code:
    1. Opens the other query for all Orders matching the current OrderDate.
    2. Assigns this to be the query for the OrderBrowse.
    3. Reverses the value of the flag variable.
    4. Hides the OrderLine browse because it’s not relevant to this display.
    5. Switches the label of the button to allow the user to change back to the original query.
    6. If the flag is set, then the trigger reverts to the original query and label, resets the flag, and views the OrderLine browse.
      There’s one more step you need to take, so that the procedure always reverts back to the original query if the user chooses one of the First, Next, Last, or Prev buttons.
  6. Create a local copy of the h-ButtonTrig1.i include file, call it h-ButtonTrig2.i, and add these lines to it:
  7. /* h-ButtonTrig2.i -- include file for the First/Next/Prev/Last buttons
       in h-CustOrderWin5.w. */
       GET {1} CustQuery.
      IF AVAILABLE Customer THEN
       DISPLAY Customer.CustNum Customer.Name Customer.Address Customer.City
               Customer.State
           WITH FRAME CustQuery IN WINDOW CustWin.
      IF lOrderDate THEN
          APPLY "CHOOSE" TO btnOrderDate.
      {&OPEN-BROWSERS-IN-QUERY-CustQuery}
      APPLY "VALUE-CHANGED" TO OrderBrowse.

    This code runs the trigger block if the flag is true, so that it reverts to the Orders OF Customer query.
  8. Run the procedure now and choose the Show all on this Date button. You can see the effect of switching queries for the browse:
Accessing the browse columns

In "Creating and Using Dynamic Temp-tables and Browses," you will learn how to create a dynamic browse. Even with a static browse it is often useful to get at some of its attributes through its handle. You have already seen how you can use the handle of a browse rather than the BROWSE browse-name static language construct to refer to a browse and access its attributes. The individual columns of the browse also have handles. You can get the handle of any browse column by walking through the list of browse columns from left to right. This section introduces you to this concept, and in later examples you’ll use this technique to act on the columns of a browse. These are the basic attributes you need to identify any column in a browse:

Browse columns have various useful attributes that you can get and, in some cases, set (such as its NAME or LABEL). You can learn about all of these attributes in the online help and in the third volume of OpenEdge Development: Progress 4GL Reference. Later examples, such as the one in the "Moving columns" , show how to use a few of these.

Locking columns

You can use the NUM-LOCKED-COLUMNS attribute to prevent one or more browse columns from scrolling out of the browse viewport when the horizontal scrollbar is used. A nonhorizontal-scrolling column is referred to as a locked column.

Locked columns are always the leftmost columns in the browse. In other words, if you set NUM-LOCKED-COLUMNS to 2, the first two columns listed in the DEFINE BROWSE statement are locked. In the next example, the Order Number and Order Date never move out of the browse viewport, no matter which of the remaining fields the user accesses with the horizontal scrollbar.

To experiment with locking columns:
  1. Double-click on the OrderBrowse to go into its property sheet.
  2. Choose the Fields button to edit the field list.
  3. Choose the Add button and add all the rest of the Order fields to the browse. Don’t add any Customer fields, as they just repeat all the values from the Customer record you’re already displaying above the browse.
  4. Back in the property sheet, set Locked Columns to 2.
  5. The AppBuilder generates a statement from the Locked Columns setting to set the NUM-LOCKED-COLUMNS attribute at run time, because this attribute cannot be set using a keyword in the DEFINE BROWSE statement:

    ASSIGN
           OrderBrowse:NUM-LOCKED-COLUMNS IN FRAME CustQuery      = 2.

  6. Run the window again. You can scroll through all the rest of the Order columns, but the first two columns are always displayed:
Moving columns

You can use the MOVE-COLUMN( ) method to rearrange the columns within a browse. For example, rather than forcing the users to scroll horizontally to see additional columns, you might allow them to reorder the columns. MOVE-COLUMN takes two arguments:

The following simple example shows how to use the MOVE-COLUMN method along with the START-SEARCH event and the column attributes introduced earlier.

The START-SEARCH event occurs when the user clicks on the column header for a browse with enabled columns. You can use this event to sort by column or for other purposes. In this case, you want to identify which column position was selected and move this column one position to the left.

To rearrange the columns you see first in the viewport without scrolling, define this START-SEARCH trigger block for the OrderBrowse:

DO:
  DEFINE VARIABLE hColumn AS HANDLE     NO-UNDO.
  DEFINE VARIABLE iColumn AS INTEGER    NO-UNDO.
  hColumn = SELF:FIRST-COLUMN.
  DO iColumn = 1 TO SELF:NUM-COLUMNS:
     IF hColumn = SELF:CURRENT-COLUMN THEN LEAVE.
     hColumn = hColumn:NEXT-COLUMN.
  END.
  IF iColumn NE 1 THEN
     SELF:MOVE-COLUMN(iColumn, iColumn - 1).
END.

Whenever you enter a trigger block, the SELF keyword evaluates to the handle of the object that initiated the event. In this case, this is the browse itself, not the browse column. The CURRENT-COLUMN attribute returns the handle of the column the user clicked on.

The code initializes the hColumn handle variable to the first column in the browse and then walks through all the columns, looking for the one with the same handle as the browse’s CURRENT-COLUMN. This identifies the sequential position of the one selected. The MOVE-COLUMN method moves this column one position to the left, unless the user selected the first column.

You can also let the user simply drag columns left or right by setting the browse COLUMN-MOVABLE attribute to true or an individual column’s MOVABLE attribute to true, as described in the "Moving the browse" .

Overlaying objects on browse cells

One useful way to extend the behavior of an updateable browse in a Windows GUI is to overlay an enabled browse cell with another object when the user enters the cell to edit it. For example, on ENTRY of a cell, you could display a combo box or a toggle box. The user selects an entry from the combo box, or checks the toggle box, and you use those values to update the SCREEN-VALUE attribute of the browse cell. The values get committed or undone along with the rest of the row.

There are two problems to overcome:

  1. The first is in calculating the geometry to let you precisely position the overlay widget. You do this by adding the X and Y attributes of the browse cell to the X and Y attributes of the browse and assigning the result to the X and Y attributes of the overlay object.
  2. You might need to move the overlay object if the user accesses the scrollbar or manipulates the browse in some other way. A trigger on the SCROLL-NOTIFY event notifies you when the user scrolls, and you can update the X and Y positioning accordingly.
  3. Note: It is not possible in every instance to capture every manipulation of a browse to reposition an overlayed object, especially if you have made browse columns resizable and so forth. Thus, this technique is not completely foolproof. However, it can be useful in many cases for extending the browse to have visual behavior that users can expect and that is not natively available with the browse.
To overlay the CreditCard field in the Order browse with a combo box:

(Not only is this convenient, but it assures that the user selects only a valid choice for the field.)

  1. Go into the OrderBrowse property sheet, click on the Fields button and move the CreditCard field up toward the head of the list, so that it is visible without horizontal scrolling.
  2. Enable the CreditCard column.
  3. Select the Combo Box icon from the AppBuilder palette and drop it onto any empty spot on the window. It is initially hidden and then moved to the proper location when it’s needed, so it doesn’t matter where it starts out.
  4. In its property sheet, give the new object a name of cCreditCard, check the No-Label toggle box, and provide the three choices shown for the List-Items:
  5. Make the Format X(16) and check the Hidden toggle box from the set of Other Settings at the bottom.
  6. Choose OK to accept these settings.
  7. Now you have two objects that will interact: the browse column called CreditCard representing the CreditCard field in the Order table and the combo box called cCreditCard that you will overlay on it.
  8. You might also have to add a line to the procedure’s main block to make sure that the combo box is hidden when the window first comes up:
  9. MAIN-BLOCK:
    DO ON ERROR UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK
      ON END-KEY UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK:
      RUN enable_UI.
      cCreditCard:HIDDEN = TRUE.
      APPLY "VALUE-CHANGED" TO OrderBrowse.
    END.

Now you need to define a series of trigger blocks to handle the following events on the browse and on the combo box:

To create these trigger blocks:
  1. Define an internal procedure called placeCombo to position and view the combo box on top of the selected browse cell:
  2. PROCEDURE placeCombo:
    DEFINE VARIABLE hCredit AS HANDLE     NO-UNDO.
    hCredit = CreditCard:HANDLE IN BROWSE OrderBrowse.
    IF hCredit:X < 0 OR hCredit:Y < 0 THEN /* Column not on screen */
       cCreditCard:VISIBLE IN FRAME CustQuery = FALSE.
    ELSE DO:
        ASSIGN cCreditCard:X IN FRAME CustQuery = hCredit:X + OrderBrowse:X
               cCreditCard:Y IN FRAME CustQuery = hCredit:Y + OrderBrowse:Y
               cCreditCard:SCREEN-VALUE = CreditCard:SCREEN-VALUE
               cCreditCard:VISIBLE IN FRAME CustQuery = TRUE.
               cCreditCard:MOVE-TO-TOP().
    END.
    END PROCEDURE.

    This procedure gets the handle to the CreditCard cell in the browse and checks its X and Y coordinates (which are in pixels). If they are not both greater than zero, then the column is not currently displayed and no action is taken. This might happen in response to a browse event that has scrolled the contents of the browse, for instance. If the column has been scrolled out of the viewport, then the column can’t be updated.
    Otherwise, the code totals the X and Y positions of the browse itself and the CreditCard cell. This is because the X and Y coordinates of the browse are relative to its frame, and the X and Y coordinates of the cell are relative to the browse. Since the combo box coordinates are relative to the frame, you need to add the browse position and the cell position together to get the cell’s position relative to the frame, which is where the combo box will go.
    Next, the code initializes the cCreditCard combo box to the SCREEN-VALUE of the selected cell. Finally, the code makes the combo box visible and uses the MOVE-TO-TOP method to make sure that it’s displayed on top of the browse and not hidden underneath it.
  3. Define an ENTRY trigger for the CreditCard browse column to run the procedure when the user clicks in the column:
  4. DO:
      RUN placeCombo.
    END.

  5. Define a LEAVE trigger for the CreditCard column:
  6. DO:
      cCreditCard:VISIBLE IN FRAME CustQuery = FALSE.
    END.

    You want this trigger to execute when the user really leaves this column, for example, by selecting another column or another row. The overlay combo has finished its usefulness at that point and you should hide it so that the simple value in the cell itself is shown. However, there is one wrinkle to this.
    As soon as the user clicks in the combo box to change its value, this action leaves the browse cell and then enters the combo box. The LEAVE trigger you just defined therefore hides the combo box, which is not at all what you want! So you have to define an ENTRY trigger for the combo box to make sure it is visible whenever it is being used.
  7. Define an ENTRY trigger for the cCreditCard combo box to make sure it displays whenever it is selected:
  8. DO:
      SELF:VISIBLE = TRUE.
    END.

    After the user has selected a value from the combo box, you need to hide the combo box, apply the new value to the browse cell, and move focus back into the browse cell.
  9. To do this, write this VALUE-CHANGED trigger for cCreditCard:
  10. DO:
      cCreditCard:VISIBLE IN FRAME CustQuery = FALSE.
      CreditCard:SCREEN-VALUE IN BROWSE OrderBrowse =
         cCreditCard:SCREEN-VALUE.
      APPLY "ENTRY" TO CreditCard IN BROWSE OrderBrowse.
    END.

    Now your changes basically work, but the placement of the combo box is messed up if the user scrolls the browse when the combo box is visible.
  11. Write this SCROLL-NOTIFY trigger for the OrderBrowse to reposition the overlay combo box properly:
  12. DO:
      RUN placeCombo.
    END.

    Because you enabled column moving, you need to do the same thing on the START-SEARCH event so that in case the user moves the CreditCard column, the combo box moves with it.
  13. Add this line to the START-SEARCH trigger for the browse:
  14. DO:
      DEFINE VARIABLE hColumn AS HANDLE NO-UNDO.
      DEFINE VARIABLE iColumn AS INTEGER NO-UNDO.
      hColumn = SELF:FIRST-COLUMN.
      DO iColumn = 1 TO SELF:NUM-COLUMNS:
          IF hColumn = SELF:CURRENT-COLUMN THEN LEAVE.
          hColumn = hColumn:NEXT-COLUMN.
      END.
      IF iColumn NE 1 THEN
          SELF:MOVE-COLUMN(iColumn, iColumn - 1).
      RUN placeCombo.
    END.

To test your changes:
  1. Run the window and select a Credit Card cell. The combo box overlays the cell:
  2. If necessary, edit your code to adjust the width of the Credit Card column and the row height of the browse so that the combo box looks as though it really fits into the cell.
  3. Select the combo box, then select one of the valid choices:
  4. When you leave the field, this becomes the new value for the cell:

Browse style options

This section offers a few ideas for changing the look of your browse.

Using stacked labels

To use more than one line for your column labels, use the stacked label syntax. Here is an example:

DEFINE BROWSE CustBrowse QUERY CustQuery DISPLAY
  CustNum COLUMN-LABEL "Customer!Number"
  Name COLUMN-LABEL "Customer!Name" WITH 10 DOWN.

You must use the COLUMN-LABEL option instead of the LABEL option. The exclamation point character indicates the line breaks.

Justifying labels

Column labels in the browse are left-justified by default. You can use the C, L, and R options (Center, Left, Right) of the LABEL attribute to modify the justification of column labels:

DEFINE BROWSE CustBrowse QUERY CustQuery DISPLAY
  CustNum LABEL "Cust. No.":C
  Name WITH 10 DOWN.

Note the colon (:) syntax. To use this option, you must attach the justification option to the end of the LABEL option. So, even if you want to use the default labels, you need to re-enter them here in order to append the justification option.

Using color to distinguish updateable columns

You can make the updateable columns in your browse a different color. For example, you can make the read-only columns gray with black text and the updateable columns blue with yellow text. This code fragment assumes you defined variables to hold the standard color values:

ASSIGN CustNum:COLUMN-BGCOLOR IN BROWSE CustBrowse = gray-color
       CustNum:COLUMN-FGCOLOR IN BROWSE CustBrowse = black-color
       Name:COLUMN-BGCOLOR IN BROWSE CustBrowse = blue-color
       Name:COLUMN-FGCOLOR IN BROWSE CustBrowse = yellow-color.

Note: In character interfaces, the COLUMN-DCOLOR attribute specifies the column color.
Using color and font to distinguish cells

On top of your basic color scheme, you may want individual cells that have key values to display in a different color or font. For example, you might want to color overdue accounts in red. This kind of cell manipulation is only valid while the cell is in the viewport. For this reason, you need to use the special ROW-DISPLAY event to check each new row as it is scrolled into the viewport. See the "Browse events" for examples and implementation notes.

Establishing ToolTip information

The DEFINE BROWSE statement supports the TOOLTIP option. You can elect to specify a ToolTip, a brief text message string that automatically displays when the mouse pointer pauses over a browse widget for which a ToolTip value is defined. You can set a ToolTip value for a variety of field-level widgets. However, they are most commonly defined for button widgets.

Using a disabled updateable browse as a read-only browse

A browse with no enabled columns is considered a read-only browse. A read-only browse has certain limitations that you might want to circumvent by defining one or more enabled columns in the browse definition. You then disable those columns at run time by setting their READ-ONLY attribute to TRUE. Using an updateable browse that has had its enabled columns turned off by way of the READ-ONLY attribute provides these benefits:

Resizable browse objects

In graphical interfaces, you (the programmer) and the user can:

Resizing the browse

To let the user resize a browse, you set the browse's RESIZABLE and SELECTABLE attributes to TRUE, as the following code fragment shows:

ASSIGN CustBrowse:RESIZABLE = TRUE
       CustBrowse:SELECTABLE = TRUE.

To resize a browse through direct manipulation, the user clicks on the browse to display the resize handles, then drags a resize handle as desired.

To resize a browse programmatically, you set the browse’s WIDTH-CHARS or WIDTH-PIXELS, HEIGHT-CHARS or HEIGHT-PIXELS, or DOWN attributes as desired.

The following code fragment programmatically resizes a browse to 50 characters wide by 40 characters high:

ASSIGN CustBrowse:WIDTH-CHARS = 50
       CustBrowse:HEIGHT-CHARS = 40.

Moving the browse

To let the user move a browse, you set the browse’s MOVABLE attribute to TRUE, as the following code fragment shows:

CustBrowse:MOVABLE = TRUE.

To move a browse at run time, the user drags the browse as desired. To move a browse programmatically, you set the browse’s X and Y attributes as desired.

The following code fragment programmatically moves a browse to the point (50,50) (in pixels) relative to the parent frame:

ASSIGN CustBrowse:X = 50
       CustBrowse:Y = 50.

Resizing the browse column

To let the user resize a browse column, use one of the following techniques:

Moving the browse column

To let the user move the columns of a browse, use one of the following techniques:

To move a browse column programmatically, you use the MOVE-COLUMN method of the browse, as shown earlier.

Changing the row height of the browse

To let the user change the row height of a browse, you set the browse’s ROW-RESIZABLE attribute to TRUE.

To change the row height of a browse, the user vertically drags a row separator that appears at run time.

To change the row height programmatically, you set the browse’s ROW-HEIGHT-CHARS or ROW-HEIGHT-PIXELS attribute.

Additional attributes

When you set up a resizable browse, whether for user or programmatic manipulation, you can set additional attributes.

When setting up a browse for user manipulation, you can set the following attributes:

When setting up a browse to resize or otherwise manipulate programmatically, you can set the following attributes:

User manipulation events

When the user moves or resizes a browse or one of its components, Progress fires one or more of the following events:

These events do not fire when you manipulate the browse programmatically.

You do not have to monitor these events. This chapter includes them only for the your reference. For example, you might want to write trigger code for one or more of these events.

For more information on these events, see the Events Reference section of OpenEdge Development: Progress 4GL Reference.

Using browse objects in character interfaces

Browse objects in character interfaces operate in two distinct modes: row mode and edit mode.

In row mode, the user can select and change focus among rows, as well as navigate (tab) to widgets outside the browse. In edit mode, the user can edit and tab between enabled cells within the browse. A user can only enter edit mode in an updateable browse, but can operate in row mode in either a read-only or an updateable browse.

Character browse modes

Figure 13–3 shows a character browse in row mode.

Figure 13–3: Character browse in row mode

The asterisks (*) are row markers that indicate editable rows in an updateable browse. Row markers do not appear:

In row mode, the highlight indicates the row that has focus and the close angle brackets (>) are tick marks that indicate selected rows. In a single selection browse, the tick mark follows the highlight because focus and selection are the same. In a multiple selection browse, focus is independent of selection, and tick marks indicate the selected rows. Figure 13–4 shows a character browse in edit mode.

Figure 13–4: Character browse in edit mode

In edit mode, all rows become deselected except the edited row. The highlight indicates the edited cell within the edited row. As the user moves from row to row during editing, the tick mark and highlight both move to indicate the row and cell being edited.

Control keys

To control operation of a character browse, Progress provides a set of dedicated control keys. These control keys apply differently in row mode or edit mode. However, in both modes, any control key that changes (or attempts to change) the selected row fires the VALUE-CHANGED event.

These control keys correspond to Progress key functions that you can redefine. On Windows, you can redefine them in the registry or in an initialization file. On UNIX, you can redefine them in the PROTERMCAP file. For more information on redefining control key functions, see the chapter on user interface environments in OpenEdge Deployment: Managing 4GL Applications.

Table 13–2 describes the key functions, default key labels, and operation of the row mode control keys.

Table 13–2: Row mode control keys
Key functions
Default key labels
Description
" "1
SPACE BAR
Fires the VALUE-CHANGED event. In multiple selection mode, this key also selects and deselects the row that has focus, alternately displaying and erasing the tick mark.
CURSOR-LEFT
CURSOR-LEFT
Scrolls the browse horizontally one column to the left.
CURSOR-RIGHT
CURSOR-RIGHT
Scrolls the browse horizontally one column to the right.
CURSOR-DOWN
CURSOR-DOWN
Moves focus down one row.2
CURSOR-UP
CURSOR-UP
Moves focus up one row.2
END
ESC-.
Moves focus to the last row in the browse.2
HOME
ESC-,
Moves focus to the first row in the browse.2
PAGE-DOWN
ESC-CURSOR-DOWN
Pages down one full page of data.2
PAGE-UP
ESC-CURSOR-UP
Pages up one full page of data.2
REPLACE
ESC-R
Changes the browse to edit mode, places focus in the first enabled cell of the currently focused row, and fires the VALUE-CHANGED event. In multiple selection mode, this key also selects the focused row and deselects any other selected rows. If you specify NO-ROW-MARKERS in the browse definition or set the ROW-MARKERS attribute to FALSE, REPLACE has no effect.
RETURN
RETURN
Fires the DEFAULT-ACTION event.
TAB
TAB
Leaves the browse and sets input focus to the next sibling of the browse in the tab order.

1 Note that Progress has no key function identifier for the space bar. In code, you reference a character string containing a single space (" ").
2 In single selection mode, this also fires the VALUE-CHANGED event because selection follows focus.

Table 13–3 describes the key functions, default key labels, and operation of the edit mode control keys.

Table 13–3: Edit mode control keys
Key function
Default key label
Description
" "1
SPACE BAR
Enters a space or zeroes numeric data in the focused cell.
CURSOR-LEFT
CURSOR-LEFT
Moves the cursor one character to the left in the cell. (Does not leave the cell.)
CURSOR-RIGHT
CURSOR-RIGHT
Moves the cursor one character to the right in the cell. (Does not leave the cell.)
CURSOR-DOWN
CURSOR-DOWN
Moves focus down to the next cell in the column and fires the VALUE-CHANGED event.
CURSOR-UP
CURSOR-UP
Moves focus up to the previous cell in the column and fires the VALUE-CHANGED event.
BACK-TAB
CTRL-U
Moves focus to the previous enabled cell in the browse (right to left, bottom to top).2 When focus is on the first cell, BACK-TAB does not function.
EDITOR-TAB
CTRL-G
Moves focus to the next enabled cell in the browse (left to right, top to bottom).2 When focus is on the last cell, the EDITOR-TAB does not function.
END
ESC-.
Moves focus to the last cell in the current column and fires the VALUE-CHANGED event.
HOME
ESC-,
Moves focus to the first cell in the current column and fires the VALUE-CHANGED event.
PAGE-DOWN
ESC-CURSOR-DOWN
Pages down one full page of data and fires the VALUE-CHANGED event.
PAGE-UP
ESC-CURSOR-UP
Pages up one full page of data and fires the VALUE-CHANGED event.
REPLACE
ESC-R
Changes the browse to row mode, with selection set to the currently focused row.
RETURN
RETURN
Moves focus to the next cell in the current column and fires the VALUE-CHANGED event.
TAB
TAB
Leaves the browse and sets input focus to the next sibling widget of the browse in the tab order.

1 Note that Progress has no key function identifier for the space bar. In code, you reference a character string containing a single space (" ").
2 If this action changes or attempts to change the selected row, it also fires the VALUE-CHANGED event.

Functional differences from the Windows graphical browse

The character browse shares most of the same functional capabilities of the Windows graphical browse, including all methods and most attributes and events.

However, there are differences from the Windows graphical browse in:

Font management

Because there is no font management within character interfaces, all font attributes are inactive for the character browse.

Color management

For color terminals, the character browse supports the following attributes to manage its color:

By default, the COLUMN-PFCOLOR and PFCOLOR values are both set from the INPUT color value. On Windows, you can specify this value in the registry or in an initialization file. On UNIX, you can specify this value in the PROTERMCAP file.

Row and cell navigation

Unlike the Windows graphical browse, the character browse does not support column searching (positioning to a row or cell by typing a character contained in that row or cell).

Unlike the Windows graphical browse, the character browse uses a different set of key functions to tab between updateable cells than between the browse and other sibling widgets. Tabbing between cells occurs only in the edit mode of the character browse using the EDIT-TAB and BACK-TAB key functions.

Tabbing forward from the character browse to a sibling object occurs in either row mode or edit mode using the TAB key function. However, tabbing backward from the character browse to a sibling object occurs only in row mode using the BACK-TAB key function.

Conclusion

This chapter has introduced you to some of the ways in which you can use the browse and customize its behavior. The browse is a complex object, and one that you can manipulate in many ways—more than can be completely documented in a single chapter. To explore the complete capabilities of the browse, you should consult the Browse Widget section of OpenEdge Development: Progress 4GL Reference or the online help. These provide both a list of all attributes and methods, and an explanation of each. Just by way of example of how extensive the browse capabilities are, Figure 13–5 shows some of the browse methods in the online Help.

Figure 13–5: Browse widget online help topic


Copyright © 2005 Progress Software Corporation
www.progress.com
Voice: (781) 280-4000
Fax: (781) 280-4095
Table of ContentsPreviousNextIndex