Python Tkinter: Tree selection

2019-01-25 23:30发布

I have created 2 trees with idlelib.TreeWidget in Canvas, left and right.

I am also able to print out the name of a tree node if double-clicked, but what I need is double-clicking one tree node will make a certain tree node visible and selected.

I have a simple example here. If you double click "level1" on the left hand side, "ccc" on the right hand side should be visible and automatically selected. How do you do that?

Please run the following code:

from Tkinter import Tk, Frame, BOTH, Canvas
from xml.dom.minidom import parseString
from idlelib.TreeWidget import TreeItem, TreeNode

class DomTreeItem(TreeItem):
   def __init__(self, node):
      self.node = node
   def GetText(self):
      node = self.node
      if node.nodeType == node.ELEMENT_NODE:
         return node.nodeName
      elif node.nodeType == node.TEXT_NODE:
         return node.nodeValue
   def IsExpandable(self):
      node = self.node
      return node.hasChildNodes()
   def GetSubList(self):
      parent = self.node
      children = parent.childNodes
      prelist = [DomTreeItem(node) for node in children]
      itemlist = [item for item in prelist if item.GetText().strip()]
      return itemlist
   def OnDoubleClick(self):
      print self.node.nodeName

left = '''
<level0>
 <level1/>
</level0>
'''
right = '''
<aaa>
 <bbb> <ccc/> </bbb>
</aaa>
'''
class Application(Frame):

   def __init__(self, parent):
      Frame.__init__(self, parent)
      self.parent = parent
      self.parent.geometry('%dx%d+%d+%d' % (800, 300, 0, 0))
      self.parent.resizable(0, 0)

      dom = parseString(left)
      item = DomTreeItem(dom.documentElement)
      self.canvas = Canvas(self, bg = "cyan")
      self.canvas.grid(column = 0, row = 0, sticky = 'NSWE')
      node = TreeNode(self.canvas, None, item)
      node.update()

      dom2 = parseString(right)
      item2 = DomTreeItem(dom2.documentElement)
      self.canvas2 = Canvas(self, bg = "yellow")
      self.canvas2.grid(column = 1, row = 0, sticky = 'NSWE')
      node2 = TreeNode(self.canvas2, None, item2)
      node2.update()

      self.pack(fill = BOTH, expand = True)

def main():
   root = Tk()
   Application(root)
   root.mainloop()

if __name__ == '__main__':
   main()  

1条回答
Melony?
2楼-- · 2019-01-26 00:05

First, your double click callback must be aware of your TreeNode node2 (I can think of global variable, attribute in DomTreeItem or bounce to another component).

Then you can rely on expand() method of TreeNode, read the children attribute and expand sequentially until the element you want. Note that children attribute is only populated after the node has been expanded.

1. Quick answer

Quick and dirty solution for the example you have provided

class DomTreeItem(TreeItem):
    def OnDoubleClick(self):
        if self.GetText() == "level1":
            node2.expand()
            node2.children[0].expand()
            node2.children[0].children[0].select()

[...]

class Application(Frame):
    def __init__(self, parent):
        global node2

2. Generic solution

Here is a more general method to display an arbitrary item in a tree.

def reach(node_tree, path):
   tokens = path.split("/")
   current_node = node_tree
   for name in tokens:
      if len(current_node.children) == 0 and current_node.state != "expanded":
         current_node.expand()
      candidates = [child for child in current_node.children if child.item.GetText() == name]
      if len(candidates) == 0:
         print("did not find '{}'".format(name))
         return
      current_node = candidates[0]
   current_node.select()

You might use it this way

if self.GetText() == "level1":
    reach(node2, "bbb/ccc")

3. Architecture proposal

Besides the expansion an selection of an item, I propose you a cleaner architecture through a DIY observer.

DIY Observer

(mimic the Tkinter bind call but does not rely on tkinter machinery since generating event with user data is not properly handled)

class DomTreeItem(TreeItem):
   def __init__(self, node, dbl_click_bindings = None):
      self.node = node
      self.dbl_click_bindings = [] if (dbl_click_bindings == None) else dbl_click_bindings

   [...]
   def OnDoubleClick(self):
      self.fireDblClick()

   def bind(self, event, callback):
      '''mimic tkinter bind
      '''
      if (event != "<<TreeDoubleClick>>"):
         print("err...")
      self.dbl_click_bindings.append(callback)
   def fireDblClick(self):
      for callback in self.dbl_click_bindings:
         callback.double_click(self.GetText())

Dedicated component

Rely on the reach method presented above.

class TreeExpander:
   def __init__(self, node_tree, matching_items):
       self.node_tree = node_tree
       self.matching_items = matching_items
   def double_click(self, item_name):
      print("double_click ({0})".format(item_name))
      if (item_name in self.matching_items):
         reach(self.node_tree, self.matching_items[item_name])

Subscription

class Application(Frame):
   def __init__(self, parent):
      [...]

      expander = TreeExpander(node2, {
         "level1": "bbb/ccc"
         })
      item.bind("<<TreeDoubleClick>>", expander)    


I did not find docs for idlelib, in this case, you can try to look at the code. The following snippet allows you to find which file host this module.

import idlelib.TreeWidget
print(idlelib.TreeWidget.__file__)
查看更多
登录 后发表回答