Summary:
See also: Arrays, Records, Result Sets, Programs, Windows, Forms, Display Array, Drag andDrop.
Before reading these lines, you should be familiar with form TABLE containers and you must know how to program a singular DISPLAY ARRAY or a DISPLAY ARRAY sub-dialog within a DIALOG instruction.
Typical GUI tree-views can be implemented with Genero by using a DISPLAY ARRAY instruction with a screen-array bound to a TREE container with tree-view specific attributes. TREE containers are very similar to TABLE containers, except that the first columns are used to display a tree of nodes on the right of the widget.
The next screen-shot shows a typical file browser written in Genero, this example implements a DIALOG instruction with two DISPLAY ARRAY sub-dialogs. The first DISPLAY ARRAY sub-dialog controls the tree-view while the second one controls the file list on the right side:
The data used to display tree-view nodes must be provided in a program array and controlled by a DISPLAY ARRAY. It is possible to control a tree-view table with a singular DISPLAY ARRAY or with a DISPLAY ARRAY sub-dialog within a DIALOG instruction.
In Genero, a tree-view model is implemented with a flat program array (i.e. a list of rows), where each row defines parent/child node identifiers to describe the structure of the tree; so, the order of the rows matters:
Tree structure parent-id child-id Node 1 NULL 1 Node 1.1 1 1.1 Node 1.2 1 1.2 Node 1.2.1 1.2 1.2.1 Node 1.2.2 1.2 1.2.2 Node 1.2.3 1.2 1.2.3 Node 1.3 1 1.3 Node 1.3.1 1.3 1.3.1 ... ... ....
Depending on your need, you can fill the program array with all rows of the tree before dialog execution, or you can fill or reduce the list of nodes dynamically upon expand / collapse action events. In the second case, you must provide additional information for each row of the program array, to indicate whether the node has children. A dynamic build of the tree-view allows you to implement programs displaying very large trees, for example in a bill of materials application, where thousands of elements can be assembled together.
Note that tree-views can display additional columns for each node, to show specific row data as in a regular table:
Create a form specification file containing a TREE container bound to a screen array. The screen array identifies the presentation elements to be used by the runtime system to display the tree-view and the additional columns.
A TREE container must be present in the LAYOUT section of the form, defining the columns of the tree-view list. The TREE container must hold at least one column defining the node texts (or names). This column will be used on the front-end side to display the tree-view widget. Additional columns can be added in the TREE container to display node information. The TREE container attributes must be declared in the ATTRIBUTES section, as explained below.
Secondary form fields have to be used to hold tree node information such as icon image, parent node id, current node id, expanded flag and parent flag. While these secondary fields can be defined as regular form fields and displayed in the tree-view list, we recommend that you use PHANTOM fields instead: Phantom fields can be listed in the screen-array but do not need to be part of the LAYOUT section. Phantom fields will only be used by the runtime system to build the tree of nodes.
Example of tree-view definition using a TREE container:
01
LAYOUT02
TREE mytree ( PARENTIDCOLUMN=parentid, IDCOLUMN=id,03
EXPANDEDCOLUMN=expanded, ISNODECOLUMN=isnode )04
{05
Tree06
[name |desc ]07
[name |desc ]08
[name |desc ]09
[name |desc ]10
[name |desc ]11
}12
END13
END14
ATTRIBUTES15
EDIT name = FORMONLY.name, IMAGECOLUMN=image;16
PHANTOM FORMONLY.image;17
PHANTOM FORMONLY.parentid;18
PHANTOM FORMONLY.id;19
PHANTOM FORMONLY.expanded;20
PHANTOM FORMONLY.isnode;21
EDIT desc = FORMONLY.description;22
END23
INSTRUCTIONS24
SCREEN RECORD sr( FORMONLY.* );25
END
Example of tree-view definition using the <Tree > tag inside a GRID container, and a TREE form element to define attributes:
01
LAYOUT02
GRID03
{04
<Tree tv >05
Tree06
[name |desc ]07
[name |desc ]08
[name |desc ]09
[name |desc ]10
[name |desc ]11
< >12
}13
END14
END15
ATTRIBUTES16
TREE tv : mytree,17
PARENTIDCOLUMN=parentid, IDCOLUMN=id,18
EXPANDEDCOLUMN=expanded, ISNODECOLUMN=isnode;19
EDIT name = FORMONLY.name, IMAGECOLUMN=image;20
PHANTOM FORMONLY.image;21
PHANTOM FORMONLY.parentid;22
PHANTOM FORMONLY.id;23
PHANTOM FORMONLY.expanded;24
PHANTOM FORMONLY.isnode;25
EDIT desc = FORMONLY.description;26
END27
INSTRUCTIONS28
SCREEN RECORD sr( FORMONLY.* );29
END
Note that the first visual column (name in above example) must be the field defining the node names, and the widget must be an EDIT or LABEL.
The following ATTRIBUTES are used to configure a TREE form element:
Tree-view definition must be completed with form fields declaration. These must be defined in the ATTRIBUTES section. The fields not used for display are declared as PHANTOM fields. The tree-view form fields must be grouped in a screen-array declared in the INSTRUCTIONS section.
The form fields required to declare a tree-view table are the following.
Description | Field type | Tree attribute to define the field | Mandatory | Default name |
Text to be displayed for the node | EDIT | N/A | yes | N/A |
Id of the node | PHANTOM | IDCOLUMN | yes | id |
Id of the parent node | PHANTOM | PARENTIDCOLUMN | yes | parentid |
Icon image for a node | PHANTOM | IMAGECOLUMN | no | N/A |
Node expansion indicator | PHANTOM | EXPANDEDCOLUMN | no | no |
Parent node indicator | PHANTOM | ISNODECOLUMN | no | no |
Note that the first three fields (node text, parent id and node id) are mandatory, and that the first visual (non-phantom) field listed in the screen array will be implicitly used to hold the text of tree-view nodes.
Additional fields (like the desc field in the above example) can be defined to display details for each node in regular columns, that will appear on the right of the tree widget.
The order of the fields in the screen array of the tree-view does not matter, but it must of course match the order of the corresponding variables in the record-array of the program.
If you need to display node-specific images, define a phantom field to hold node images and attach it to the tree-view definition by using the IMAGECOLUMN attribute. Alternatively you can globally define images for all nodes with the IMAGEEXPANDED, IMAGECOLLAPSED and the IMAGELEAF attributes of the TREE form element.
In the program code, you must define a dynamic array of records with the DEFINE instruction. The Genero runtime system will use that program array as the model for the tree-view list. A tree of nodes will be automatically built according to the data found in the program array. The front-end can then render the tree of nodes in a tree-view list.
The members of the program array must correspond to the elements of the screen-array of the tree table, by number and data types.
The name of the array members does not matter; the purpose of each member is defined by the name of the corresponding screen-array members declared in the form file. Program array members and screen-array members are bound by position.
The next code example defines a program array with a member structure corresponding to the screen-array defined in the form example of the previous section.
01
DEFINE tree_arr DYNAMIC ARRAY OF RECORD02
name STRING, -- text to be displayed for the node03
pid STRING, -- id of the parent node04
id STRING, -- id of the current node05
image STRING, -- name of the image file for the node (can be null)06
expanded BOOLEAN, -- node expansion flag (TRUE/FALSE) (optional)07
isnode BOOLEAN, -- children indicator flag (TRUE/FALSE) (optional)08
description STRING -- user field describing the node09
END RECORD
The name, pid, id members are mandatory. These hold respectively the node text, parent and current node identifiers that define the structure of the tree.
The image member will hold the name of the little icon to be displayed for each node and leaf. You can omit this member, if you do not want to display images, or when then tree defines default images with the IMAGEEXPANDED, IMAGECOLLAPSED and the IMAGELEAF attributes.
The expanded member can be used to handle node expansion by program. You can query this member to check whether a node is expanded, or set the value to expand a specific node.
The isnode member can be used to indicate whether a given node has children, without filling the array with rows defining the child nodes. This information will be used by front-ends to decorate a node as a parent, even if no children are present. The program should then fill the array with child nodes when an expand action is invoked (see Dynamic filling of very large trees).
Note that the program array can hold more columns (like the description field), which can be displayed in regular table columns as part of a node's data.
Remember the order of the program array members must match the screen-array members in the form file, but this order can be different from the column order used in the layout, with the exception of the first column defining the text of nodes (i.e. name field in above example).
Once the program array is defined according to the screen-array of the tree-view table, you must fill the rows with the correct parent/child relationships defining the structure of the tree.
You can safely fill the program array directly before the dialog execution or in BEFORE DISPLAY / BEFORE DIALOG control blocks. However, once the dialog has started, you must use the DIALOG methods insertNode(), appendNode() and deleteNode(), if you want to modify the tree, otherwise secondary data information like multi-range selection and cell attributes will not be synchronized.
The rows must be filled in the correct order, to reflect the parent/child relationship.
If a row defines a tree-view node with a parent identifier that does not exist, or if the child row is inserted under the wrong parent row, the orphan row will become a new node at the root of the tree.
In order to fill the program array with database rows defining the tree structure, you will need to write a recursive function, keeping track of the current level of the nodes to be created for a given parent.
The next example shows how to fill the array with data coming from a database table having the following structure:
CREATE TABLE dbtree ( id SERIAL NOT NULL, parentid INTEGER NOT NULL, name VARCHAR(20) NOT NULL )
The difficulty with fetching a tree from a database table is in the cursor management, which can not be used recursively. A workaround for this problem is to fetch all the children of a given node at once, then call the function recursively for each of the fetched nodes:
01
TYPE tree_t RECORD02
id INTEGER,03
parentid INTEGER,04
name VARCHAR(20)05
END RECORD06
07
DEFINE tree_arr tree_t08
09
FUNCTION fetch_tree(pid)10
DEFINE pid, i, j, n INTEGER11
DEFINE a DYNAMIC ARRAY OF tree_t12
DEFINE t tree_t13
14
DECLARE cu1 CURSOR FOR SELECT * FROM dbtree WHERE parentid = pid15
LET n = 016
FOREACH cu1 INTO t.*17
LET n = n + 118
LET a[n].* = t.*19
END FOREACH20
21
FOR i = 1 TO n22
LET j = tree_arr.getLength() + 123
LET tree_arr[j].name = a[i].name24
LET tree_arr[j].id = a[i].id25
LET tree_arr[j].parentid = a[i].parentid26
CALL fetch_tree(a[i].id)27
END FOR28
29
END FUNCTION
After the program array has been filled, you must execute a DISPLAY ARRAY instruction. This can be a singular DISPLAY ARRAY or a DISPLAY ARRAY sub-dialog within a DIALOG instruction.
The next code example implements a DISPLAY ARRAY binding the program array called tree_arr to the sr screen-array, attaching the dialog to the tree table defined in the form:
01
DISPLAY ARRAY tree_arr TO sr.* ATTRIBUTES(UNBUFFERED)02
BEFORE DISPLAY03
CALL fell_tree(tree_arr)04
BEFORE ROW05
DISPLAY "Current row is: ", DIALOG.getCurrentRow("sr")06
END DISPLAY
It is not possible to use the paged mode (ON FILL BUFFER) when the decoration is a treeview list. The dialog needs the complete set of open nodes with parent/child relation to handle the treeview display, with the paged mode only a given window of the dataset is known by the dialog. If you use a the paged mode in DISPLAY ARRAY with a treeview as decoration, the program will raise an error at runtime. TreeViews can be filled dynamically with ON EXPAND / ON COLLAPSE triggers. See Dynamic filling of very large trees for more details.
If needed, you can implement traditional DISPLAY ARRAY control blocks like BEFORE ROW or AFTER ROW:
01
DISPLAY ARRAY tree_arr TO sr.* ATTRIBUTES(UNBUFFERED)02
BEFORE ROW03
DISPLAY "BEFORE ROW - Current row is: ", DIALOG.getCurrentRow("sr")04
AFTER ROW05
DISPLAY "AFTER ROW - Current row is: ", DIALOG.getCurrentRow("sr")06
END DISPLAY
During the DISPLAY ARRAY execution, it is possible to modify the content of the tree model (i.e. the program array), by inserting, adding or removing nodes programmatically. However, you should not directly modify the program array: You must use the DIALOG class methods insertNode(), appendNode() and deleteNode() to modify the tree model. By using these methods, the dialog can synchronize internal data, otherwise the tree display would be corrupted.
When a huge tree needs to be displayed (for example, when implementing a bill of materials application), you should optimize tree data filling by creating the nodes on demand. There is no need to fill the complete program array with all possible nodes (down to the last leaf), when only the first levels/branches of the tree are displayed on the screen.
To implement a dynamically filled tree, you need first to define an additional column in the tree-table, to indicate whether a given node has children. That field will be used by Genero to render a node with a [+] button, and let the end user click on the node to expand it, even if no child nodes are created yet.
In the DISPLAY ARRAY code, if a node is expanded (or collapsed), the dialog will invoke the ON EXPAND (or ON COLLAPSE) triggers, to let the program add (or remove) rows in the array, to adapt the tree data dynamically according to navigation events.
01
DEFINE row_index INTEGER02
...03
DISPLAY ARRAY tree_arr TO sr.* ATTRIBUTES(UNBUFFERED)04
ON EXPAND (row_index)05
DISPLAY "EXPAND - Expanded row is : ", row_index06
-- Fill with children nodes for tree_arr[row_index]07
ON COLLAPSE (row_index)08
DISPLAY "COLLAPSE - Collapsed row is: ", row_index09
-- Remove children nodes of tree_arr[row_index]10
END DISPLAY
While you can safely fill the program array directly before the dialog execution, once the dialog has started, you must use DIALOG methods such as insertNode() to modify the tree, otherwise secondary data information like multi-range selection and cell attributes will not be synchronized. This is typically the case when implementing a dynamically-filled tree with ON EXPAND / ON COLLAPSE triggers.
See also Example 2 implementing dynamic tree filling.
By default, the built-in sort is enabled when you use a TREE container; when the end user clicks on column headers, the runtime system sorts the visual representation of the program array. Tree nodes are ordered by levels; the children nodes are ordered inside a given parent node.
This is a powerful built-in feature. However, in some cases, the tree structure must be static (i.e. the order of the nodes must not change) and you don't want the end user to sort the rows. To prevent the built-in sort, use the UNSORTABLECOLUMNS attribute for the TREE container:
01
LAYOUT02
...03
END04
ATTRIBUTES05
TREE tv : mytree, UNSORTABLECOLUMNS, ...06
...
Multi-row selection can be used with a DISPLAY ARRAY controlling a TREE container. However, because of the tree-view ergonomic differences with simple tables, the selection of tree nodes follows some specific rules:
See also the topic discussing multi-row selection.
It is possible to implement Drag and Drop with a DISPLAY ARRAY controlling a TREE container. The nodes can be moved around in the same tree, can be dropped outside the tree or can be inserted in the tree from external sources.
For more details, see Drag & Drop.
Form file:
01
LAYOUT02
GRID03
{04
<Tree t1 >05
Name Index06
[c1 |c2 ]07
[c1 |c2 ]08
[c1 |c2 ]09
[c1 |c2 ]10
}11
END12
END13
14
ATTRIBUTES15
LABEL c1 = FORMONLY.name;16
LABEL c2 = FORMONLY.idx;17
PHANTOM FORMONLY.pid;18
PHANTOM FORMONLY.id;19
PHANTOM FORMONLY.exp;20
TREE t1: tree121
IMAGEEXPANDED = "open",22
IMAGECOLLAPSED = "folder",23
IMAGELEAF = "file",24
PARENTIDCOLUMN = pid,25
IDCOLUMN = id,26
EXPANDEDCOLUMN = exp;27
END28
29
INSTRUCTIONS30
SCREEN RECORD sr_tree(name, pid, id, idx, exp);31
END
Static tree DISPLAY ARRAY:
01
DEFINE tree DYNAMIC ARRAY OF RECORD02
name STRING,03
pid STRING,04
id STRING,05
idx INT,06
expanded BOOLEAN07
END RECORD08
09
MAIN10
OPEN FORM f FROM "stat-tree"11
DISPLAY FORM f12
CALL fill(4)13
DISPLAY ARRAY tree TO sr_tree.* ATTRIBUTE(UNBUFFERED)14
BEFORE ROW15
DISPLAY "Current row : ", arr_curr()16
END DISPLAY17
END MAIN18
19
FUNCTION fill(max_level)20
DEFINE max_level, p INT21
CALL tree.clear()22
LET p = fill_tree(max_level, 1, 0, NULL)23
END FUNCTION24
25
FUNCTION fill_tree(max_level, level, p, pid)26
DEFINE max_level, level INT27
DEFINE p INT28
DEFINE i INT29
DEFINE id, pid STRING30
DEFINE name STRING31
IF level < max_level THEN32
LET name = "Node "33
ELSE34
LET name = "Leaf "35
END IF36
FOR i = 1 TO level37
LET p = p + 138
IF pid IS NULL THEN39
LET id = i40
ELSE41
LET id = pid || "." || i42
END IF43
LET tree[p].id = id44
LET tree[p].pid = pid45
LET tree[p].idx = p46
LET tree[p].expanded = FALSE47
LET tree[p].name = name || level || '.' || i48
IF level < max_level THEN49
LET p = fill_tree(max_level, level + 1, p, id)50
END IF51
END FOR52
RETURN p53
END FUNCTION
Form file:
01
LAYOUT02
GRID03
{04
<Tree t1 >05
Name Description06
[c1 |c2 ]07
[c1 |c2 ]08
[c1 |c2 ]09
[c1 |c2 ]10
}11
END12
END13
14
ATTRIBUTES15
LABEL c1 = FORMONLY.name;16
PHANTOM FORMONLY.pid;17
PHANTOM FORMONLY.id;18
PHANTOM FORMONLY.hasChildren;19
LABEL c2 = FORMONLY.descr;20
TREE t1: tree121
IMAGEEXPANDED = "open",22
IMAGECOLLAPSED = "folder",23
IMAGELEAF = "file",24
PARENTIDCOLUMN = pid,25
IDCOLUMN = id,26
ISNODECOLUMN = hasChildren;27
END28
29
INSTRUCTIONS30
SCREEN RECORD sr_tree(FORMONLY.*);31
END
Dynamic tree DISPLAY ARRAY:
01
DEFINE tree DYNAMIC ARRAY OF RECORD02
name STRING,03
pid STRING,04
id STRING,05
hasChildren BOOLEAN,06
description STRING07
END RECORD08
09
MAIN10
DEFINE id INTEGER11
12
OPEN FORM f FROM "dyna-tree"13
DISPLAY FORM f14
15
LET tree[1].pid = 016
LET tree[1].id = 117
LET tree[1].name = "Root"18
LET tree[1].hasChildren = TRUE19
DISPLAY ARRAY tree TO sr_tree.* ATTRIBUTE(UNBUFFERED)20
BEFORE DISPLAY21
CALL DIALOG.setSelectionMode("sr_tree",1)22
ON EXPAND(id)23
CALL expand(DIALOG,id)24
ON COLLAPSE(id)25
CALL collapse(DIALOG,id)26
END DISPLAY27
END MAIN28
29
FUNCTION collapse(d,p)30
DEFINE d ui.Dialog31
DEFINE p INT32
WHILE p < tree.getLength()33
IF tree[p + 1].pid != tree[p].id THEN EXIT WHILE END IF34
CALL d.deleteNode("sr_tree", p + 1)35
END WHILE36
END FUNCTION37
38
FUNCTION expand(d,p)39
DEFINE d ui.Dialog40
DEFINE p INT41
DEFINE id STRING42
DEFINE i, x INT43
FOR i = 1 TO 444
LET x = d.appendNode("sr_tree", p)45
LET id = tree[p].id || "." || i46
LET tree[x].id = id47
-- tree[x].pid is implicitly set by the appendNode() method...48
LET tree[x].name = "Node " || id49
IF i MOD 2 THEN50
LET tree[x].hasChildren = TRUE51
ELSE52
LET tree[x].hasChildren = FALSE54
END IF55
LET tree[x].description = "This is node " || tree[x].name56
END FOR57
END FUNCTION