TreeControl

From wiki.visual-prolog.com

Revision as of 10:45, 28 October 2013 by Thomas Linder Puls (talk | contribs) (Category)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Demo4 with locks and colors

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'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 whether 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 has not yet been displayed. The treeControl (repeatedly, if necessary) calls tryGetParent 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 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 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 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 built 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"),
        % 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 treeModel 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, 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. 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 treeModel{dirNode}. The model is implemented by the class dirTreeModel

interface dirTreeModel supports treeModel{dirNode}
end interface dirTreeModel
 
class dirTreeModel : dirTreeModel
predicates
    get : () -> dirTreeModel.
end class dirTreeModel

It is important to call the get predicate instead of calling the constructor, because the get predicate returns the same object whenever it is called so that a single tree model is shared between all treeControl's. The get predicate implements a so called singleton (see wikipedia: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 dirTreeModel model inherits from the support class treeModel 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 dirNode:

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 dirNode events into appropriate treeModel events. This is done by the onChange which is a dirNode::changeListener:

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 onChange 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 @Node is string, 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 This that implements the model. Again we inherit the changeEvent stuff from a suitable instance of treeModel; 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 onShow 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 treeNodeDC and it must then render the node into the treeNodeDC.

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 imageList's. The imageList'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 menuTag'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 onMenuNewSubdir 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).