Duplicating a Tkinter treeview - python-3.x

I'm trying to display a ttk treeview in another window. The only option, it seems, is to iterate through the original treeview and populate the new one accordingly.
However I can't seem to get all the (many) subfolders in the right place, everything is mixed up as of the 2d level (i.e., I get the root folders and their children right, and after that the subfolders seem to be inserted at random locations).
The function is :
def getsubchildren(item=''):
children = []
for child in original_treeview.get_children(item):
i = new_treeview.insert(item, 'end', text=original_treeview.item(child)
['text'],values=original_treeview.item(child)['values'])
children.append(i)
for subchild in children:
getsubchildren(subchild)
And calling the function with getsubchildren(item=''), to start iterating from the first level.
There must be something I'm doing wrong, but I can't identify the issue and my attempts at modifying the function have only given a poorer result.
Any idea ?
Thanks,

Without known the depth of the item you need to check if the item has children. If so you need the function to call itself in a loop. Here is a working exampel:
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
maintree = ttk.Treeview(root)
maintree.pack()
first = maintree.insert("","end",text='first')
second= maintree.insert(first,"end",text='second')
third= maintree.insert(second,"end",text='third')
fourth= maintree.insert(third,"end",text='fourth')
st = maintree.insert("","end",text='1st')
nd= maintree.insert(st,"end",text='2nd')
rd= maintree.insert(nd,"end",text='3rd')
th= maintree.insert(rd,"end",text='4th')
top = tk.Toplevel(root)
subtree = ttk.Treeview(top)
subtree.pack()
def copy_item_tree(item,child):
for child in maintree.get_children(child):
item = subtree.insert(item,"end",
text=maintree.item(child)['text'])
if maintree.get_children(child):
copy_item_tree(item,child)
def copy_tree():
for child in maintree.get_children():
item = subtree.insert("","end",text=maintree.item(child)['text'])
copy_item_tree(item,child)
button = tk.Button(root,text='Copy Tree', command=copy_tree)
button.pack(fill='x')
root.mainloop()

Related

How to correctly change the number of columns and their names after creating the treeview (tkinter)

I am trying to create my own class treeview
I ran into the fact that after trying to change the number of columns, it persistently creates one more.
I can't figure out why this is happening and how to change it.
Help.
import tkinter
from tkinter import ttk
class TableTreeView (ttk.Treeview):
def __init__(self, root):
super().__init__(root)
def create_columns(self, columns_count):
columns_names = []
for i in range(columns_count):
columns_names.append('№'+str(i+1))
columns_names = tuple(columns_names)
self['columns'] = columns_names
for i in range(columns_count):
self.heading(columns_names[i], text=columns_names[i])
root = tkinter.Tk()
table = TableTreeView(root)
table.create_columns(2)
table.pack()
root.mainloop()
I have also tried this approach:
self.config({"columns": columns_names})
You should not use append command to change. You can change existing object by reaching it self or other pointed variables and set it's content like below
self['show'] = 'headings'

ttk Treeview, get item's bbox after scrolling into view

I am working on an editable tkinter.ttk.Treeview subclass. For editing I need to place the edit widget on top of a choosen "cell" (list row/column). To get the proper coordinates, there is the Treeview.bbox() method.
If the row to be edited is not in view (collapsed or scrolled away), I cannot get its bbox obviously. Per the docs, the see() method is meant to bring an item into view in such a case.
Example Code:
from tkinter import Tk, Button
from tkinter.ttk import Treeview
root = Tk()
tv = Treeview(root)
tv.pack()
iids = [tv.insert("", "end", text=f"item {n}") for n in range(20)]
# can only get bbox once everything is on screen.
n = [0]
def show_bbox():
n[0] += 1
iid = iids[n[0]]
b = tv.bbox(iid)
if not b:
# If not visible, scroll into view and try again
tv.see(iid)
# ... but this still doesn't return a valid bbox!?
b = tv.bbox(iid)
print(f"bbox of item {n}", b)
btn = Button(root, text="bbox", command=show_bbox)
btn.pack(side="bottom")
root.mainloop()
(start, then click the button until you reach an invisible item)
The second tv.bbox() call ought to return a valid bbox, but still returns empty string. Apparently see doesnt work immediately, but enqeues the viewport change into the event queue somehow. So my code cannot just proceed synchronously as it seems.
How to solve this? Can see() be made to work immediately? If not, is there another workaround?
The problem is that even after calling see, the item isn't visible (and thus, doesn't have a bounding box) until it is literally drawn on the screen.
A simple solution is to call tv.update_idletasks() immediately after calling tv.see(), which should cause the display to refresh.
Another solution is to use tv.after to schedule the display of the box (or the overlaying of an entry widget) to happen after mainloop has a chance to refresh the window.
def print_bbox(iid):
bbox = tv.bbox(iid)
print(f"bbox of item {iid}", bbox)
def show_bbox():
n[0] += 1
iid = iids[n[0]]
tv.see(iid)
tv.after_idle(print_bbox, iid)

Python Tkinter: Creating checkbuttons from a list

I am building a gui in tkinter with a list task_list = [].
Tasks are appended to/deleted from the list in the gui.
I want a window with checkboxes for every item in the list.
So if there's 10 items in the list, there should also be 10 checkboxes.
If there's 5 items in the list, there should be 5 corresponding checkboxes.
Can this be done?
I can't find anything on it
Thanks!
Here.
from tkinter import *
task_list=["Call","Work","Help"]
root=Tk()
Label(root,text="My Tasks").place(x=5,y=0)
placement=20
for tasks in task_list:
Checkbutton(root,text=str(tasks)).place(x=5,y=placement)
placement+=20
root.mainloop()
Using grid.
from tkinter import *
task_list=["Call","Work","Help"]
root=Tk()
Label(root,text="My Tasks").grid(row=0,column=0)
placement=3
for tasks in task_list:
Checkbutton(root,text=str(tasks)).grid(row=placement,column=0,sticky="w")
placement+=3
root.mainloop()
Here is my code for this issue:
from tkinter import Tk, Checkbutton, IntVar, Frame, Label
from functools import partial
task_list = ['Task 1', 'Task 2', 'Task 3', 'Work', 'Study']
def choose(index, task):
print(f'Selected task: {task}' if var_list[index].get() == 1 else f'Unselected task: {task}')
root = Tk()
Label(root, text='Tasks').grid(column=0, row=0)
frame = Frame(root)
frame.grid(column=0, row=1)
var_list = []
for index, task in enumerate(task_list):
var_list.append(IntVar(value=0))
Checkbutton(frame, variable=var_list[index],
text=task, command=partial(choose, index, task)).pack()
root.mainloop()
First I would like to mention that it is possible to mix layout manager methods like in this example. The main window uses grid as layout management method and I have gridded a frame to the window, but notice that Checkbuttons are getting packed, that is because frame is a different container so it is possible to use a different layout manager, which in this case makes it easier because pack just puts those checkbuttons one after another.
The other stuff:
There is the task list which would contain the tasks.
Then I have defined a function choose() this function prints out something. It depends on a variable. The comparison happens like this: print out this if value is this else print out this. It is just an if/else statement in one line and all it checks is if the IntVar in that list in that specific index is value 1 so "on". And there are two argument this function takes in: index and task. The index is meant to get the according IntVar from the var_list and the task is meant to display what tasks was chosen or unchosen.
Then the simple root = Tk() and root.mainloop() at the end.
Then is the label that just explains it.
Then the frame and here You can see that both label and frame were gridded using .grid()
Then the var_list = [] just creates an empty list.
Then comes the loop:
It loops over each item in the task_list and extracts the index of that item in the list and the value itself. This is done by using enumerate() function.
Each iteration appends a IntVar(value=0) to the var_list and since this appending happens at the same time as the items are read from the task_list the index of that IntVar in the list is the same as the current item in the task_list so that same index can be used for access.
Then a Checkbutton is created, its master is the frame (so that .pack() can be used) and the text=task so it corresponds to task name, the variable is set as a specific item in the var_list by index and this all has to be done so that a reference to that IntVar is kept. Then comes command=partial(choose, index, task) which may seem confusing but all partial does is basically this function will now execute always with the variables just given so those variables will always be the same for this function for this Checkbutton. And the first argument of partial is the function to be executed and next are arguments this function takes in. Then the Checkbutton gets packed.
If You have any questions ask.
Useful sources:
About partial() (though there are other sources too)
About Checkbutton (other sources about this too)
One line if/else statements

tkinter canvas - extract object id from event?

Is there a way I can extract the canvas object's id from an event?
For example, I'd like to add an item to a canvas, and bind to it - but if I have multiple items of them on my canvas, I need to distinguish between them.
def add_canvas_item(self,x,y):
canvas_item_id = self.canvas.create_oval(x-50,y-50,x+50,y+50, fill='green')
self.canvas.tag_bind(canvas_item_id ,"<ButtonPress-1>",self.stateClicked)
def itemClicked(self,event):
print("Item XYZ Clicked!") <- Where XYZ is the ID of the item
I have some very "hacky" ways around this (keep track of the mouse, and ask the canvas for the nearest item to that point) but that doesn't seem like the "best" way.
Is there a better way?
You can use the find_withtag() function which returns the clicked item as in the example below:
from tkinter import *
root = Tk()
canvas = Canvas(root)
canvas.pack()
def itemClicked(event):
canvas_item_id = event.widget.find_withtag('current')[0]
print('Item', canvas_item_id, 'Clicked!')
def add_canvas_item(x,y):
canvas_item_id = canvas.create_oval(x-50,y-50,x+50,y+50, fill='green')
canvas.tag_bind(canvas_item_id ,'<ButtonPress-1>', itemClicked)
add_canvas_item(100,100) # Test item 1
add_canvas_item(250,150) # Test item 2
root.mainloop()
Brief description at Tracking Mouse Actions for Many Canvas Objects

Tkinter - How to trace expanding list of variables

What I am trying to do track when any values in a list of StringVar change, even when the list is expanding. Any additions to the list before the trace statement will result in the callback. But any additions afterward, such as when pressing a button, will not cause any callback.
import tkinter as tk
root = tk.Tk()
frame = tk.Frame(root)
frame.grid(row=0)
L = []
def add_entry(event):
L.append(tk.StringVar())
tk.Entry(frame,textvariable=L[len(L)-1]).grid(row=len(L),padx=(10,10),pady=(5,5))
add = tk.Button(frame,text='add Entry',command='buttonpressed')
add.grid(row=0)
add.bind('<Button-1>',add_entry)
for i in range(2):
L.append(tk.StringVar())
tk.Entry(frame,textvariable=L[len(L)-1]).grid(row=len(L),padx=(10,10),pady=(5,5))
for i in L:
i.trace('w',lambda *arg:print('Modified'))
root.mainloop()
Modifying the first two Entry's prints out Modified, but any Entry's after the trace is run, such as the ones produced when a button is pressed, will not.
How do I make it so that trace method will run the callback for the entire list of variables even if the list is expanded?
Simple suggestion, change your add_entry function to something like this:
def add_entry(event):
L.append(tk.StringVar())
tk.Entry(frame,textvariable=L[len(L)-1]).grid(row=len(L),padx=(10,10),pady=(5,5))
L[len(L)-1].trace('w',lambda *arg:print('Modified'))
Extra suggestions:
This add = tk.Button(frame,text='add Entry',command='buttonpressed') is assigning a string to command option, means it will try to execute that string when button is clicked(which will do nothing). Instead, you can assign your function add_entry to command option and it will call that function when button is clicked and you can avoid binding Mouse Button1 click to your Button(Note: No need to use argument event in function when using like this). Read more here
Python supports negative indexing of List, so you can call L[-1] to retrieve the last element in the list instead of calling L[len(L)-1]).
Once you change your add_entry function as suggested, you can reduce your code to
import tkinter as tk
root = tk.Tk()
frame = tk.Frame(root)
frame.grid(row=0)
L = []
def add_entry():
global L
L.append(tk.StringVar())
tk.Entry(frame,textvariable=L[-1]).grid(row=len(L),padx=(10,10),pady=(5,5))
L[-1].trace('w',lambda *arg:print('Modified'))
add = tk.Button(frame,text='add Entry',command=add_entry)
add.grid(row=0)
for i in range(2):
add_entry()
root.mainloop()

Resources