tree en tkinter¶
exemple de rendu
# Highly optimized Tkinter tree control
# by Charles E. "Gene" Cash
#
# This is documented more fully on my homepage at
# http://home.cfl.rr.com/genecash/ and if it's not there, look in the Vaults
# of Parnassus at http://www.vex.net/parnassus/ which I promise to keep
# updated.
#
# Thanks to Laurent Claustre <claustre@esrf.fr> for sending lots of helpful
# bug reports.
#
# This copyright license is intended to be similar to the FreeBSD license.
#
# Copyright 1998 Gene Cash All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY GENE CASH ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# This means you may do anything you want with this code, except claim you
# wrote it. Also, if it breaks you get to keep both pieces.
#
# 02-DEC-98 Started writing code.
# 22-NOV-99 Changed garbage collection to a better algorithm.
# 28-AUG-01 Added logic to deal with exceptions in user callbacks.
# 02-SEP-01 Fixed hang when closing last node.
# 07-SEP-01 Added binding tracking so nodes got garbage-collected.
# Also fixed subclass call to initialize Canvas to properly deal
# with variable arguments and keyword arguments.
# 11-SEP-01 Bugfix for unbinding code.
# 13-OCT-01 Added delete & insert methods for nodes (by email request).
# LOTS of code cleanup.
# Changed leading double underscores to PVT nomenclature.
# Added ability to pass Node subclass to Tree constructor.
# Removed after_callback since subclassing Node is better idea.
# 15-OCT-01 Finally added drag'n'drop support. It consisted of a simple
# change to the Node PVT_click method, and addition of logic like
# the example in Tkdnd.py. It took 3 days to grok the Tkdnd
# example and 2 hours to make the code changes. Plus another 1/2
# day to get a working where() function.
# 16-OCT-01 Incorporated fixes to delete() and dnd_commit() bugs by
# Laurent Claustre <claustre@esrf.fr>.
# 17-OCT-01 Added find_full_id() and cursor_node() methods.
# 18-OCT-01 Fixes to delete() on root during collapse and with
# drag-in-progress flag by Laurent Claustre <claustre@esrf.fr>.
# 10-FEB-02 Fix to prev_visible() by Nicolas Pascal <pascal@esrf.fr>.
# Fixes which made insert_before()/insert_after() actually work.
# Also added expand/collapse indicators like Internet Explorer
# as requested by Nicolas.
# 11-FEB-02 Another fix to prev_visible(). It works this time. Honest.
# 31-MAY-02 Added documentation strings so the new PYthon 2.2 help function
# is a little more useful.
# 19-AUG-02 Minor fix to eliminate crash in "treedemo-icons.py" caused by
# referencing expand/collapse indicators when lines are turned off.
# 15-OCT-02 Used new idiom for calling Canvas superclass.
# 18-NOV-02 Fixed bug discovered by Amanjit Gill <amanjit.gill@gmx.de>, where
# I didn't pass "master" properly to the Canvas superclass. Sigh.
# One step forward, one step back.
import Tkdnd
from Tkinter import *
#------------------------------------------------------------------------------
def report_callback_exception():
"""report exception on sys.stderr."""
import traceback
import sys
sys.stderr.write("Exception in Tree control callback\n")
traceback.print_exc()
#------------------------------------------------------------------------------
class Struct:
"""Helper object for add_node() method"""
def __init__(self):
pass
#------------------------------------------------------------------------------
class Node:
"""Tree helper class that's instantiated for each element in the tree. It
has several useful attributes:
parent_node - immediate parent node
id - id assigned at creation
expanded_icon - image displayed when folder is expanded to display
children
collapsed_icon - image displayed when node is not a folder or folder is
collapsed.
parent_widget - reference to tree widget that contains node.
expandable_flag - is true when node is a folder that may be expanded or
collapsed.
expanded_flag - true to indicate node is currently expanded.
h_line - canvas line to left of node image.
v_line - canvas line below node image that connects children.
indic - expand/collapse canvas image.
label - canvas text label
symbol - current canvas image
Please note that methods prefixed PVT_* are not meant to be used by
client programs."""
def __init__(self, parent_node, id, collapsed_icon, x, y,
parent_widget=None, expanded_icon=None, label=None,
expandable_flag=0):
"""Create node and initialize it. This also displays the node at the
given position on the canvas, and binds mouseclicks."""
# immediate parent node
self.parent_node=parent_node
# internal name used to manipulate things
self.id=id
# bitmaps to be displayed
self.expanded_icon=expanded_icon
self.collapsed_icon=collapsed_icon
# tree widget we belong to
if parent_widget:
self.widget=parent_widget
else:
self.widget=parent_node.widget
# for speed
sw=self.widget
# our list of child nodes
self.child_nodes=[]
# flag that node can be expanded
self.expandable_flag=expandable_flag
self.expanded_flag=0
# add line
if parent_node and sw.line_flag:
self.h_line=sw.create_line(x, y, x-sw.dist_x, y)
else:
self.h_line=None
self.v_line=None
# draw approprate image
self.symbol=sw.create_image(x, y, image=self.collapsed_icon)
# add expand/collapse indicator
self.indic=None
if expandable_flag and sw.line_flag and sw.plus_icon and sw.minus_icon:
self.indic=sw.create_image(x-sw.dist_x, y, image=sw.plus_icon)
# add label
self.label=sw.create_text(x+sw.text_offset, y, text=label, anchor='w')
# single-click to expand/collapse
if self.indic:
sw.tag_bind(self.indic, '<1>', self.PVT_click)
else:
sw.tag_bind(self.symbol, '<1>', self.PVT_click)
# for drag'n'drop target detection
sw.tag_bind(self.symbol, '<Any-Enter>', self.PVT_enter)
sw.tag_bind(self.label, '<Any-Enter>', self.PVT_enter)
# for testing (gotta make sure nodes get properly GC'ed)
#def __del__(self):
# print self.full_id(), 'deleted'
# ----- PUBLIC METHODS -----
def set_collapsed_icon(self, icon):
"""Set node's collapsed image"""
self.collapsed_icon=icon
if not self.expanded_flag:
self.widget.itemconfig(self.symbol, image=icon)
def set_expanded_icon(self, icon):
"""Set node's expanded image"""
self.expanded_icon=icon
if self.expanded_flag:
self.widget.itemconfig(self.symbol, image=icon)
def parent(self):
"""Return node's parent node"""
return self.parent_node
def prev_sib(self):
"""Return node's previous sibling (the child immediately above it)"""
i=self.parent_node.child_nodes.index(self)-1
if i >= 0:
return self.parent_node.child_nodes[i]
else:
return None
def next_sib(self):
"""Return node's next sibling (the child immediately below it)"""
i=self.parent_node.child_nodes.index(self)+1
if i < len(self.parent_node.child_nodes):
return self.parent_node.child_nodes[i]
else:
return None
def next_visible(self):
"""Return next lower visible node"""
n=self
if n.child_nodes:
# if you can go right, do so
return n.child_nodes[0]
while n.parent_node:
# move to next sibling
i=n.parent_node.child_nodes.index(n)+1
if i < len(n.parent_node.child_nodes):
return n.parent_node.child_nodes[i]
# if no siblings, move to parent's sibling
n=n.parent_node
# we're at bottom
return self
def prev_visible(self):
"""Return next higher visible node"""
n=self
if n.parent_node:
i=n.parent_node.child_nodes.index(n)-1
if i < 0:
return n.parent_node
else:
j=n.parent_node.child_nodes[i]
return j.PVT_last()
else:
return n
def children(self):
"""Return list of node's children"""
return self.child_nodes[:]
def get_label(self):
"""Return string containing text of current label"""
return self.widget.itemcget(self.label, 'text')
def set_label(self, label):
"""Set current text label"""
self.widget.itemconfig(self.label, text=label)
def expanded(self):
"""Returns true if node is currently expanded, false otherwise"""
return self.expanded_flag
def expandable(self):
"""Returns true if node can be expanded (i.e. if it's a folder)"""
return self.expandable_flag
def full_id(self):
"""Return list of IDs of all parents and node ID"""
if self.parent_node:
return self.parent_node.full_id()+(self.id,)
else:
return (self.id,)
def expand(self):
"""Expand node if possible"""
if not self.expanded_flag:
self.PVT_set_state(1)
def collapse(self):
"""Collapse node if possible"""
if self.expanded_flag:
self.PVT_set_state(0)
def delete(self, me_too=1):
"""Delete node from tree. ("me_too" is a hack not to be used by
external code, please!)"""
sw=self.widget
if not self.parent_node and me_too:
# can't delete the root node
raise ValueError, "can't delete root node"
self.PVT_delete_subtree()
# move everything up so that distance to next subnode is correct
n=self.next_visible()
x1, y1=sw.coords(self.symbol)
x2, y2=sw.coords(n.symbol)
if me_too:
dist=y2-y1
else:
dist=y2-y1-sw.dist_y
self.PVT_tag_move(-dist)
n=self
if me_too:
if sw.pos == self:
# move cursor if it points to current node
sw.move_cursor(self.parent_node)
self.PVT_unbind_all()
sw.delete(self.symbol)
sw.delete(self.label)
sw.delete(self.h_line)
sw.delete(self.v_line)
sw.delete(self.indic)
self.parent_node.child_nodes.remove(self)
# break circular ref now, so parent may be GC'ed later
n=self.parent_node
self.parent_node=None
n.PVT_cleanup_lines()
n.PVT_update_scrollregion()
def insert_before(self, nodes):
"""Insert list of nodes as siblings before this node. Call parent
node's add_node() function to generate the list of nodes."""
i=self.parent_node.child_nodes.index(self)
self.parent_node.PVT_insert(nodes, i, self.prev_visible())
def insert_after(self, nodes):
"""Insert list of nodes as siblings after this node. Call parent
node's add_node() function to generate the list of nodes."""
i=self.parent_node.child_nodes.index(self)+1
self.parent_node.PVT_insert(nodes, i, self.PVT_last())
def insert_children(self, nodes):
"""Insert list of nodes as children of this node. Call node's
add_node() function to generate the list of nodes."""
self.PVT_insert(nodes, 0, self)
def toggle_state(self):
"""Toggle node's state between expanded and collapsed, if possible"""
if self.expandable_flag:
if self.expanded_flag:
self.PVT_set_state(0)
else:
self.PVT_set_state(1)
# ----- functions for drag'n'drop support -----
def PVT_enter(self, event):
"""detect mouse hover for drag'n'drop"""
self.widget.target=self
def dnd_end(self, target, event):
"""Notification that dnd processing has been ended. It DOES NOT imply
that we've been dropped somewhere useful, we could have just been
dropped into deep space and nothing happened to any data structures,
or it could have been just a plain mouse-click w/o any dragging."""
if not self.widget.drag:
# if there's been no dragging, it was just a mouse click
self.widget.move_cursor(self)
self.toggle_state()
self.widget.drag=0
# ----- PRIVATE METHODS (prefixed with "PVT_") -----
# these methods are subject to change, so please try not to use them
def PVT_last(self):
"""Return bottom-most node in subtree"""
n=self
while n.child_nodes:
n=n.child_nodes[-1]
return n
def PVT_find(self, search):
"""Used by searching functions"""
if self.id != search[0]:
# this actually only goes tilt if root doesn't match
return None
if len(search) == 1:
return self
# get list of children IDs
i=map(lambda x: x.id, self.child_nodes)
# if there is a child that matches, search it
try:
return self.child_nodes[i.index(search[1])].PVT_find(search[1:])
except:
return None
def PVT_insert(self, nodes, pos, below):
"""Create and insert new children. "nodes" is list previously created
via calls to add_list(). "pos" is index in the list of children where
the new nodes are inserted. "below" is node which new children should
appear immediately below."""
if not self.expandable_flag:
raise TypeError, 'not an expandable node'
# for speed
sw=self.widget
# expand and insert children
children=[]
self.expanded_flag=1
sw.itemconfig(self.symbol, image=self.expanded_icon)
if sw.minus_icon and sw.line_flag:
sw.itemconfig(self.indic, image=sw.minus_icon)
if len(nodes):
# move stuff to make room
below.PVT_tag_move(sw.dist_y*len(nodes))
# get position of first new child
xp, dummy=sw.coords(self.symbol)
dummy, yp=sw.coords(below.symbol)
xp=xp+sw.dist_x
yp=yp+sw.dist_y
# create vertical line
if sw.line_flag and not self.v_line:
self.v_line=sw.create_line(
xp, yp,
xp, yp+sw.dist_y*len(nodes))
sw.tag_lower(self.v_line, self.symbol)
n=sw.node_class
for i in nodes:
# add new subnodes, they'll draw themselves
# this is a very expensive call
children.append(
n(parent_node=self, expandable_flag=i.flag, label=i.name,
id=i.id, collapsed_icon=i.collapsed_icon,
expanded_icon=i.expanded_icon, x=xp, y=yp))
yp=yp+sw.dist_y
self.child_nodes[pos:pos]=children
self.PVT_cleanup_lines()
self.PVT_update_scrollregion()
sw.move_cursor(sw.pos)
def PVT_set_state(self, state):
"""Common code forexpanding/collapsing folders. It's not re-entrant,
and there are certain cases in which we can be called again before
we're done, so we use a mutex."""
while self.widget.spinlock:
pass
self.widget.spinlock=1
# expand & draw our subtrees
if state:
self.child_nodes=[]
self.widget.new_nodes=[]
if self.widget.get_contents_callback:
# this callback needs to make multiple calls to add_node()
try:
self.widget.get_contents_callback(self)
except:
report_callback_exception()
self.PVT_insert(self.widget.new_nodes, 0, self)
# collapse and delete subtrees
else:
self.expanded_flag=0
self.widget.itemconfig(self.symbol, image=self.collapsed_icon)
if self.indic:
self.widget.itemconfig(self.indic, image=self.widget.plus_icon)
self.delete(0)
# release mutex
self.widget.spinlock=0
def PVT_cleanup_lines(self):
"""Resize connecting lines"""
if self.widget.line_flag:
n=self
while n:
if n.child_nodes:
x1, y1=self.widget.coords(n.symbol)
x2, y2=self.widget.coords(n.child_nodes[-1].symbol)
self.widget.coords(n.v_line, x1, y1, x1, y2)
n=n.parent_node
def PVT_update_scrollregion(self):
"""Update scroll region for new size"""
x1, y1, x2, y2=self.widget.bbox('all')
self.widget.configure(scrollregion=(x1, y1, x2+5, y2+5))
def PVT_delete_subtree(self):
"""Recursively delete subtree & clean up cyclic references to make
garbage collection happy"""
sw=self.widget
sw.delete(self.v_line)
self.v_line=None
for i in self.child_nodes:
# delete node's subtree, if any
i.PVT_delete_subtree()
i.PVT_unbind_all()
# delete widgets from canvas
sw.delete(i.symbol)
sw.delete(i.label)
sw.delete(i.h_line)
sw.delete(i.v_line)
sw.delete(i.indic)
# break circular reference
i.parent_node=None
# move cursor if it's in deleted subtree
if sw.pos in self.child_nodes:
sw.move_cursor(self)
# now subnodes will be properly garbage collected
self.child_nodes=[]
def PVT_unbind_all(self):
"""Unbind callbacks so node gets garbage-collected. This wasn't easy
to figure out the proper way to do this. See also tag_bind() for the
Tree widget itself."""
for j in (self.symbol, self.label, self.indic, self.h_line,
self.v_line):
for k in self.widget.bindings.get(j, ()):
self.widget.tag_unbind(j, k[0], k[1])
def PVT_tag_move(self, dist):
"""Move everything below current icon, to make room for subtree using
the Disney magic of item tags. This is the secret of making
everything as fast as it is."""
# mark everything below current node as movable
bbox1=self.widget.bbox(self.widget.root.symbol, self.label)
bbox2=self.widget.bbox('all')
self.widget.dtag('move')
self.widget.addtag('move', 'overlapping',
bbox2[0], bbox1[3], bbox2[2], bbox2[3])
# untag cursor & node so they don't get moved too
self.widget.dtag(self.widget.cursor_box, 'move')
self.widget.dtag(self.symbol, 'move')
self.widget.dtag(self.label, 'move')
# now do the move of all the tagged objects
self.widget.move('move', 0, dist)
def PVT_click(self, event):
"""Handle mouse clicks by kicking off possible drag'n'drop
processing"""
if self.widget.drop_callback:
if Tkdnd.dnd_start(self, event):
x1, y1, x2, y2=self.widget.bbox(self.symbol)
self.x_off=(x1-x2)/2
self.y_off=(y1-y2)/2
else:
# no callback, don't bother with drag'n'drop
self.widget.drag=0
self.dnd_end(None, None)
#------------------------------------------------------------------------------
class Tree(Canvas):
# do we have enough possible arguments?!?!?!
def __init__(self, master, root_id, root_label='',
get_contents_callback=None, dist_x=15, dist_y=15,
text_offset=10, line_flag=1, expanded_icon=None,
collapsed_icon=None, regular_icon=None, plus_icon=None,
minus_icon=None, node_class=Node, drop_callback=None,
*args, **kw_args):
# pass args to superclass (new idiom from Python 2.2)
Canvas.__init__(self, master, *args, **kw_args)
# this allows to subclass Node and pass our class in
self.node_class=node_class
# keep track of node bindings
self.bindings={}
# cheap mutex spinlock
self.spinlock=0
# flag to see if there's been any d&d dragging
self.drag=0
# default images (BASE64-encoded GIF files)
if expanded_icon == None:
self.expanded_icon=PhotoImage(
data='R0lGODlhEAANAKIAAAAAAMDAwICAgP//////ADAwMAAAAAAA' \
'ACH5BAEAAAEALAAAAAAQAA0AAAM6GCrM+jCIQamIbw6ybXNSx3GVB' \
'YRiygnA534Eq5UlO8jUqLYsquuy0+SXap1CxBHr+HoBjoGndDpNAAA7')
else:
self.expanded_icon=expanded_icon
if collapsed_icon == None:
self.collapsed_icon=PhotoImage(
data='R0lGODlhDwANAKIAAAAAAMDAwICAgP//////ADAwMAAAAAAA' \
'ACH5BAEAAAEALAAAAAAPAA0AAAMyGCHM+lAMMoeAT9Jtm5NDKI4Wo' \
'FXcJphhipanq7Kvu8b1dLc5tcuom2foAQQAyKRSmQAAOw==')
else:
self.collapsed_icon=collapsed_icon
if regular_icon == None:
self.regular_icon=PhotoImage(
data='R0lGODlhCwAOAJEAAAAAAICAgP///8DAwCH5BAEAAAMALAAA' \
'AAALAA4AAAIphA+jA+JuVgtUtMQePJlWCgSN9oSTV5lkKQpo2q5W+' \
'wbzuJrIHgw1WgAAOw==')
else:
self.regular_icon=regular_icon
if plus_icon == None:
self.plus_icon=PhotoImage(
data='R0lGODdhCQAJAPEAAAAAAH9/f////wAAACwAAAAACQAJAAAC' \
'FIyPoiu2sJyCyoF7W3hxz850CFIA\nADs=')
else:
self.plus_icon=plus_icon
if minus_icon == None:
self.minus_icon=PhotoImage(
data='R0lGODdhCQAJAPEAAAAAAH9/f////wAAACwAAAAACQAJAAAC' \
'EYyPoivG614LAlg7ZZbxoR8UADs=')
else:
self.minus_icon=minus_icon
# horizontal distance that subtrees are indented
self.dist_x=dist_x
# vertical distance between rows
self.dist_y=dist_y
# how far to offset text label
self.text_offset=text_offset
# flag controlling connecting line display
self.line_flag=line_flag
# called just before subtree expand/collapse
self.get_contents_callback=get_contents_callback
# called after drag'n'drop
self.drop_callback=drop_callback
# create root node to get the ball rolling
self.root=node_class(parent_node=None, label=root_label,
id=root_id, expandable_flag=1,
collapsed_icon=self.collapsed_icon,
expanded_icon=self.expanded_icon,
x=dist_x, y=dist_y, parent_widget=self)
# configure for scrollbar(s)
x1, y1, x2, y2=self.bbox('all')
self.configure(scrollregion=(x1, y1, x2+5, y2+5))
# add a cursor
self.cursor_box=self.create_rectangle(0, 0, 0, 0)
self.move_cursor(self.root)
# make it easy to point to control
self.bind('<Enter>', self.PVT_mousefocus)
# totally arbitrary yet hopefully intuitive default keybindings
# stole 'em from ones used by microsoft tree control
# page-up/page-down
self.bind('<Next>', self.pagedown)
self.bind('<Prior>', self.pageup)
# arrow-up/arrow-down
self.bind('<Down>', self.next)
self.bind('<Up>', self.prev)
# arrow-left/arrow-right
self.bind('<Left>', self.ascend)
# (hold this down and you expand the entire tree)
self.bind('<Right>', self.descend)
# home/end
self.bind('<Home>', self.first)
self.bind('<End>', self.last)
# space bar
self.bind('<Key-space>', self.toggle)
# ----- PRIVATE METHODS (prefixed with "PVT_") -----
# these methods are subject to change, so please try not to use them
def PVT_mousefocus(self, event):
"""Soak up event argument when moused-over"""
self.focus_set()
# ----- PUBLIC METHODS -----
def tag_bind(self, tag, seq, *args, **kw_args):
"""Keep track of callback bindings so we can delete them later. I
shouldn't have to do this!!!!"""
# pass args to superclass
func_id=apply(Canvas.tag_bind, (self, tag, seq)+args, kw_args)
# save references
self.bindings[tag]=self.bindings.get(tag, [])+[(seq, func_id)]
def add_list(self, list=None, name=None, id=None, flag=0,
expanded_icon=None, collapsed_icon=None):
"""Add node construction info to list"""
n=Struct()
n.name=name
n.id=id
n.flag=flag
if collapsed_icon:
n.collapsed_icon=collapsed_icon
else:
if flag:
# it's expandable, use closed folder icon
n.collapsed_icon=self.collapsed_icon
else:
# it's not expandable, use regular file icon
n.collapsed_icon=self.regular_icon
if flag:
if expanded_icon:
n.expanded_icon=expanded_icon
else:
n.expanded_icon=self.expanded_icon
else:
# not expandable, don't need an icon
n.expanded_icon=None
if list == None:
list=[]
list.append(n)
return list
def add_node(self, name=None, id=None, flag=0, expanded_icon=None,
collapsed_icon=None):
"""Add a node during get_contents_callback()"""
self.add_list(self.new_nodes, name, id, flag, expanded_icon,
collapsed_icon)
def find_full_id(self, search):
"""Search for a node"""
return self.root.PVT_find(search)
def cursor_node(self, search):
"""Return node under cursor"""
return self.pos
def see(self, *items):
"""Scroll (in a series of nudges) so items are visible"""
x1, y1, x2, y2=apply(self.bbox, items)
while x2 > self.canvasx(0)+self.winfo_width():
old=self.canvasx(0)
self.xview('scroll', 1, 'units')
# avoid endless loop if we can't scroll
if old == self.canvasx(0):
break
while y2 > self.canvasy(0)+self.winfo_height():
old=self.canvasy(0)
self.yview('scroll', 1, 'units')
if old == self.canvasy(0):
break
# done in this order to ensure upper-left of object is visible
while x1 < self.canvasx(0):
old=self.canvasx(0)
self.xview('scroll', -1, 'units')
if old == self.canvasx(0):
break
while y1 < self.canvasy(0):
old=self.canvasy(0)
self.yview('scroll', -1, 'units')
if old == self.canvasy(0):
break
def move_cursor(self, node):
"""Move cursor to node"""
self.pos=node
x1, y1, x2, y2=self.bbox(node.symbol, node.label)
self.coords(self.cursor_box, x1-1, y1-1, x2+1, y2+1)
self.see(node.symbol, node.label)
def toggle(self, event=None):
"""Expand/collapse subtree"""
self.pos.toggle_state()
def next(self, event=None):
"""Move to next lower visible node"""
self.move_cursor(self.pos.next_visible())
def prev(self, event=None):
"""Move to next higher visible node"""
self.move_cursor(self.pos.prev_visible())
def ascend(self, event=None):
"""Move to immediate parent"""
if self.pos.parent_node:
# move to parent
self.move_cursor(self.pos.parent_node)
def descend(self, event=None):
"""Move right, expanding as we go"""
if self.pos.expandable_flag:
self.pos.expand()
if self.pos.child_nodes:
# move to first subnode
self.move_cursor(self.pos.child_nodes[0])
return
# if no subnodes, move to next sibling
self.next()
def first(self, event=None):
"""Go to root node"""
# move to root node
self.move_cursor(self.root)
def last(self, event=None):
"""Go to last visible node"""
# move to bottom-most node
self.move_cursor(self.root.PVT_last())
def pageup(self, event=None):
"""Previous page"""
n=self.pos
j=self.winfo_height()/self.dist_y
for i in range(j-3):
n=n.prev_visible()
self.yview('scroll', -1, 'pages')
self.move_cursor(n)
def pagedown(self, event=None):
"""Next page"""
n=self.pos
j=self.winfo_height()/self.dist_y
for i in range(j-3):
n=n.next_visible()
self.yview('scroll', 1, 'pages')
self.move_cursor(n)
# ----- functions for drag'n'drop support -----
def where(self, event):
"""Determine drag location in canvas coordinates. event.x & event.y
don't seem to be what we want."""
# where the corner of the canvas is relative to the screen:
x_org=self.winfo_rootx()
y_org=self.winfo_rooty()
# where the pointer is relative to the canvas widget,
# including scrolling
x=self.canvasx(event.x_root-x_org)
y=self.canvasy(event.y_root-y_org)
return x, y
def dnd_accept(self, source, event):
"""Accept dnd messages, i.e. we're a legit drop target, and we do
implement d&d functions."""
self.target=None
return self
def dnd_enter(self, source, event):
"""Get ready to drag or drag has entered widget (create drag
object)"""
# this flag lets us know there's been drag motion
self.drag=1
x, y=self.where(event)
x1, y1, x2, y2=source.widget.bbox(source.symbol, source.label)
dx, dy=x2-x1, y2-y1
# create dragging icon
if source.expanded_flag:
self.dnd_symbol=self.create_image(x, y,
image=source.expanded_icon)
else:
self.dnd_symbol=self.create_image(x, y,
image=source.collapsed_icon)
self.dnd_label=self.create_text(x+self.text_offset, y,
text=source.get_label(),
justify='left',
anchor='w')
def dnd_motion(self, source, event):
"""Move drag icon"""
self.drag=1
x, y=self.where(event)
x1, y1, x2, y2=self.bbox(self.dnd_symbol, self.dnd_label)
self.move(self.dnd_symbol, x-x1+source.x_off, y-y1+source.y_off)
self.move(self.dnd_label, x-x1+source.x_off, y-y1+source.y_off)
def dnd_leave(self, source, event):
"""Finish dragging or drag has left widget (destroy drag object)"""
self.delete(self.dnd_symbol)
self.delete(self.dnd_label)
def dnd_commit(self, source, event):
"""Object has been dropped here"""
# call our own dnd_leave() to clean up
self.dnd_leave(source, event)
# process pending events to detect target node
# update_idletasks() doesn't do the trick if source & target are
# on different widgets
self.update()
if not self.target:
# no target node
return
# we must update data structures based on the drop
if self.drop_callback:
try:
# called with dragged node and target node
# this is where a file manager would move the actual file
# it must also move the nodes around as it wishes
self.drop_callback(source, self.target)
except:
report_callback_exception()
#------------------------------------------------------------------------------
# the good 'ol test/demo code
if __name__ == '__main__':
import os
import sys
# default routine to get contents of subtree
# supply this for a different type of app
# argument is the node object being expanded
# should call add_node()
def get_contents(node):
path=apply(os.path.join, node.full_id())
for filename in os.listdir(path):
full=os.path.join(path, filename)
name=filename
folder=0
if os.path.isdir(full):
# it's a directory
folder=1
elif not os.path.isfile(full):
# but it's not a file
name=name+' (special)'
if os.path.islink(full):
# it's a link
name=name+' (link to '+os.readlink(full)+')'
node.widget.add_node(name=name, id=filename, flag=folder)
root=Tk()
root.title(os.path.basename(sys.argv[0]))
tree=os.sep
if sys.platform == 'win32':
# we could call the root "My Computer" and mess with get_contents()
# to return "A:", "B:", "C:", ... etc. as it's children, but that
# would just be terminally cute and I'd have to shoot myself
tree='C:'+os.sep
# create the control
t=Tree(master=root,
root_id=tree,
root_label=tree,
get_contents_callback=get_contents,
width=300)
t.grid(row=0, column=0, sticky='nsew')
# make expandable
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
# add scrollbars
sb=Scrollbar(root)
sb.grid(row=0, column=1, sticky='ns')
t.configure(yscrollcommand=sb.set)
sb.configure(command=t.yview)
sb=Scrollbar(root, orient=HORIZONTAL)
sb.grid(row=1, column=0, sticky='ew')
t.configure(xscrollcommand=sb.set)
sb.configure(command=t.xview)
# must get focus so keys work for demo
t.focus_set()
# we could do without this, but it's nice and friendly to have
Button(root, text='Quit', command=root.quit).grid(row=2, column=0,
columnspan=2)
# expand out the root
t.root.expand()
root.mainloop()