TreeControl
The treeControl (introduced in Visual Prolog 7.3 Commercial Edition) 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.
Basic concepts
To use the tree cotrol 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'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 is used by the treeControl to determine wheter a node is a branch (which will have a "+" for expansion) or a leaf.
tryGetParent is used by the treeControl to find the parent of nodes that have never yet been rendered. For example, the treeControl has just been shown and only the roots have therefore been displayed. The program no asks to set focus to a certain node, which have not yet been displayed. The treeControl (repeatedly, if necessary) calls tryGetParent to establich 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 attach a treeChangedListener 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 that provides and implements the treeModel, so it is the programmer that should fire these events to trigger update.
Examples below will illustrate this.
nodeRenderer
The nodeRenderer defines the graphical appearences 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 is a predicate which receives a node and a tree node device context (treeNodeDC), and it must render the node to the device context.
The treeNodeDC 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 nodeRenderer is called with a certain node, it must set the text 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 nodePropertyChanged 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 nodeRenderer 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 treeControl
The standard model
The first two uses the "standard" model (treeModel_std), 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 build in advance and that it stays unchanged while the treeControl exists.
When using the standard model @Node must be treeNode_std. 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 Tree:
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 treeNode_std-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"), % Assosiate 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 uses custom models, which 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 needs to be displayed are interrogated.
Demo3 and Demo4 both uses the obovious 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 treeModel is essential in this context.
Demo3 illustrates (academically, of course) 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, dirNode:
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
dirNode's have as you can see many things that are very suitable for a treeModel, some of them have perhaps been added specifically to support treeModel. But it is worth noticing that the dirNode is not a just a treeModel-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 treeControls.
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 setName, addSubdirectory and removeSubdirectory not only affect the structure of objects in the program, they update the physical directories on the disk.
In Demo3 we want to have dirNode's in the tree.