tkinter canvas - extract object id from event? - python-3.x

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

Related

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)

Duplicating a Tkinter treeview

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()

PyQt QScrollArea doesn't display widgets

I am somewhat new to GUI programming and very new to PyQt, and I'm trying to build a GUI that displays a list of questions. I have created a QuestionBank class that subclasses QWidget and overrides the .show() method to display the list properly. I have tested this alone and it works correctly. However, the list of questions can be quite long, so I've been trying to make it scrollable. Rather than add a QScrollBar to the widget and then set up the event triggers by hand, I've been trying to my QuestionBank widget in a QScrollArea based on the syntax I've seen in examples online. While the scroll area shows up fine, it does not at all display the question bank but rather just shows a blank outline.
The QuestionBank class looks like this:
class QuestionBank(QWidget):
BUFFER = 10 # space between questions (can be modified)
def __init__(self, parent, questions):
# `parent` should be the QWidget that contains the QuestionBank, or None if
# QuestionBank is top level
# `questions` should be a list of MasterQuestion objects (not widgets)
QWidget.__init__(self, parent)
self.questions = [MasterQuestionWidget(self, q) for q in questions]
self.bottomEdge = 0
def show(self, y=BUFFER):
QWidget.show(self)
for q in self.questions:
# coordinates for each each question
q.move(QuestionBank.BUFFER, y)
q.show()
# update y-coordinate so that questions don't overlap
y += q.frameGeometry().height() + QuestionBank.BUFFER
self.bottomEdge = y + 3 * QuestionBank.BUFFER
# ... other methods down here
My code for showing the scroll bar looks like this:
app = QApplication(sys.argv)
frame = QScrollArea()
qs = QuestionBank(None, QFileManager.importQuestions())
qs.resize(350, 700)
frame.setGeometry(0, 0, 350, 300)
frame.setWidget(qs)
frame.show()
sys.exit(app.exec_())
I have tried many variants of this, including calling resize on frame instead of qs, getting rid of setGeometry, and setting the parent of qs to frame instead of None and I have no idea where I'm going wrong.
If it helps, I'm using PyQt5
Here is the question bank without the scroll area, to see what it is supposed to look like:
Here is the output of the code above with the scroll area:
This variation on the code is the only one that produces any output whatsoever, the rest just have blank windows. I'm convinced its something simple I'm missing, as the frame is obviously resizing correctly and it obviously knows what widget to display but its not showing the whole thing.
Any help is much appreciated, thank you in advance.

Splitting tkinter canvas and frame apart

Got a tkinter frame on the left being used for labels, checkbuttons, etc. On the right is a canvas displaying a map. I can scroll over the map and it will give the longitude/latitude coordinates of where the mouse pointer is located on the map at the time in question. I can click on the map and it will zoom in on the map. The problem is when I'm on the frame where I want to display underlying map data as I scroll the mouse across the frame the longitude/latitude changes, even though I'm not on the canvas. If I click on the frame, haven't put any checkbuttons on there yet to test it that way, it zooms right in just like it would over on the canvas.
Is there any way to split apart the action 'sensing' of the frame and canvas to keep them separate.
I would post up the code, a bit lengthy, but I got get out of here as I'm already running late.
Edit:
I'm back and thanks to Bryan's reply I think I understand what he was saying to do, just not sure how to do it. In a couple of attempts nothing seemed to work. Granted I'm still not fully sure of the (self,parent) 'addressing' method in the code below.
Also I see high probability coming up in the not to distant future of needing to be able to reference the mouse button to both t he canvas and the frame separately, aka have it do different things depending on where I have clicked on. Fortunately with the delay thanks to having to get out of here earlier and with Bryan's answer I have been able to shorten the code down even more and now have code that is doing exactly what I'm talking about. The delay in posting code worked to my benefit.
import tkinter as tk
from tkinter import *
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.frame = tk.Frame(self,bg='black', width=1366, height=714)
self.frame1 = tk.Frame(self,bg='gray', width=652, height=714)
self.frame.pack()
self.canvas = tk.Canvas(self, background="black", width=714, height=714)
self.canvas.pack_propagate(0)
self.canvas.place(x=652,y=0)
self.frame1.pack_propagate(0)
self.frame1.place(x=0,y=0)
self.longitudecenter = -95.9477127
self.latitudecenter = 36.989772
self.p = 57.935628
global v
s = Canvas(self, width=150, height=20)
s.pack_propagate(0)
s.place(x=0,y=695)
v = Label(s, bg='gray',fg='black',borderwidth=0,anchor='w')
v.pack()
parent.bind("<Motion>", self.on_motion)
self.canvas.focus_set()
self.canvas.configure(xscrollincrement=1, yscrollincrement=1)
def on_motion(self, event):
self.canvas.delete("sx")
self.startx, self.starty = self.canvas.canvasx(event.x),self.canvas.canvasy(event.y)
px = -(round((-self.longitudecenter + (self.p/2))- (self.startx * (self.p/714)),5))
py = round((self.latitudecenter + (self.p/2))-(self.starty * (self.p /714)),5)
v.config(text = "Longitude: " + str(px))
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
This is part of what I've been using. How do I change it so I can bind to to the frame and to the canvas separately. Right now I only need, with the case of the mouse position, to be able to bind to the canvas, but in the future I will need to be able to use mouse clicks, maybe even mouse position separately on the canvas and frame.(who knows given how much this project has changed/advanced since I started it three weeks ago...the sky is the limit).
If you want a binding to only fire for a specific widget, but the binding on that widget rather than on a containing widget.
Change this:
parent.bind("<Motion>", self.on_motion)
To this:
self.canvas.bind("<Motion>", self.on_motion)

tkinter GUI design: managing variables from multiple widgets/toolbars

{Edit: the answer by Bryan Oakley in the suggested duplicate question enter link description here a) fires a response on change to the array variable (arrayvar.trace mode="w"), and I need it triggered on FocusOut, as described in my original question; b) works for Python 2, but I'm having trouble converting it to work in Python 3.5. I'm currently using his and pyfunc's answers as leads and trying to figure out a similar solution using a FocusOut event.}
I am working on a tkinter GUI that lets a user select a particular type of calculation, using a pair of radio button lists. Based on the selections, a tool bar is populated with multiple modular entry widgets, one for each variable the calculation requires. The goal is to have the numerical entry values passed to the model, which will return data to be graphed on a canvas or matplotlib widget.
My question is: what typical strategy is used for gathering and continually refreshing values from multiple widgets, in order to update displays and to pass them on to the model? The trick here is that there will be a large number of possible calculation types, each with their own toolbar. I'd like the active toolbar to be "aware" of its contents, and ping the model on every change to a widget entry.
I think the widgets and the toolbar would have to be classes, where the toolbar can query each widget for a fresh copy of its entry values when a change is detected, and store them as some collection that is passed to the model. I'm not entirely sure how it can track changes to the widgets. Using a "validate='focusout' " validation on the entry widget (e.g. as in
this validation reference )
suggests itself, but I already use "validate='key' " to limit all entries to numbers. I don't want to use "validate=all" and piggyback onto it because I don't want to continually ask the model to do a lengthy calculation on every keypress.
I'm new to GUI programming, however, so I may be barking up the wrong tree. I'm sure there must be a standard design pattern to address this, but I haven't found it.
Below is a screenshot of a mockup to illustrate what I want the GUI to do. The Task radiobutton controls which secondary button menu appears below. The selection in the second menu populates the top toolbar with the necessary entry widgets.
The following code does (mostly) what I want. The ToolBar frame objects will store the values from its contained widgets, and call the appropriate model as needed. The VarBox objects are Entry widgets with extra functionality. Hitting Tab or Return refreshes the data stored in the ToolBar dictionary, tells the ToolBar to send data to the model, and shifts focus to the next VarBox widget.
from tkinter import *
# Actual model would be imported. "Dummy" model for testing below.
def dummy_model(dic):
"""
A "dummy" model for testing the ability for a toolbar to ping the model.
Argument:
-dic: a dictionary whose values are numbers.
Result:
-prints the sum of dic's values.
"""
total = 0
for value in dic.values():
total += value
print('The total of the entries is: ', total)
class ToolBar(Frame):
"""
A frame object that contains entry widgets, a dictionary of
their current contents, and a function to call the appropriate model.
"""
def __init__(self, parent=None, **options):
Frame.__init__(self, parent, **options)
self.vars = {}
def call_model(self):
print('Sending to dummy_model: ', self.vars)
dummy_model(self.vars)
class VarBox(Frame):
"""
A customized Frame containing a numerical entry box
Arguments:
-name: Name of the variable; appears above the entry box
-default: default value in entry
"""
def __init__(self, parent=None, name='', default=0.00, **options):
Frame.__init__(self, parent, relief=RIDGE, borderwidth=1, **options)
Label(self, text=name).pack(side=TOP)
self.widgetName = name # will be key in dictionary
# Entries will be limited to numerical
ent = Entry(self, validate='key') # check for number on keypress
ent.pack(side=TOP, fill=X)
self.value = StringVar()
ent.config(textvariable=self.value)
self.value.set(str(default))
ent.bind('<Return>', lambda event: self.to_dict(event))
ent.bind('<FocusOut>', lambda event: self.to_dict(event))
# check on each keypress if new result will be a number
ent['validatecommand'] = (self.register(self.is_number), '%P')
# sound 'bell' if bad keypress
ent['invalidcommand'] = 'bell'
#staticmethod
def is_number(entry):
"""
tests to see if entry is acceptable (either empty, or able to be
converted to a float.)
"""
if not entry:
return True # Empty string: OK if entire entry deleted
try:
float(entry)
return True
except ValueError:
return False
def to_dict(self, event):
"""
On event: Records widget's status to the container's dictionary of
values, fills the entry with 0.00 if it was empty, tells the container
to send data to the model, and shifts focus to the next entry box (after
Return or Tab).
"""
if not self.value.get(): # if entry left blank,
self.value.set(0.00) # fill it with zero
# Add the widget's status to the container's dictionary
self.master.vars[self.widgetName] = float(self.value.get())
self.master.call_model()
event.widget.tk_focusNext().focus()
root = Tk() # create app window
BarParentFrame = ToolBar(root) # holds individual toolbar frames
BarParentFrame.pack(side=TOP)
BarParentFrame.widgetName = 'BarParentFrame'
# Pad out rest of window for visual effect
SpaceFiller = Canvas(root, width=800, height=600, bg='beige')
SpaceFiller.pack(expand=YES, fill=BOTH)
Label(BarParentFrame, text='placeholder').pack(expand=NO, fill=X)
A = VarBox(BarParentFrame, name='A', default=5.00)
A.pack(side=LEFT)
B = VarBox(BarParentFrame, name='B', default=3.00)
B.pack(side=LEFT)
root.mainloop()

Resources