TreeControl



The treeControl is a generic model based control. Meaning:


 * Generic: it contains data of your choice
 * Model based: the control displays the contents of a data model

This article is related to the treeControlDemo example that comes with Visual Prolog Commercial Edition. But some of the code in the article is simplified other code may be improved (improvements will be reflected in later versions of the example).

The treeControl was introduced in Visual Prolog 7.3 Commercial Edition.

Basic concepts
To use the tree control it must be attached to a tree model and a node renderer.


 * Tree model
 * The tree model defines the tree that the control must display. It only describes the structure and data contents of the tree; the actual presentation in the control is defined by the node renderer. The tree control interrogates the tree model about the parts of the tree that should be shown.  It also listens to notifications from the modes such that changes in the tree can be reflected on the display.  Several tree controls can be attached to a single tree model.  The notification strategy ensures that changes in the model are reflected in all attached tree controls.


 * Node renderer
 * The node renderer is responsible for rendering the nodes of the tree. More precisely the node renderer determines the text label, font, icon, etc. of each node.

Code overview
This section provides an overview of the most important parts of the involved code.

treeControl
The following code extract illustrates some of the most fundamental things about the treeControl

interface treeControl{@Node} supports control properties model : treeModel{@Node}. domains nodeRenderer = (@Node, treeNodeDC NodeDC). properties nodeRenderer : nodeRenderer. ...

First of all the control is parameterized over the @Node's it contains; where nodes are branches as well as leafs. It is attached to a model by setting the model property and to a node renderer by setting the nodeRenderer property.

Typically the treeControl is created inside the auto-generated generatedInitialize predicate and the model and node renderer is attached in the constructor after the call to generatedInitialize:

clauses new(Parent):- formWindow::new(Parent), generatedInitialize, % treeControl_ctl is created here treeControl_ctl:model := This, % This class implements the model treeControl_ctl:nodeRenderer := nodeRenderer, % the local predicate nodeRenderer handles the rendering ...

treeModel
The tree model describes the tree and sends notifications about changes in the tree. The part that describes the tree looks like this (condensed):

interface treeModel{@Node} predicates getRoot_nd : -> @Node Root nondeterm. getChild_nd : (@Node Parent) -> @Node Child nondeterm. hasChildren : (@Node Parent) determ. tryGetParent : (@Node Child) -> @Node Parent determ.

The tree model is also parameterized over the @Node</vp>'s. It has predicates for obtaining the roots, and the children and parent of a certain node. It is a fundamental property that the tree control asks about the nodes it needs to know about. The tree control is not loaded with everything; it only receive data by need (e.g. when the user expands a node).

The treeControl cashes data to a large extend: The first time a certain node is expanded the treeControl ask the tree model about the children of this node, but it does not ask again even if the node is collapsed and expanded again.

hasChildren</vp> is used by the treeControl</vp> to determine whether a node is a branch (which will have a "+" for expansion) or a leaf.

tryGetParent</vp> is used by the treeControl</vp> to find the parent of nodes that have never yet been rendered. For example, the treeControl</vp> has just been shown and only the roots have therefore been displayed. The program no asks to set focus to a certain node, which has not yet been displayed. The treeControl</vp> (repeatedly, if necessary) calls tryGetParent</vp> to establish the position of the node in the tree.

The notification part of the model looks like this (condensed):

interface treeModel{@Node} ... domains event = nodePropertyChanged(@Node UpdatedNode); % Only node property changed subTreeChanged(@Node ChangedTree); % Entire subtree changed branchChanged(@Node ChangedTree); % Immediate children changed toplevelChanged; % Only Toplevel has changed, children are assumed unaltered allChanged. % Entire forest has changed domains treeChangedListener = (event* Events). properties changedEvent : eventSource{event*} (o).

The treeControl</vp> attach a treeChangedListener</vp> and is therefore informed about various kinds of changes so that it can update the contents of the window accordingly.

You should notice that it is the user (=programmer) of the treeControl</vp> that provides and implements the treeModel</vp>, so it is the programmer that should fire these events to trigger update.

Examples below will illustrate this.

nodeRenderer
The nodeRenderer</vp> defines the graphical appearances of the nodes in the tree. For a certain node it must at least define the text label that will be displayed in the tree, but it can also control icons, fonts and text color.

The nodeRenderer</vp> is a predicate which receives a node and a tree node device context (treeNodeDC</vp>), and it must render the node to the device context.

The treeNodeDC</vp> looks like this:

interface treeNodeDC open vpiDomains properties text : string (i). bitmapIdx : integer (i). selectedBitmapIdx : integer (i). stateBitmapIdx : integer (i). font : font (i). textColor : color (i). backColor : color (i). end interface treeNodeDC

So when the <vp>nodeRenderer</vp> is called with a certain node, it must set the <vp>text</vp> property to the text that will be displayed for this node, and so forth. Bitmap handling will be discussed below.

Notice that the treeControl only ask the rendering predicate one time for each node, unless it receives a notification that makes the node invalid. The <vp>nodePropertyChanged</vp> event in particular means that the rendering of the node is invalid. If the node is in a changed subtree or branch it is also invalid and the <vp>nodeRenderer</vp> will be invoked again.

treeControlDemo
The treeControlDemo program that comes as one of the examples in Visual Prolog Commercial Edition contains four different usages of the <vp>treeControl</vp>

The standard model
The first two uses the "standard" model (<vp>treeModel_std</vp>), which is an off-the-self tree model for "small stable" trees. Using the standard model the entire tree is constructed before the treeControl is shown. This may be an easy choice for "small stable" trees, but it is actually not the recommended way to deal with trees, because it both requires that the entire tree is built in advance and that it stays unchanged while the treeControl exists.

When using the standard model <vp>@Node</vp> must be <vp>treeNode_std</vp>. When you add the control to the form you should also set the "@Node" property to "treeNode_std" and then the auto-generated code will look like this:

% This code is maintained automatically, do not update it manually. 09:52:43-7.1.2010 facts treeControl_ctl : treecontrol{treeNode_std}. ...

In Demo1 the tree is built from a functor structure <vp>Tree</vp>:

clauses new(Parent):- formWindow::new(Parent), generatedInitialize, treeControl_ctl:autofitContainer := true, % Make simple tree Tree = node("Root",               [node("Tree1", [leaf("Leaf1.1"), leaf("Leaf1.2"), leaf("Leaf1.3") ]),                   node("Tree2", [leaf("Leaf2.1"), leaf("Leaf2.2"), leaf("Leaf2.3") ]),                   node("Tree3", [leaf("Leaf3.1"), leaf("Leaf3.2"), leaf("Leaf3.3") ])               ]),        % Make a tree model and associate it to the TreeView control Model = treeModel_std::new([Tree]), treeControl_ctl:model := Model, treeControl_ctl:nodeRenderer := Model:nodeRenderer.

Initially the user will only see the collapsed root, "Root". The other nodes will appear as the user expands the tree.

In Demo2 the tree is constructed as a structure of <vp>treeNode_std</vp>-objects:

clauses new(Parent):- formWindow::new(Parent), generatedInitialize, treeControl_ctl:autofitContainer := true, setInitFocus(treeControl_ctl), % Make an object-based tree with additional visual properties Root=treeNode_std::new("Root"), Tree1=treeNode_std::new(Root,"Tree1"), Font=getFont, _=vpi::fontGetAttrs(Font,_,FontSize), NewFont=vpi::fontSetAttrs(Font,[fs_italic],FontSize), Leaf11=treeNode_std::new(Tree1,"Leaf1.1"), Leaf11:font := NewFont, Leaf12=treeNode_std::new(Tree1,"Leaf1.2"), Leaf12:font := NewFont, Leaf13=treeNode_std::new(Tree1,"Leaf1.3"), Leaf13:font := NewFont, Tree2=treeNode_std::new(Root,"Tree2"), Tree2:backColor := color_Azure, _Leaf21=treeNode_std::new(Tree2,"Leaf2.1"), _Leaf22=treeNode_std::new(Tree2,"Leaf2.2"), _Leaf23=treeNode_std::new(Tree2,"Leaf2.3"), Tree3=treeNode_std::new(Root,"Tree3"), Tree3:backColor := color_LightPink, Leaf31=treeNode_std::new(Tree3,"Leaf3.1"), Leaf31:textColor := color_DkGray, Leaf32=treeNode_std::new(Tree3,"Leaf3.2"), Leaf32:textColor := color_DarkCyan, Leaf33=treeNode_std::new(Tree3,"Leaf3.3"), Leaf33:textColor := color_DarkGreen, % Make a tree model TreeModel=treeModel_std::new, TreeModel:addTree(Root), % Ambient properties work as defaults for nodes without any specifics properties defined treeControl_ctl:imageList:=imageList::new(16,16), _ = treeControl_ctl:imageList:addImageFromfile(@"..\ressource\folder.bmp"), % Associate tree model with tree control treeControl_ctl:model := TreeModel, treeControl_ctl:nodeRenderer := TreeModel:nodeRenderer.

When constructing the nodes directly it is possible to control more rendering aspects of the tree: font, color, bitmap.

Custom models
Demo3 and Demo4 use custom models, which is the recommended strategy. Once you have learned how to do it, it is not more difficult than using the standard model, sometimes it is actually easier. Furthermore, you have the advantage of dynamic trees than can be displayed in several places at once, and that only nodes that need to be displayed are "calculated".

Demo3 and Demo4 both use the obvious tree that exists on any computer, the directory structure. It is quite clear that it would be rather inefficient to create the entire tree in advance using the standard model. That would require that you traversed the entire directory structure of the disk before you showed the tree. And in most situations the user will only look at a little subset of the nodes anyway.

So the "lazy" approach of the <vp>treeModel</vp> is essential in this context.

Demo3 illustrates a situation that it often the case in real applications. The "real" application works with directories (why else present them in a tree), so in the application we already have an object representation of directories, <vp>dirNode</vp>:

interface dirNode

predicates getSubDirectories_nd : -> dirNode nondeterm. hasSubDirectories : determ.

predicates getName : -> string Name. getFullName : -> string FullName. getParent : -> dirNode Parent. tryGetParent : -> dirNode Parent determ.

setName : (string NewName).

predicates addSubdirectory : -> dirNode. removeSubdirectory : (dirNode).

domains changeEvent = dirCreate(dirNode); dirDelete(dirNode); dirRename(dirNode).

domains changeListener = (changeEvent).

predicates addChangeListener : (changeListener). dispatchChange : (changeEvent). % Dispatches change to all registered listeners and thereafter to parent directory node

end interface dirNode

<vp>dirNode</vp>'s have as you can see many things that are very suitable for a <vp>treeModel</vp>, some of them have perhaps been added specifically to support <vp>treeModel</vp>. But it is worth noticing that the <vp>dirNode</vp> is not a just a <vp>treeModel</vp>-thing, it is actually a directory model. Meaning that the operations (and events) makes very good sense on (from) a directory also when it does not occur in any <vp>treeControl</vp>s.

The need for notifications often spread down to other kinds of data: To provide notifications for directory trees we need notifications from directories.

Notice, that <vp>setName</vp>, <vp>addSubdirectory</vp> and <vp>removeSubdirectory</vp> not only affect the structure of objects in the program, they update the physical directories on the disk.

In Demo3 we want to have <vp>dirNode</vp>'s in the tree. So in the dialog/form editor we write "dirNode" for the "@Node" property. As a result fact declaration the auto-generated code looks like this:

% This code is maintained automatically, do not update it manually. 09:51:09-7.1.2010 facts treeControl_ctl : treecontrol{dirNode}.

Given this the tree model must be an object of type <vp>treeModel{dirNode}</vp>. The model is implemented by the class <vp>dirTreeModel</vp>

interface dirTreeModel supports treeModel{dirNode} end interface dirTreeModel

class dirTreeModel : dirTreeModel predicates get : -> dirTreeModel. end class dirTreeModel

It is important to call the <vp>get</vp> predicate instead of calling the constructor, because the <vp>get</vp> predicate returns the same object whenever it is called so that a single tree model is shared between all <vp>treeControl</vp>'s. The <vp>get</vp> predicate implements a so called singleton (see Singleton pattern):

implement dirTreeModel ... class facts model_fact : (dirTreeModel) determ.

clauses get=Model:- model_fact(Model), !.   get=Model:- Model=new, assert(model_fact(Model)).

The <vp>dirTreeModel</vp> model inherits from the support class <vp>treeModel</vp> instantiated to the appropriate type:

implement dirTreeModel inherits treeModel{dirNode} ...

This gives us the changedEvent property without more work.

We store the root in a fact and delegate all model interrogation to the relevant <vp>dirNode</vp>:

implement dirTreeModel ... facts - node_facts root_fact : (dirNode) determ.

clauses getRoot_nd=DirRoot:- root_fact(DirRoot).

clauses getChild_nd(ParentNode) = ParentNode:getSubDirectories_nd.

clauses tryGetParent(ChildNode) = ChildNode:tryGetParent.

We must also transform the <vp>dirNode</vp> events into appropriate <vp>treeModel</vp> events. This is done by the <vp>onChange</vp> which is a <vp>dirNode::changeListener</vp>:

implement dirTreeModel ... predicates onChange : dirNode::changeListener. clauses onChange(dirNode::dirCreate(NewDir)):- ParentDir=NewDir:getParent, changedEvent:notify([subtreeChanged(ParentDir)]).

onChange(dirNode::dirDelete(OldDir)):- ParentDir=OldDir:getParent, changedEvent:notify([subtreeChanged(ParentDir)]).

onChange(dirNode::dirRename(Dir)):- Path=Dir:getFullName, changedEvent:notify([nodePropertyChanged(Dir)]).

The only thing that remains is the creation of the object where we obtain and store the root and attach <vp>onChange</vp> to it:

implement dirTreeModel ... clauses new:- DirNode=dirNode::getRoot, assert(root_fact(DirNode)), DirNode:addChangeListener(onChange).

Demo4 illustrates how to implement a tree model directly in the form code. Again the tree shows the disk, but this time we will use a really lightweight node type. Each node must uniquely represent a directory, we have chosen the full path of the directory (using upper/lower case letters as they are found on the disk).

So in Demo4 <vp>@Node</vp> is <vp>string</vp>, and the essential model parts looks like this:

implement demo4 inherits formWindow, treeModel{string} ... clauses new(Parent):- formWindow::new(Parent), generatedInitialize, ...       treeControl_ctl:model := This, ...

clauses getRoot_nd = fileName::removeLastSlash(Drive) :- fileName::getFirstDirectory(directory::getCurrentDirectory, Drive, _).

clauses getChild_nd(Path) = directory::getSubDirectories_nd(Path).

clauses tryGetParent(Path) = fileName::removeLastSlash(Parent) :- Last = filename::getLastDirectory(Path, Parent), "" <> Last.

...

% This code is maintained automatically, do not update it manually. 09:51:44-7.1.2010 facts treeControl_ctl : treecontrol{string}. ...

It is <vp>This</vp> that implements the model. Again we inherit the <vp>changeEvent</vp> stuff from a suitable instance of <vp>treeModel</vp>; in Demo4 we will not utilize the change event mechanism, but since it is part of the model we have to provide an implementation. The model interrogation predicates are implemented as simple directory operations.

The <vp>onShow</vp> predicate nicely illustrates that the programmer uses path's (i.e. nodes) when they manipulate the control. We select a directory by selecting its node, which is the full path of the directory:

implement demo4 ... predicates onShow : window::showListener. clauses onShow(_Source, _Data) :- Dir = directory::getCurrentDirectory, treeControl_ctl:select(Dir).

In this representation it is important that we use correct upper-lower case letters.

Node Rendering
The standard models also provides node rendering, which is controllable through properties in the node objects. When initializing a standard model from a functor structure the rendering cannot be controlled.

Demo3 and Demo4 illustrate how node rendering is done in when using a custom model. As mentioned above the node node renderer receives a node and a <vp>treeNodeDC</vp> and it must then render the node into the <vp>treeNodeDC</vp>.

The node renderer in Demo3 is completely trivial:

predicates nodeRenderer : nodeRenderer. clauses nodeRenderer(Node, NodeDC) :- NodeDC:text := Node:getName.

It simply sets the label text of the node. Everything else will be handled by the default mechanisms, which means that font will be the one used in the dialog/form and the colors will be "standard" typically black on white (but may be influenced by theme and other stuff). The bitmap defaults to the first bitmap.

The node rendering in Demo4 is more interesting. Bitmaps are handled through <vp>imageList</vp>'s. The <vp>imageList</vp>'s are created and attached in the constructor:

clauses new(Parent):- formWindow::new(Parent), generatedInitialize, treeControl_ctl:imageList:= imageList::new(16, 16), treeControl_ctl:stateImageList := imageList::new(7, 12), _ = treeControl_ctl:stateImageList:addImageFromfile(@"..\ressource\folder.bmp"), % dummy to receive index 0, since 0 means no bitmap treeControl_ctl:autofitContainer := true, treeControl_ctl:model := This, treeControl_ctl:nodeRenderer := nodeRenderer.

Again the label text is set, but here we also randomly set state bitmaps and use other colors. The state bitmap is a bitmap that is placed to the right of the real bitmap to indicate it state of the node, if it is set to 0 no state bitmap will be shown. We have chosen to use an empty bit map to represent "nothing" because removing the bitmap will cause nodes to "jump in an out".

predicates nodeRenderer : treeControl{string}::nodeRenderer. clauses nodeRenderer(Path, NodeDC) :- NodeDC:text := getLabel(Path), NodeDC:bitmapIdx := treeControl_ctl:imageList:addImageFromfile(@"..\ressource\folder.bmp"), if math::random > 0.80 then NodeDC:stateBitmapIdx := treeControl_ctl:stateImageList:addImageFromfile(@"..\ressource\lock.bmp") else NodeDC:stateBitmapIdx := treeControl_ctl:stateImageList:addImageFromfile(@"..\ressource\blank.bmp") end if, if math::random > 0.80 then NodeDC:textColor := color_DarkGreen, NodeDC:backColor := color_AliceBlue end if.

Context Menu
Demo3 also illustrates how to setup a context menu and how to handle the menu events.

A context menu should present some of the interesting menu entries from the main menu. Meaning that it should not introduce menu entries that are not in the main menu.

For simplicity of the example the context menu in Demo3 violates this guideline completely. You should imagine that the <vp>menuTag</vp>'s in Demo3 was actually tags from the main menu:

implement demo3 ... constants menu_new_subdir : menuTag = 10000. menu_del_subdir : menuTag = 10001. menu_rename_subdir : menuTag = 10002. menu_explore_subdir : menuTag = 10003.

We attach menu item listeners to the form:

implement demo3 ... clauses new(Parent, Node) :- formWindow::new(Parent), generatedInitialize, ...       % Add menu actions addMenuItemListener(menu_new_subdir, onMenuNewSubdir), addMenuItemListener(menu_del_subdir, onMenuDelSubdir), addMenuItemListener(menu_rename_subdir, onMenuRenameSubdir), addMenuItemListener(menu_explore_subdir, onMenuExploreSubdir), ...

As example <vp>onMenuNewSubdir</vp> will create a new sub directory to the selected directory and start editing of that node:

implement demo3 ... predicates onMenuNewSubdir : menuItemListener. clauses onMenuNewSubdir(_, _) :- Node = treeControl_ctl:trygetSelected, !,       Path = Node:getFullName, write("Creating new subdirectory of ", Path, "..."), nl, NewNode = Node:addSubdirectory, treeControl_ctl:nodeEdit(NewNode).

onMenuNewSubdir(_, _).

The actual creation of the pop-up menu is handled in the context menu handler of the tree control:

implement demo3 ... clauses new(Parent, Node) :- formWindow::new(Parent), generatedInitialize, ...       treeControl_ctl:setContextMenuResponder(onContextMenu), ...

The context menu handler must deal mouse invocation as well as keyboard invocation:

implement demo3 ... predicates onContextMenu : window::contextMenuResponder. clauses onContextMenu(_, mouse(Pnt)) = contextMenuHandled:- Node = treeControl_ctl:tryGetNodeAtPosition(Pnt), !,       treeControl_ctl:select(Node), showPopup(Pnt). onContextMenu(_, keyboard) = contextMenuHandled:- Node = treeControl_ctl:tryGetSelected, Rct = treeControl_ctl:tryGetNodeRect(Node, onlyText), Rct = rct(X1, Y1, _, Y2), Pnt = pnt(X1, Y1 + (Y2-Y1) div 2), !,       showPopup(Pnt). onContextMenu(_, _) = defaultContextMenuHandling.

predicates showPopup : (pnt Pnt). clauses showPopup(PntTree) :- PntForm = treeControl_ctl:mapPointTo(This, PntTree), menuPopup(dynmenu( [txt(menu_explore_subdir, "Explore in New Window", noAccelerator, b_true, mis_none, []), separator, txt(menu_new_subdir, "New Subdirectory", noAccelerator, b_true, mis_none, []), txt(menu_del_subdir, "Delete Directory", noAccelerator, b_true, mis_none, []), txt(menu_rename_subdir, "Rename Directory...", noAccelerator, b_true, mis_none, []) ]), PntForm, align_left).