Tree Controls

wxTreeCtrl Lazy evaluation (using expansion/scrolling events to load only the visible elements into the controls) and other stuff(look through the page)

See also:

Lazy Evaluation Tree Controls

   1 import wx, random
   2 
   3 
   4 class LazyTree(wx.TreeCtrl):
   5     ''' LazyTree is a simple "Lazy Evaluation" tree, that is, it only adds 
   6         items to the tree view when they are needed.'''
   7 
   8     def __init__(self, *args, **kwargs):
   9         super(LazyTree, self).__init__(*args, **kwargs)
  10         self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.OnExpandItem)
  11         self.Bind(wx.EVT_TREE_ITEM_COLLAPSING, self.OnCollapseItem)
  12         self.__collapsing = False
  13         root = self.AddRoot('root')
  14         self.SetItemHasChildren(root)
  15 
  16     def OnExpandItem(self, event):
  17         # Add a random number of children and randomly decide which 
  18         # children have children of their own.
  19         nrChildren = random.randint(1, 6)
  20         for childIndex in range(nrChildren):
  21             child = self.AppendItem(event.GetItem(), 'child %d'%childIndex)
  22             self.SetItemHasChildren(child, random.choice([True, False]))
  23 
  24     def OnCollapseItem(self, event):
  25         # Be prepared, self.CollapseAndReset below may cause
  26         # another wx.EVT_TREE_ITEM_COLLAPSING event being triggered.
  27         if self.__collapsing:
  28             event.Veto()
  29         else:
  30             self.__collapsing = True
  31             item = event.GetItem()
  32             self.CollapseAndReset(item)
  33             self.SetItemHasChildren(item)
  34             self.__collapsing = False
  35 
  36 
  37 class LazyTreeFrame(wx.Frame):
  38     def __init__(self, *args, **kwargs):
  39         super(LazyTreeFrame, self).__init__(*args, **kwargs)
  40         self.__tree = LazyTree(self)
  41 
  42 
  43 if __name__ == "__main__":
  44     app = wx.App(False)
  45     frame = LazyTreeFrame(None)
  46     frame.Show()
  47     app.MainLoop()

Lazy Evaluation Directory Browser Example

This is a demonstration of maintaining a directory listing tree. It uses lazy evaluation to update itself when a directory is expanded out. Doing a full evaluation on application startup takes 20+ seconds on my computer, whereas this is nearly instantaneous and minimal. I will update this code as I refine it, remove some of the kludges, and refactor it in a nicer way.

I made the code as verbose and explanatory as possible. The only thing a beginning wxPython developer might wonder about is the tree.GetPyData and tree.SetPyData functions. These let you associate an arbitrary python object with the item ID of a wxPython object. In this program the object is a 2-tuple containing the full directory path and a boolean indicator of whether that entry has been crawled and expanded before. A example call is:

# store the path information and that it has been previously expanded
tree.SetPyData(rootID, ('c:\\', True))

Anyway, on to the sample application:

import wx
import os.path, dircache

class MyFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        # this is just setup boilerplate
        kwds["style"] = wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)
        
        # our tree object, self.tree
        self.tree = wx.TreeCtrl(self, -1, style=wx.TR_HAS_BUTTONS|wx.TR_DEFAULT_STYLE|wx.SUNKEN_BORDER)

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.tree, 1, wx.EXPAND, 0)
        self.SetAutoLayout(True)
        self.SetSizer(sizer)
        sizer.Fit(self)
        sizer.SetSizeHints(self)
        self.Layout()

        # register the self.onExpand function to be called
        wx.EVT_TREE_ITEM_EXPANDING(self.tree, self.tree.GetId(), self.onExpand)
        # initialize the tree
        self.buildTree('/')

    def onExpand(self, event):
        '''onExpand is called when the user expands a node on the tree
        object. It checks whether the node has been previously expanded. If
        not, the extendTree function is called to build out the node, which
        is then marked as expanded.'''
        
        # get the wxID of the entry to expand and check it's validity
        itemID = event.GetItem()
        if not itemID.IsOk():
            itemID = self.tree.GetSelection()
        
        # only build that tree if not previously expanded
        old_pydata = self.tree.GetPyData(itemID)
        if old_pydata[1] == False:
            # clean the subtree and rebuild it
            self.tree.DeleteChildren(itemID)
            self.extendTree(itemID)
            self.tree.SetPyData(itemID,(old_pydata[0], True))

    def buildTree(self, rootdir):
        '''Add a new root element and then its children'''        
        self.rootID = self.tree.AddRoot(rootdir)
        self.tree.SetPyData(self.rootID, (rootdir,1))
        self.extendTree(self.rootID)
        self.tree.Expand(self.rootID)
        
    def extendTree(self, parentID):
        '''extendTree is a semi-lazy directory tree builder. It takes
        the ID of a tree entry and fills in the tree with its child
        subdirectories and their children - updating 2 layers of the
        tree. This function is called by buildTree and onExpand methods'''
        
        # This is something to work around, because Windows will list
        # this directory but throw a WindowsError exception if you
        # try to use the listdir() command on it. I need a better workaround
        # for this...this is a temporary kludge.
        excludeDirs=["c:\\System Volume Information","/System Volume Information"]
        
        # retrieve the associated absolute path of the parent
        parentDir = self.tree.GetPyData(parentID)[0]
        
        
        subdirs = dircache.listdir(parentDir)
        subdirs.sort()
        for child in subdirs:
            child_path = os.path.join(parentDir,child)
            if os.path.isdir(child_path) and not os.path.islink(child):
                if child_path in excludeDirs:
                    continue                
                # add the child to the parent
                childID = self.tree.AppendItem(parentID, child)
                # associate the full child path with its tree entry
                self.tree.SetPyData(childID, (child_path, False))
                
                # Now the child entry will show up, but it current has no
                # known children of its own and will not have a '+' showing
                # that it can be expanded to step further down the tree.
                # Solution is to go ahead and register the child's children,
                # meaning the grandchildren of the original parent
                newParent = child
                newParentID = childID
                newParentPath = child_path
                newsubdirs = dircache.listdir(newParentPath)
                newsubdirs.sort()
                for grandchild in newsubdirs:
                    grandchild_path = os.path.join(newParentPath,grandchild)
                    if os.path.isdir(grandchild_path) and not os.path.islink(grandchild_path):                        
                        grandchildID = self.tree.AppendItem(newParentID, grandchild)
                        self.tree.SetPyData(grandchildID, (grandchild_path,False))
                        
if __name__ == "__main__":
    app = wx.PySimpleApp(0)
    wx.InitAllImageHandlers()
    frame = MyFrame(None, -1, "")
    app.SetTopWindow(frame)
    frame.Show()
    app.MainLoop()

-- Carl Scharenberg (carlscharenberg@yahoo.com)

Recursively building a directory list into a wxTreeCtrl

This is a series of functions that I wrote for my mp3 player program to import items into a wxTreeCtrl as a folder tree of sorts, it preserves folder structure and places the items in both a list and a dictionary (note, the mp3 player specific stuff still remains, but you should be able to get the gist of the functions from it)

   1     def FMNUBuildFolder(self, event):
   2 
   3         dlg = wxDirDialog(self)
   4 
   5         try:
   6             if dlg.ShowModal() == wxID_OK:
   7                 dir = dlg.GetPath()
   8                 self.root = ""
   9                 self.list.DeleteAllItems()
  10                 self.tree.DeleteAllItems()
  11                 self.musiclist = []
  12                 self.musiclistdict = {}
  13                 self.nummp3s = 0
  14                 self.numparsed = 0
  15                 self.StartBuildFromDir(dir)
  16                 self.BuildMusicDict()
  17                 self.ParseMusicList()
  18                 self.tree.Expand(self.root)
  19         finally:
  20             dlg.Destroy()
  21 
  22     def StartBuildFromDir(self, dir):
  23         rootname = os.path.split(dir)
  24         self.root = self.tree.AddRoot(rootname[1])
  25         self.rootdir = dir
  26         self.BuildChildrenFromDir(self.root, dir)
  27 
  28     def BuildChildrenFromDir(self, parent, dir):
  29         dirlisting = os.listdir(dir)
  30         for listing in dirlisting:
  31             pathinquestion = os.path.join(dir, listing)
  32             if os.path.isfile(pathinquestion):
  33                 extension = os.path.splitext(pathinquestion)
  34                 extension = extension[1]
  35                 if extension == ".mp3":
  36                     child = self.tree.AppendItem(parent, listing)
  37                     childdata = self.tree.GetItemData(child)
  38                     childdata.path = pathinquestion
  39                     id3info = ID3(pathinquestion)
  40                     mp3info = [id3info.get('TITLE', ""),
  41                                id3info.get('ARTIST', ""),
  42                                id3info.get('GENRE', ""),
  43                                id3info.get('YEAR', ""),
  44                                id3info.get('ALBUM', ""),
  45                                id3info.get('TRACKNUMBER', ""),
  46                                pathinquestion]
  47                     self.musiclist.append(mp3info)
  48                     self.nummp3s += 1
  49             elif os.path.isdir(pathinquestion):
  50                 newparent = self.tree.AppendItem(parent, listing)
  51                 newdir = os.path.join(dir, listing)
  52                 self.BuildChildrenFromDir(newparent, newdir)
  53 
  54     def ParseMusicList(self):
  55         items = self.musiclistdict.items()
  56         for i, x in enumerate(items):
  57             key, data = x
  58             self.list.InsertStringItem(i, data[0])
  59             self.list.SetStringItem(i, 0, data[0])
  60             self.list.SetStringItem(i, 1, data[1])
  61             self.list.SetStringItem(i, 2, data[2])
  62             self.list.SetStringItem(i, 3, data[3])
  63             self.list.SetStringItem(i, 4, data[4])
  64             self.list.SetStringItem(i, 5, data[5])
  65             self.list.SetStringItem(i, 6, data[6])
  66             self.list.SetItemData(i, key)
  67 
  68     def BuildMusicDict(self):
  69         key = 0
  70         for mp3listing in self.musiclist:
  71             key += 1
  72             newmp3listing = tuple(mp3listing)
  73             self.musiclistdict[key] = newmp3listing
  74         self.itemDataMap = self.musiclistdict
  75         wxColumnSorterMixin.__init__(self, 7)

- Jeff Peck (Sanguinus@earthlink.net)

Building a tree non-recursively

Here's another approach which doesn't use a recursively function. For large trees you probably wants to add logic which removes items that no longer will be used from the ids dict. But it works nicely for a small directory structure. Don't forget to import os.path.

   1 root = '/path/to/root'
   2 ids = {root : tree.AddRoot(root)}
   3 for (dirpath, dirnames, filenames) in os.walk(root):
   4     for dirname in dirnames:
   5         fullpath = os.path.join(dirpath, dirname)
   6         ids[fullpath] = tree.AppendItem(ids[dirpath], dirname)
   7     for filename in sorted(filenames):
   8         tree.AppendItem(ids[dirpath], filename)

- Örjan Persson

Comments...

I will add a example for dragging items in a wxTreeCtrl and what is more interresting how to drag a branch in a wxTreeCtrl. I'll try my best but i'm a german kid and my english (espacially my grammar) is horrible. So i'll be very lucky if someone would parse my written lines after adding here and translate it in -real- english.

--René Freund

Drag and Drop with a wxTreeCtrl

Here's something that messes with drag-and-drop for a wxTreeCtrl. It allows the drag of "Series" nodes onto "Collection" nodes only and the drag of "Collection" nodes onto "Series" nodes only. It implements the DropSource's GiveFeedback method to provide the user with information about whether or not there is a legal drop at the cursor's current position.

NOTE: This code only works on Windows under wxPython 2.4.1.2. I see three issues when I try to run it on the Mac OS-X version of wxPython:

1. In the OnBeginDrag method, the line:

does not work because of a problem with event.GetPoint(). This can be replaced with the following:

(windowx, windowy) = wx.wxGetMousePosition() # Translate the Mouse's Screen Position to the Mouse's Control Position (x, y) = self.tree.ScreenToClientXY(windowx, windowy) # Now use the tree's HitTest method to find out about the potential drop target for the current mouse position (sel_item, flags) = self.tree.HitTest((x, y))}}}

2. The DropSource GiveFeedback() method only gets called once, rather than repeatedly when the mouse moves. This means that feedback is not given as it should be.

3. If the DropTarget's OnData() method vetos the data drop, that veto is not passed back to the originating OnBeginDrag method, which is therefore unaware that the drag has been vetoed.

See also: TreeCtrlDnD which does work under something other than Windows.

   1 # This example is provided with no promises or warranties regarding accuracy or suitability for any purpose whatsoever.
   2 
   3 """ -- DragandDrop.pyw --
   4     A sample program demonstrating a Drag and Drop implementation within a wxTreeCtrl.
   5     The wxTreeCrtl shows two types of nodes, Series nodes and Collection nodes.
   6     This implementation allows users to drag Series nodes onto Collection nodes and
   7     Collection nodes onto Series nodes, but it blocks dragging nodes onto the same types.
   8     In my application, dragging certain types of nodes onto other types of nodes is
   9     often a meaningful action.  It does not always change the tree structure, but it
  10     often causes the copying or movement of data behind the scenes.  """
  11 
  12 
  13 __author__ = 'David K. Woods, Ph.D., Wisconsin Center for Education Research, University of Wisconsin, Madison, dwoods at wcer dot wisc dot edu'
  14 
  15 # Import wxPython
  16 from wxPython import wx
  17 # use the fast cPickle tool instead of the regular Pickle
  18 import cPickle
  19 
  20 
  21 # Declare the class for the main application Dialog Window
  22 class MainWindow(wx.wxDialog):
  23    """ This window displays a Tree Control that processes drag-and-drop activities. """
  24    def __init__(self,parent,id,title):
  25       # Create a Dialog Box
  26       wx.wxDialog.__init__(self,parent,-h4, title, size = (320,600), style=wx.wxDEFAULT_FRAME_STYLE|wx.wxNO_FULL_REPAINT_ON_RESIZE)
  27       # Make the Dialog box white
  28       self.SetBackgroundColour(wx.wxWHITE)
  29 
  30       # Add a wxTreeCtrl to the Dialog box.  Use Layout Constraints to position it in the Dialog.
  31       lay = wx.wxLayoutConstraints()
  32       lay.top.SameAs(self, wx.wxTop, 10)         # Top margin of 10
  33       lay.bottom.SameAs(self, wx.wxBottom, 30)   # Bottom margin of 30, to leave room for a button
  34       lay.left.SameAs(self, wx.wxLeft, 10)       # Left margin of 10
  35       lay.right.SameAs(self, wx.wxRight, 10)     # Right margin of 10
  36       self.tree = wx.wxTreeCtrl(self, -1, style=wx.wxTR_HAS_BUTTONS)
  37       self.tree.SetConstraints(lay)
  38 
  39       # Add a wxButton to the Dialog box.  Use Layout Constraints to position it in the Dialog.
  40       lay = wx.wxLayoutConstraints()
  41       lay.top.SameAs(self, wx.wxBottom, -25)     # Position the button at the bottom of the Dialog.
  42       lay.centreX.SameAs(self, wx.wxCentreX)     # Center the button horizontally
  43       lay.height.AsIs()
  44       lay.width.AsIs()
  45       btn = wx.wxButton(self, wx.wxID_OK, "OK")
  46       btn.SetConstraints(lay)
  47 
  48       # Add a root node to the wxTreeCtrl.
  49       self.treeroot = self.tree.AddRoot('Transana Database')
  50 
  51       # Add a Series Root and three subnodes
  52       self.seriesroot = self.tree.AppendItem(self.treeroot, 'Series')
  53       item = self.tree.AppendItem(self.seriesroot, 'Series 1')
  54       item = self.tree.AppendItem(self.seriesroot, 'Series 2')
  55       item = self.tree.AppendItem(self.seriesroot, 'Series 3')
  56 
  57       # Add a Collection Root and three subnodes
  58       self.collectionsroot = self.tree.AppendItem(self.treeroot, 'Collections')
  59       item = self.tree.AppendItem(self.collectionsroot, 'Collection 1')
  60       item = self.tree.AppendItem(self.collectionsroot, 'Collection 2')
  61       item = self.tree.AppendItem(self.collectionsroot, 'Collection 3')
  62 
  63       # Expand the nodes so everything is visible initially
  64       self.tree.Expand(self.treeroot)
  65       self.tree.Expand(self.seriesroot)
  66       self.tree.Expand(self.collectionsroot)
  67 
  68       # Define the Begin Drag Event for the tree
  69       wx.EVT_TREE_BEGIN_DRAG(self.tree, self.tree.GetId(), self.OnBeginDrag)
  70 
  71       # Define the Drop Target for the tree.  The custom drop target object
  72       # accepts the tree as a parameter so it can query it about where the
  73       # drop is supposed to be occurring to see if it will allow the drop.
  74       dt = TestDropTarget(self.tree)
  75       self.tree.SetDropTarget(dt)
  76 
  77       # Lay out the screen and maintain the present layout
  78       self.Layout()
  79       self.SetAutoLayout(wx.true)
  80 
  81       # Show the Dialog modally
  82       self.ShowModal()
  83       # Destroy the Dialog when we are done with it
  84       self.Destroy()
  85 
  86 
  87    def OnBeginDrag(self, event):
  88       """ Left Mouse Button initiates "Drag" for Tree Nodes """
  89       # Items in the tree are not automatically selected with a left click.
  90       # We must select the item that is initially clicked manually!!
  91       # We do this by looking at the screen point clicked and applying the tree's
  92       # HitTest method to determine the current item, then actually selecting the item
  93       sel_item, flags = self.tree.HitTest(event.GetPoint())
  94       self.tree.SelectItem(sel_item)
  95 
  96       # Determine what Item is being "dragged", and grab it's data
  97       tempStr = "%s" % (self.tree.GetItemText(sel_item))
  98       print "A node called %s is being dragged" % (tempStr)
  99 
 100       # Create a custom Data Object for Drag and Drop
 101       ddd = TestDragDropData()
 102       # In this case, the data is trivial, but this could easily be a more complex object.
 103       ddd.SetSource(tempStr)
 104 
 105       # Use cPickle to convert the data object into a string representation
 106       pddd = cPickle.dumps(ddd, 1)
 107 
 108       # Now create a wxCustomDataObject for dragging and dropping and
 109       # assign it a custom Data Format
 110       cdo = wx.wxCustomDataObject(wx.wxCustomDataFormat('TransanaData'))
 111       # Put the pickled data object in the wxCustomDataObject
 112       cdo.SetData(pddd)
 113 
 114       # Sorry, I am not able to figure out how to extract data from a wxDataObjectComposite until after
 115       # it's been dropped, so I'm ignoring that aspect of things for now.  I've left the code commented
 116       # out for others who wish to pursue it.
 117 
 118       # Now put the CustomDataObject into a DataObjectComposite
 119 #      tdo = wx.wxDataObjectComposite()
 120 #      tdo.Add(cdo)
 121 
 122       # Create a Custom DropSource Object.  The custom drop source object
 123       # accepts the tree as a parameter so it can query it about where the
 124       # drop is supposed to be occurring to see if it will allow the drop.
 125       tds = TestDropSource(self.tree)
 126       # Associate the Data with the Drop Source Object
 127       # I've associated both the pickled CustomDataObject and the raw data object here
 128       # so that I can easily work with the data being dragged.  I'm sure this is poor
 129       # form and unnecessary, but there it is.  You don't like it, you are welcome to fix it!
 130       tds.SetData(cdo, ddd)    # (tdo) would be used in place of (cdo) if I were using the DataObjectComposite object
 131 
 132       # Initiate the Drag Operation
 133       dragResult = tds.DoDragDrop(wx.true)
 134 
 135       # Report the result of the final drop when everything else is completed by the other objects
 136       if dragResult == wx.wxDragCopy:
 137           print "Result indicated successful copy"
 138       elif dragResult == wx.wxDragMove:
 139           print "Result indicated successful move"
 140       else:
 141           print "Result indicated failed drop"
 142       print
 143 
 144 
 145 class TestDropSource(wx.wxDropSource):
 146    """ This is a custom DropSource object designed to provide feedback to the user during the drag """
 147    def __init__(self, tree):
 148       # Create a Standard wxDropSource Object
 149       wx.wxDropSource.__init__(self)
 150       # Remember the control that initiate the Drag for later use
 151       self.tree = tree
 152 
 153    # SetData accepts an object (obj) that has been prepared for the DropSource SetData() method
 154    # and a copy of the original data for internal use.  I suppose I should rewrite this to
 155    # just accept the original data and do the wxDataObject packaging here.  Maybe when I have time.
 156    def SetData(self, obj, originalData):
 157       # Set the prepared object as the wxDropSource Data
 158       wx.wxDropSource.SetData(self, obj)
 159       # hold onto the original data for later use
 160       self.data = originalData
 161 
 162    # I want to provide the user with feedback about whether their drop will work or not.
 163    def GiveFeedback(self, effect):
 164       # This method does not provide the x, y coordinates of the mouse within the control, so we
 165       # have to figure that out the hard way. (Contrast with DropTarget's OnDrop and OnDragOver methods)
 166       # Get the Mouse Position on the Screen
 167       (windowx, windowy) = wx.wxGetMousePosition()
 168       # Translate the Mouse's Screen Position to the Mouse's Control Position
 169       (x, y) = self.tree.ScreenToClientXY(windowx, windowy)
 170       # Now use the tree's HitTest method to find out about the potential drop target for the current mouse position
 171       (id, flag) = self.tree.HitTest((x, y))
 172       # I'm using GetItemText() here, but could just as easily use GetPyData()
 173       tempStr = self.tree.GetItemText(id)
 174 
 175       # This line compares the data being dragged (self.data) to the potential drop site given by the current
 176       # mouse position (tempStr).  If we are going (from Series to Collection) or (from Collection to Series),
 177       # we return FALSE to indicate that we should use the default drag-and-drop feedback, which will indicate
 178       # that the drop is legal.  If not, we return TRUE to indicate we are using our own feedback, which is
 179       # implemented by changing the cursor to a "No_Entry" cursor to indicate the drop is not allowed.
 180       # Note that this code here does not prevent the drop.  That has to be implemented in the Drop Target
 181       # object.  It just provides visual feedback to the user.
 182       if self.data.SourceText.startswith('Series') and tempStr.startswith('Collection') or \
         self.data.SourceText.startswith('Collection') and tempStr.startswith('Series'):
 183          # FALSE indicates that feedback is NOT being overridden, and thus that the drop is GOOD!
 184          return wx.false
 185       else:
 186          # Set the cursor to give visual feedback that the drop will fail.
 187          self.tree.SetCursor(wx.wxStockCursor(wx.wxCURSOR_NO_ENTRY))
 188          # Setting the Effect to wxDragNone has absolutely no effect on the drop, if I understand this correctly.
 189          effect = wx.wxDragNone
 190          # returning TRUE indicates that the default feedback IS being overridden, thus that the drop is BAD!
 191          return wx.true
 192 
 193 
 194 
 195 class TestDragDropData(object):
 196    """ This is a custom DragDropData object.  It's pretty minimalist, but could be much more complex if you wanted. """
 197    def __init__(self):
 198       self.SourceText = ''
 199 
 200    def __repr__(self):
 201       return "SourceText = '%s'" % (self.SourceText)
 202 
 203    def SetSource(self, txt):
 204       self.SourceText = txt
 205 
 206 
 207 
 208 class TestDropTarget(wx.wxPyDropTarget):
 209    """ This is a custom DropTarget object designed to match drop behavior to the feedback given by the custom
 210        Drag Object's GiveFeedback() method. """
 211    def __init__(self, tree):
 212       # use a normal wxPyDropTarget
 213       wx.wxPyDropTarget.__init__(self)
 214       # Remember the source Tree Control for later use
 215       self.tree = tree
 216 
 217       # specify the data formats to accept
 218       self.df = wx.wxCustomDataFormat('TransanaData')
 219       # Specify the data object to accept data for this format
 220       self.cdo = wx.wxCustomDataObject(self.df)
 221       # Set the DataObject for the PyDropTarget
 222       self.SetDataObject(self.cdo)
 223 
 224    def OnEnter(self, x, y, d):
 225       # print "OnEnter %s, %s, %s" % (x, y, d)
 226       # Just allow the normal wxDragResult (d) to pass through here
 227       return d
 228 
 229    def OnLeave(self):
 230       # print "OnLeave"
 231       pass
 232 
 233    def OnDrop(self, x, y):
 234       # Process the "Drop" event
 235       # print "Drop:  x=%s, y=%s" % (x, y)
 236       # Use the tree's HitTest method to find out about the potential drop target for the current mouse position
 237       (id, flag) = self.tree.HitTest((x, y))
 238       # I'm using GetItemText() here, but could just as easily use GetPyData()
 239       tempStr = self.tree.GetItemText(id)
 240       # Remember the Drop Location for later Processing (in OnData())
 241       self.DropLocation = tempStr
 242       print "Dropped on %s." % tempStr
 243       # Because the DropSource GiveFeedback Method can change the cursor, I find that I
 244       # need to reset it to "normal" here or it can get stuck as a "No_Entry" cursor if
 245       # a Drop is abandoned.
 246       self.tree.SetCursor(wx.wxStockCursor(wx.wxCURSOR_ARROW))
 247       # We don't yet have enough information to veto the drop, so return TRUE to indicate
 248       # that we should proceed to the OnData method
 249       return wx.true
 250 
 251    # You can't know about the Source Data's characteristics in the OnDragOver method, so instead of
 252    # doing any processing here to determine if a drop is legal or not, we use the DropSource's OnFeedback
 253    # method and the DropTarget's OnData method to implement feedback and to allow or veto the drop
 254    # respectively
 255    # def OnDragOver(self, x, y, d):
 256    #    print "OnDragOver %s, %s, %s" % (x, y, d)
 257    #    I used to have a bunch of other logic here, but it could only know drop target information,
 258    #    and I needed to know about the Source of the Drag as well.
 259 
 260    def OnData(self, x, y, d):
 261       # once OnDrop returns TRUE, this method is automatically called.
 262       # print "OnData %s, %s, %s" % (x, y, d)
 263       # Let's get the data being dropped so we can do some processing logic
 264       if self.GetData():
 265          data = cPickle.loads(self.cdo.GetData())
 266          print "Drop Data = '%s' dropped onto %s" % (data, self.DropLocation)
 267 
 268       # This line compares the data being dragged (data) to the potential drop site determined in OnDrop and
 269       # passed here as self.DropLocation.  If we are going (from Series to Collection) or (from Collection to Series),
 270       # we do nothing to veto the drop because the drop is legal.  If not, we veto the drop by changing the wxDragResult
 271       # variable to indicate the drop is not allowed.
 272       # Note that this code here implements the drop logic and needs to match the logic in the DropSource's GiveFeedback()
 273       # method.  I should probably write a common function that both objects use to determine how to act.
 274       if data.SourceText.startswith('Series') and self.DropLocation.startswith('Collection') or \
         data.SourceText.startswith('Collection') and self.DropLocation.startswith('Series'):
 275          # If we meet the criteria, we do nothing to interfere with the drop process
 276          pass
 277       else:
 278          # If the test fails, we prevent the drop process by altering the wxDropResult (d)
 279          d = wx.wxDragNone
 280 
 281 
 282       # We can implement the desired behavior resulting from the drag and drop here!
 283       # Note that this example intentionally does nothing more than give feedback via the print statement.
 284       # If you want to experiment with having drag and drop alter the tree, be my guest.
 285       if d == wx.wxDragCopy:
 286           print "OnData Result indicated successful copy"
 287       elif d == wx.wxDragMove:
 288           print "OnData Result indicated successful move"
 289       else:
 290           print "OnData Result indicated failed drop"
 291       return d
 292 
 293 
 294 
 295 class MyApp(wx.wxApp):
 296    def OnInit(self):
 297       frame = MainWindow(None, -1, "Drag and Drop Test")
 298       self.SetTopWindow(frame)
 299       return wx.true
 300 
 301 
 302 app = MyApp(0)
 303 app.MainLoop()

- David Woods (dwoods at wcer dot wisc dot edu)


Recursively building a list into a wxTreeCtrl (yet another sample)

This is a simple function I wrote to insert a list with variable depth into a treecontrol. I use this in my pyciv application where it builds a tree control that the user can click on to navigate faster.

   1 #define the tree, the amount of children depends on the length of the list
   2 _treeList = [
   3         #project
   4         (['Project', ['Geometries', 'Verticals']]),
   5         #geometry
   6         (['Geometry', ['Points', 'Layers', 'Water']]),
   7         #soil
   8         (['Soil', ['Soiltypes']]),
   9         ]
  10 
  11 #this function builds the list
  12 def rec_filltreelist( self, element, child, firstChild ):
  13         newchild = child
  14         for e in element:
  15                 if isinstance( e, list ):
  16                         self.rec_filltreelist( e, newchild, firstChild )
  17                 else:
  18                         newchild = self.tree.AppendItem(child, e)
  19                         if not firstChild: firstChild = newchild
  20 
  21 #and this is how you call the function
  22 self.tree = wxTreeCtrl(splitter, tID, style=wxTR_HAS_BUTTONS|wxTR_HAS_VARIABLE_ROW_HEIGHT )
  23         root = self.tree.AddRoot("Overview")
  24         firstChild = None
  25         self.rec_filltreelist( _treeList, root, None )
  26         self.tree.Expand(root)
  27 
  28 #always handy to attach some kind of event to this list
  29 EVT_LEFT_DOWN (self.tree, self.OnTreeLeftDown)
  30 
  31 #and here is how to find out what was called (could be done better I admit, but it does the job)
  32 def OnTreeLeftDown(self, event):
  33         pt = event.GetPosition();
  34         item, flags = self.tree.HitTest(pt)
  35         parent = self.tree.GetItemParent(item)
  36         itemname = self.tree.GetItemText(item)
  37         if itemname == 'Points':
  38                 self.ShowPointsDialog()
  39         elif itemname == 'Layers':
  40                 self.ShowLayersDialog()
  41         ...
  42         event.Skip()

I hope this helps out someone!

Rob - delphiro dot zonnet dot nl

Simple Drag and Drop

I hunted around for quite a while and it was never quite made clear to me that if you want to use Drag and Drop to re-arrange a wxTreeCtrl, you've just got to make the tree respond to EVT_TREE_BEGIN_DRAG and EVT_TREE_END_DRAG in a simple way: call event.Allow() in Begin and then actually do the re-organization in End. If you add these lines to the wxPython/demo/wxTreeCtrl.py demo, you'll get an idea of what I mean and how simple this is. No drop targets.

   1     def __init__(self, ...):
   2 
   3         # These go at the end of __init__
   4         self.tree.Bind(wx.EVT_TREE_BEGIN_DRAG, self.OnBeginDrag)
   5         self.tree.Bind(wx.EVT_TREE_END_DRAG, self.OnEndDrag)
   6 
   7     def OnBeginDrag(self, event):
   8         '''Allow drag-and-drop for leaf nodes.'''
   9         self.log.WriteText("OnBeginDrag")
  10         if self.tree.GetChildrenCount(event.GetItem()) == 0:
  11             event.Allow()
  12             self.dragItem = event.GetItem()
  13         else:
  14             self.log.WriteText("Cant drag a node that has children")
  15 
  16 
  17     def OnEndDrag(self, event):
  18         '''Do the re-organization if possible'''
  19 
  20         self.log.WriteText("OnEndDrag")
  21        #If we dropped somewhere that isn't on top of an item, ignore the event
  22         if not event.GetItem().IsOk():
  23             return
  24 
  25         # Make sure this memeber exists.
  26         try:
  27             old = self.dragItem
  28         except:
  29             return
  30 
  31         # Get the other IDs that are involved
  32         new = event.GetItem()
  33         parent = self.tree.GetItemParent(new)
  34         if not parent.IsOk():
  35             return
  36 
  37         # Move 'em
  38         text = self.tree.GetItemText(old)
  39         self.tree.Delete(old)
  40         self.tree.InsertItem(parent, new, text)

Titus - titus a cs . ucr . edu

Drag and Drop with Folder Moving and Rearranging

By combining some recipes in the Wiki, along with the example in the wxPython Demo, I was able to come up with a fancy tree control.

DragAndDropWithFolderMovingAndRearranging

TreeCtrl with automatic scrolling while dragging

The following sample script demonstrates how to achieve automatic scrolling while dragging an item beyond the visible area of a tree control. In the sample only the lowest level items can be dragged.

Here's briefly how it works:

The methods that do the scrolling account for the differences between wxGTK and wxMSW. Note that automatic scrolling on wxMSW works only, if the tree control has an image list associated with it, thus showing little icons left to the text.

Christian

import  wx

#---------------------------------------------------------------------------

class MyTreeCtrl(wx.TreeCtrl):
    def __init__(self, parent, id, style):
        wx.TreeCtrl.__init__(self, parent, id, style=style)

    def OnCompareItems(self, item1, item2):
        t1 = self.GetItemText(item1)
        t2 = self.GetItemText(item2)
        if t1 < t2: return -1
        if t1 == t2: return 0
        return 1

#---------------------------------------------------------------------------

class TestTreeCtrlPanel(wx.Panel):
    def __init__(self, parent):
        # Use the WANTS_CHARS style so the panel doesn't eat the Return key.
        wx.Panel.__init__(self, parent, -1, style=wx.WANTS_CHARS)
        self.Bind(wx.EVT_SIZE, self.OnSize)

        self.tree = MyTreeCtrl(self, -1, wx.TR_HAS_BUTTONS)

        isz = (16,16)
        il = wx.ImageList(isz[0], isz[1])
        self.fldridx     = il.Add(wx.ArtProvider_GetBitmap(wx.ART_FOLDER,      wx.ART_OTHER, isz))
        self.fldropenidx = il.Add(wx.ArtProvider_GetBitmap(wx.ART_FILE_OPEN,   wx.ART_OTHER, isz))
        self.fileidx     = il.Add(wx.ArtProvider_GetBitmap(wx.ART_NORMAL_FILE, wx.ART_OTHER, isz))

        self.tree.SetImageList(il)
        self.il = il

        self.root = self.tree.AddRoot("The Root Item")
        self.tree.SetPyData(self.root, None)
        self.tree.SetItemImage(self.root, self.fldridx, wx.TreeItemIcon_Normal)
        self.tree.SetItemImage(self.root, self.fldropenidx, wx.TreeItemIcon_Expanded)

        for x in range(7):
            child = self.tree.AppendItem(self.root, "Item %d" % x)
            self.tree.SetPyData(child, None)
            self.tree.SetItemImage(child, self.fldridx, wx.TreeItemIcon_Normal)
            self.tree.SetItemImage(child, self.fldropenidx, wx.TreeItemIcon_Expanded)

            for y in range(5):
                last = self.tree.AppendItem(child, "item %d-%s" % (x, chr(ord("a")+y)))
                self.tree.SetPyData(last, None)
                self.tree.SetItemImage(last, self.fileidx, wx.TreeItemIcon_Normal)

        self.tree.Expand(self.root)
        self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnSelChanged, self.tree)

        self.Bind(wx.EVT_TREE_BEGIN_DRAG, self.OnDragBegin)
        self.Bind(wx.EVT_TREE_END_DRAG, self.OnDragEnd)

        self.Bind(wx.EVT_TIMER, self.OnTime)        

    def OnDragBegin(self, evt):
        item = evt.GetItem()
        
        if self.tree.GetItemParent(item) == self.root:
            evt.Veto()
            return

        self.dragitem = item
        evt.Allow()
        self.tree.Bind(wx.EVT_MOTION, self.OnMotion)
        self.tree.Bind(wx.EVT_LEFT_UP, self.OnMouseLeftUp)
        
    def OnDragEnd(self, evt):
        target = evt.GetItem()

        if not target.IsOk() or target == self.root:
            return

        text = self.tree.GetItemText(self.dragitem)
        if self.tree.GetItemParent(target) == self.root:
            added = self.tree.AppendItem(target, text)
        else:
            parent = self.tree.GetItemParent(target)
            added = self.tree.InsertItemBefore(parent, self.findItem(target), text)

        self.tree.Delete(self.dragitem)
        self.tree.SetItemImage(added, self.fileidx, wx.TreeItemIcon_Normal)

        self.tree.SelectItem(added)
        self.tree.EnsureVisible(added)
            
    def OnMouseLeftUp(self, evt):
        self.tree.Unbind(wx.EVT_MOTION)
        self.tree.Unbind(wx.EVT_LEFT_UP)
        evt.Skip()
        
    def OnMotion(self, evt):
        size = self.tree.GetSize()
        x,y = evt.GetPosition()
        
        if y < 0 or y > size[1] and not hasattr(self, 'timer'):
            self.timer = wx.Timer(self)
            self.timer.Start(70)
        evt.Skip()
        
    def OnTime(self, evt):
        x,y = self.tree.ScreenToClient(wx.GetMousePosition())
        size = self.tree.GetSize()

        if y < 0:
            self.ScrollUp()
        elif y > size[1]:
            self.ScrollDown()
        else:
            del self.timer
            return
        self.timer.Start(70)
        
    def ScrollUp(self):
        if "wxMSW" in wx.PlatformInfo:
            self.tree.ScrollLines(-1)
        else:
            first = self.tree.GetFirstVisibleItem()
            prev = self.tree.GetPrevSibling(first)
            if prev:
                # drill down to find last expanded child
                while self.tree.IsExpanded(prev):
                    prev = self.tree.GetLastChild(prev)
            else:
                # if no previous sub then try the parent
                prev = self.tree.GetItemParent(first)

            if prev:
                self.tree.ScrollTo(prev)
            else:
                self.tree.EnsureVisible(first)

    def ScrollDown(self):
        if "wxMSW" in wx.PlatformInfo:
            self.tree.ScrollLines(1)
        else:
            # first find last visible item by starting with the first
            next = None
            last = None
            item = self.tree.GetFirstVisibleItem()
            while item:
                if not self.tree.IsVisible(item): break
                last = item
                item = self.tree.GetNextVisible(item)

            # figure out what the next visible item should be,
            # either the first child, the next sibling, or the
            # parent's sibling
            if last:
                if self.tree.IsExpanded(last):
                    next = self.tree.GetFirstChild(last)[0]
                else:
                    next = self.tree.GetNextSibling(last)
                    if not next:
                        prnt = self.tree.GetItemParent(last)
                        if prnt:
                            next = self.tree.GetNextSibling(prnt)

            if next:
                self.tree.ScrollTo(next)
            elif last:
                self.tree.EnsureVisible(last)

    def traverse(self, parent=None):
        if parent is None:
            parent = self.root
        nc = self.tree.GetChildrenCount(parent, False)

        def GetFirstChild(parent, cookie):
            return self.tree.GetFirstChild(parent)
        
        GetChild = GetFirstChild
        cookie = 1
        for i in range(nc):
            child, cookie = GetChild(parent, cookie)
            GetChild = self.tree.GetNextChild
            yield child

    def findItem(self, item):
        parent = self.tree.GetItemParent(item)
        for n,i in enumerate(self.traverse(parent)):
            if item == i:
                return n
                
    def OnSize(self, event):
        w,h = self.GetClientSizeTuple()
        self.tree.SetDimensions(0, 0, w, h)

    def OnSelChanged(self, event):
        self.item = event.GetItem()
        event.Skip()


#---------------------------------------------------------------------------

def runTest(frame, nb, log):
    win = TestTreeCtrlPanel(nb, log)
    return win

def __test():

    class MyApp(wx.App):
        def OnInit(self):
            wx.InitAllImageHandlers()
            frame = wx.Frame(None, -1, 'Testframe', size=(200,400))
            panel = TestTreeCtrlPanel(frame)
            frame.Show()
            return True


    app = MyApp(0)
    app.MainLoop()

if __name__ == '__main__':
    __test()


The content on this page came from ListAndTreeControls, which was divided into this page and ListControls .

TreeControls (last edited 2010-01-17 07:26:05 by newacct)