[wxPython] MVCTree info

Hi all, here's some introductory material for wxMVCTree I put together last
night. It's quite rough and probably doesn't answer all your questions, but
I'll refine and expand it this weekend, as well as incorporate the answers
to any questions I get between now and then.

wxMVCTree is a control which handles hierarchical data. It is constructed in
model-view-controller architecture, so the display of that data, and the
content of the data can be changed greatly without affecting the other
parts.

Model-View-Controller means that there are three parts of the control, which
handle orthogonal duties.

The Model is the actual data, either data which is inherently hierarchical
or which can be presented as such. A file system or a W3C DOM model of
document content are good examples. Note that the data does not have to be
customized in any way to work with wxMVCTree. Rather you write a wxTreeModel
subclass which tells the wxMVCTree how to access and make sense of your
data. By way of contrast, wxTree could be said to maintain its own internal
model, which it lets you populate one piece at a time.

The View is the presentation layer, what the user sees and interacts with.
Why is this important? Because the data can be presented many different
ways. How could you do a tree view differently? Consider a genealogy chart,
as it appears in history books. This is a tree structure, but you just can't
make a wxTree do this trick, it's hard-wired for one kind of view. For an
even more radical tree view, see
http://starship.python.net/crew/xoltar/images/pathfinder.gif for an example.
This is unfortunately proprietary code that I can't release presently, but
it's just an MVCTree with a custom view.

The Controller binds the Model and the View together, and handles a lot of
the bookkeeping and delegating.

wxMVCTree actually is even more configurable than MVC normally implies,
because almost every aspect of it is pluggable.
    wxMVCTree - Overall controller, and the window that actually gets placed
    in the GUI.
        Painter - Paints the control. The 'view' part of MVC.
           NodePainter - Paints just the nodes
           LinePainter - Paints just the lines between the nodes
           TextConverter - Figures out what text to print for each node
        Editor - Edits the contents of a node, if the model is editable.
        LayoutEngine - Determines initial placement of nodes
        Transform - Adjusts positions of nodes for movement or special
effects.
        TreeModel - Contains the data which the rest of the control acts
        on. The 'model' part of MVC.

Let's consider the wxMVCTree demo code. It presents a window with a
wxMVCTree which takes the filesystem as its model.

def selchanging(evt):
    logger.write("SelChanging!\n")

#similar functions omitted...

def runTest(frame, nb, log):
    global logger
    logger = log
    p = wxMVCTree(nb, -1)
    #Tell wxMVCTree to treat every node as a node with children (a 'folder')
    #and only convert it to a leaf when the user opens it and discovers
there's
    #nothing there.
    p.SetAssumeChildren(true)
    #We tell the wxMVCTree to use the filesystem as its model, starting at
the
    #Python directory.
    p.SetModel(LateFSTreeModel(os.path.normpath(os.getcwd() + os.sep
+'..')))
    #Uncomment this to enable live filename editing!
# p.AddEditor(FileEditor(p))
    p.SetMultiSelect(true)

    #Hook up our event logging for the demo..
    EVT_MVCTREE_SEL_CHANGING(p, p.GetId(), selchanging)
    #More event funcs omitted...
    return p

Almost nothing there for all the functionality we got. LateFSTeeModel is
where the action is happening, and it works the same as any other
wxTreeModel. Here's wxTreeModel in its entirety:
class wxTreeModel:
    """
    Interface for tree models
    """
    def GetRoot(self):
        raise NotImplementedError
    def SetRoot(self, root):
        raise NotImplementedError
    def GetChildCount(self, node):
        raise NotImplementedError
    def GetChildAt(self, node, index):
        raise NotImplementedError
    def GetParent(self, node):
        raise NotImplementedError
    def AddChild(self, parent, child):
        if hasattr(self, 'tree') and self.tree:
            self.tree.NodeAdded(parent, child)
    def RemoveNode(self, child):
        if hasattr(self, 'tree') and self.tree:
            self.tree.NodeRemoved(child)
    def InsertChild(self, parent, child, index):
        if hasattr(self, 'tree') and self.tree:
            self.tree.NodeInserted(parent, child, index)
    def IsLeaf(self, node):
        raise NotImplementedError

    def IsEditable(self, node):
        return false

    def SetEditable(self, node):
        return false

So when wxMVCTree needs to paint, it calls model.GetRoot() to get the first
data object. Then it calls model.GetChildCount() to find out whether this is
a leaf or a branch node. If it's a branch node, it calls model.GetChildAt()
once for each child node, and the process continues. Let's see how
LateFSTreeModel implements these. LateFSTreeModel derives from FSTreeModel,
which derives from BasicTreeModel. Many tree models will descend from
BasicTreeModel, as it does a lot of the work for you. Here are the relevant
parts.

class BasicTreeModel(wxTreeModel):
    """
    A very simple treemodel implementation, but flexible enough for many
needs.
    """
    def __init__(self):
        self.children = {}
        self.parents = {}
        self.root = None
    def GetRoot(self):
        return self.root
    def SetRoot(self, root):
        self.root = root
    def GetChildCount(self, node):
        if self.children.has_key(node):
            return len(self.children[node])
        else:
            return 0
    def GetChildAt(self, node, index):
        return self.children[node][index]

Simple enough. Now FSTreeModel customizes this to deal with a filesystem
specifically. When created, it takes a file path and builds its model from
that.

    def __init__(self, path):
        BasicTreeModel.__init__(self)
        import string
        fw = FileWrapper(path, string.split(path, os.sep)[-1])
        self._Build(path, fw)
        self.SetRoot(fw)
        self._editable = true
    def _Build(self, path, fileWrapper):
        for name in os.listdir(path):
            fw = FileWrapper(path, name)
            self.AddChild(fileWrapper, fw)
            childName = path + os.sep + name
            if os.path.isdir(childName):
                self._Build(childName, fw)

LateFSTreeModel, on the other hand, doesn't load the whole FS at once, it
waits until the user opens a folder before it bothers to load that part of
the tree. Here's how it differs:

    def _Build(self, path, parent):
        ppath = parent.path + os.sep + parent.fileName
        if not os.path.isdir(ppath):
            return
        for name in os.listdir(ppath):
            fw = FileWrapper(ppath, name)
            self.AddChild(parent, fw)
    def GetChildCount(self, node):
        if self.children.has_key(node):
            return FSTreeModel.GetChildCount(self, node)
        else:
            self._Build(node.path, node)
            return FSTreeModel.GetChildCount(self, node)

    def IsLeaf(self, node):
        return not os.path.isdir(node.path + os.sep + node.fileName)

Note how _Build is called from GetChildCount and not from the __init__
method. It is possible to have an even cleaner model that creates fewer
objects (all these FileWrappers do take space after all), but for most
models starting with BasicTreeModel will get you going quickly.

Custom Views can change the presentation of the data completely. For most
applications you don't need an entire view change, though, you just want the
regular tree, but with icons for instance. the View in an wxMVCTree control
is represented by a subclass of the Painter class. The standard view for
wxMVCTree is called TreePainter, and it presents the view of the data that
you see in the wxPython demo. TreePainter actually delegates its painting to
other classes, namely NodePainter and LinePainter... By deriving a subclass
of one of these, you could change the presentation of the nodes or the lines
connecting them without having to write a whole new view class, thereby
avoiding a lot of duplicated work in screen geometry, etc.

If you want to be able to edit the contents of the tree, you need an Editor
subclass which understands how to manipulate the data in your model. Once
you instantiate the Editor, you call AddEditor on the wxMVCTree control to
register it for use. When the user signifies (through clicking) that he/she
wants to edit a node, the tree obtains the data object for the node the user
clicked on, and calls CanEdit on each of the registered editors to determine
if the editor can edit this kind of data. If it returns true, then the tree
calls the Edit method, which places a control on the tree to handle user
input. Consider the FileEditor that lets us change filenames in the demo
code above:

class FileEditor(Editor):
    def Edit(self, node):
        treenode = self.tree.nodemap[node]
        self.editcomp = wxTextCtrl(self.tree, -1)
        for rect in self.tree.painter.rectangles:
            if rect[0] == treenode:
                self.editcomp.SetPosition((rect[1][0], rect[1][1]))
                break
        self.editcomp.SetValue(node.fileName)
        self.editcomp.SetSelection(0, len(node.fileName))
        self.editcomp.SetFocus()
        self.treenode = treenode
        EVT_KEY_UP(self.editcomp, self._key)
        EVT_LEFT_DOWN(self.editcomp, self._mdown)
        self.editcomp.CaptureMouse()

    def CanEdit(self, node):
        return isinstance(node, FileWrapper)

    def EndEdit(self, commit):
        if not self.tree._EditEnding(self.treenode.data):
            return
        if commit:
            node = self.treenode.data
            try:
                os.rename(node.path + os.sep + node.fileName, node.path +
os.sep + self.editcomp.GetValue())
                node.fileName = self.editcomp.GetValue()
            except:
                traceback.print_exc()
        self.editcomp.ReleaseMouse()
        self.editcomp.Destroy()
        del self.editcomp
        self.tree.Refresh()

    def _key(self, evt):
        if evt.KeyCode() == WXK_RETURN:
            self.EndEdit(true)
        elif evt.KeyCode() == WXK_ESCAPE:
            self.EndEdit(false)
        else:
            evt.Skip()

    def _mdown(self, evt):
        if evt.IsButton():
            pos = evt.GetPosition()
            edsize = self.editcomp.GetSize()
            if pos.x < 0 or pos.y < 0 or pos.x > edsize.width or pos.y >
edsize.height:
                self.EndEdit(false)

ยทยทยท

-----Original Message-----
From: Sebastien Pierre [SMTP:spierre@rational.com]
Sent: Tuesday, July 25, 2000 10:39 AM
To: wxpython-users@wxwindows.org
Subject: Re: [wxPython] MVCTree info

brk@jenkon.com wrote:
> I'm the author of wxMVCTree, though I haven't looked at it
> some time. I'll try to put together some documentation tonight for
inclusion
> in the next version, and I'll post a copy here.

Wow that's pretty cool :wink:
BTW, I've noticed a flickering [when scolling]of the wxMVCTree when used
in a window
with scrollbars under Linux (not tested under Windows) .
This can be really annoying, and I guess it would bring a lot if fixed !
This is "just" a double-buffering trick I think!

PS:Do not forget to put some examples in your documentation !

Thank you for being so quick!
Cheers,
Seb.

--
Sebastien Pierre.- spierre@rational.com \ Bored of Notepad,
R a t i o n a l software | XML \ WordPad or UltraEdit ?
. Rational Unified Process | technology \ try Vim !
. Vancouver, B.C, -Canada | intern \ <http://www.vim.org>

_______________________________________________
wxPython-users mailing list wxPython-users@wxwindows.org
http://wxwindows.org/mailman/listinfo/wxpython-users