Getting Tkinter pack_forget to work only on unique objects - python

Ubuntu Linux 11.04 / Python 2.7 / Tkinter
I'm a novice at Python GUIs and I'm having trouble making single objects disappear and reappear. pack_forget kind of works for me, but the way I'm doing it makes all the objects blink in and out, when I only want one at a time. In the boiled-down example below, I'm trying to only make object "a" appear and then disappear and then do the same for object "b".
I would be very grateful for any help, plus some better docs on this would be cool too. The stuff I've been able to find has been light on examples.
from Tkinter import *
import time
class Box:
def __init__(self, canvas):
self.canvas = canvas
def type(self, type):
if type == "1":
self.if = self.canvas.create_rectangle(0, 0, 50, 50, fill="red")
elif type == "2":
self.if = self.canvas.create_rectangle(50, 50, 100, 100, fill="blue")
def hide(self):
self.canvas.pack_forget()
self.canvas.update_idletasks()
root.update()
def unhide(self):
self.canvas.pack()
self.canvas.update_idletasks()
root.update()
root = Tk()
frame = Frame(root)
frame.pack()
canvas = Canvas(frame, width=500, height=200)
canvas.pack()
root.update()
a = Box(canvas)
a.type("1")
b = Box(canvas)
b.type("2")
a.unhide()
time.sleep(.5)
a.hide()
time.sleep(1)
b.unhide()
time.sleep(.5)
b.hide()
time.sleep(1)

There are many things wrong or curious about your code. For one, you should never call sleep in a program that has an event loop. When you call that, your whole program will freeze. If you want something to happen after a fixed period of time, use the after method to schedule a function to run in the future.
Second, there is no point in calling root.update after calling self.canvas.update_idletasks. For one, assume it is a rule written in stone that you should never call update. It's OK to call update if you know the ramifications of that, but since you are just learning and don't know, assume it's unsafe to call it. Plus, update does the same work as update_idletasks and more, so if you do choose to call update, calling update_idletasks is unnecessary.
As to your real problem. You seem to be wanting to create two distinct "Box" objects, and want to hide and show them independently (?). However, both boxes are rectangles that are drawn on the same canvas. When you call pack_forget, that affects the whole canvas so both of these objects will disappear and then reappear.
It isn't clear what your intention is. If each instance of "Box" is just a rectangle on the canvas, you don't want to use pack_forget because that works on widgets, not canvas objects.
If you just want the rectangles to appear and disappear you have several choices. You can destroy and recreate them each time, or you can use the canvas move or coords method to move the item to a non-visible portion of the canvas. You could also manipulate the stacking order to raise or lower an object above or below any or all other objects.
Here's a quick example that uses the trick of moving the items outside the visible area of the canvas.
from Tkinter import *
import time
class Box:
def __init__(self, canvas, type):
self.canvas = canvas
if type == "1":
self.rect = canvas.create_rectangle(0, 0, 50, 50, fill="red")
elif type == "2":
self.rect = canvas.create_rectangle(50, 50, 100, 100, fill="blue")
self.coords = canvas.coords(canvas, self.rect)
def hide(self):
# remember where this object was
self.coords = canvas.coords(self.rect)
# move it to an invisible part of the canvas
self.canvas.move(self.rect, -1000, -1000)
def unhide(self):
# restore it to where it was
self.canvas.coords(self.rect, *self.coords)
root = Tk()
frame = Frame(root)
frame.pack()
canvas = Canvas(frame, width=500, height=200)
canvas.pack()
a = Box(canvas, type="1")
b = Box(canvas, type="2")
root.after(1000, a.hide)
root.after(2000, a.unhide)
root.after(3000, b.hide)
root.after(4000, b.unhide)
root.mainloop()
Finally, if what you really want is for an object to blink continuously, you can have the hide method automatically call the unhide method and visa versa:
def hide(self):
# remember where this object was
self.coords = canvas.coords(self.rect)
# move it to an invisible part of the canvas
self.canvas.move(self.rect, -1000, -1000)
# unhide after a second
self.canvas.after(1000, self.unhide)
def unhide(self):
# restore it to where it was
self.canvas.coords(self.rect, *self.coords)
# re-hide after a second
self.canvas.after(1000, self.hide)

Related

How can I bring a Canvas to front?

I overlapped 5 Tk.Canvas objects and each will have different images. I want to bring each canvas to front of every other canvases to draw pictures in the most-front canvas.
class window_tk():
def __init__(self,main):
self.main=main
self.canvas_org = tk.Canvas(self.main, bg='white')
self.canvas_layer1 = tk.Canvas(self.main, bg='red')
self.canvas_layer2 = tk.Canvas(self.main, bg='green')
self.canvas_layer3 = tk.Canvas(self.main, bg='blue')
self.canvas_layer4 = tk.Canvas(self.main, bg='black')
self.btn_load = tk.Button(self.main,text = "Load Image",command = self.load_ct)
self.btn_layer1 = tk.Button(self.main,text = "Draw in L1",command = self.bring_1)
self.btn_layer2 = tk.Button(self.main,text = "Draw in L2",command = self.bring_2)
self.btn_layer3 = tk.Button(self.main,text = "Draw in L3",command = self.bring_3)
self.btn_layer4 = tk.Button(self.main,text = "Draw in L4",command = self.bring_4)
def bring_1(self):
self.canvas_layer1.place(x=50,y=00)
def bring_2(self):
self.canvas_layer2.place(x=50, y=00)
def bring_3(self):
self.canvas_layer3.place(x=50, y=00)
def bring_4(self):
self.canvas_layer4.place(x=50, y=00)
I thought the canvas.place() function will bring the canvas front but it was not. Which function can I use ? Or should I unpack all other canvases ?
Since Canvas has override the .tkraise() function, you need to call TCL command directly:
self.canvas.tk.call('raise', self.canvas._w)
Please see the answer given by acw1668. The lift function doesn't work for Canvas objects. His answer is correct.
All tkinter objects, Canvas included, support the following method:
w.lift(aboveThis=None)
If the argument is None, the window containing w is moved to the top of the window stacking order. To move the window just above some Toplevel window w, pass w as an argument.
This gives you full control over which widget sits on top.
https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/universal.html
Now that I re-read that, I see that its language is slightly incorrect. "w" is any tkinter widget, "above_this" is another tkinter widget. The function places "w" above "above_this" in the stacking order.
You can use the following functions -
canvas.tag_raise(canvas_layer4) -> For bringing to front
canvas.tag_lower(canvas_layer4) -> For pushing back

Tkinter: Button in a rectangle on a canvas of frame in Python 3.

I have been trying to create a moving oval that is more like a button. As of now I don't intend to perform any function on clicking the button, so I have created a pass function.
The scenario is as follows:
The frame in the tkinter window is covered by a canvas in magenta color, with a circle (in yellow), at a designated position. This circle is actually a button, which when pressed opens a popup menu displaying some info (not essential right now). I have managed to create the circle but struggling to incorporate a button into the oval. The button and the circle must be together, because this pair is supposed to move in the canvas frame every 3 second (sounds like a GPS dot but the dot being a button).
But when i am trying to create the button, the canvas vanishes, and the frame resizes according to the width of the button.
Kindly help me in identifying the mistake and the correct code accordingly:
enter code here
from tkinter import *
import random
import time
def nothing():
pass
main = Tk()
frame_1 = Frame(main)
frame_1.grid(row=0, column=0)
main_canvas = Canvas(frame_1, width=200, height=200, bg='magenta')
oval = main_canvas.create_oval(20, 20, 40, 40, outline='black', fill='yellow')
main_canvas.pack()
frame_2 = Frame(main)
frame_2.grid(row=0, column=1)
'''
button2 = Button(main_canvas, text="Q", command=nothing, anchor=W)
button2.configure(width=3, activebackground="#33B5E5", relief=FLAT)
button2_window = main_canvas.create_window(10, 10, anchor=NW, window=button2)
button2.pack(side=TOP)
'''
label_f2_1 = Label(frame_2, text="")
label_f2_1.pack()
label_f2_2 = Label(frame_2, text="")
label_f2_2.pack()
x_current, y_current = 30, 30
for loops in range(86400):
x_new = random.randint(10, 190)
y_new = random.randint(10, 190)
main_canvas.move(oval, x_new-x_current, y_new-y_current)
x_current, y_current = x_new, y_new
main_canvas.update()
time.sleep(1)
now = str(time.ctime())
label_f2_2.configure(text=now[10:])
label_f2_1.configure(text=now[0:10])
# print(time.localtime())
main.mainloop()
Using PyEdu running on Python 3.7 interpreter.
The canvas vanishes because that's the behavior of pack -- it will cause a widget to grow or shrink to fit its children. The button is a child of the canvas, so the canvas tries to shrink to fit the canvas.
This solution is wrong in many ways, For example, to add a widget to a canvas you shouldn't use pack, place, or grid. Instead, you should use the canvas method create_window.
However, you don't need a button inside the oval at all. You can create a binding that will call a function whenever the user clicks directly on the oval.
For example, the following will cause the function showInfo to be called whenever the oval is clicked:
def showInfo(event):
print("you clicked on the oval")
main_canvas.tag_bind(oval, "<1>", showInfo)
HOWEVER, this won't work very well because of how you're doing your animation loop. Your code is sleeping more than anything else, which makes it very hard for tkinter to process events. Tkinter cannot process events while sleep is sleeping.
Instead of using a for loop, you need to write a function that does everything that you are doing inside the loop (ie: it draws one frame of animation). This function can use after to call itself again in the future, setting up a perpetual loop.
For example:
def doOneIteration():
newx = random.randint(10, 190)
newy = random.randint(10, 190)
main_canvas.coords(oval, newx, newy, newx+20, newy+20)
now = str(time.ctime())
label_f2_2.configure(text=now[10:])
label_f2_1.configure(text=now[0:10])
main.after(1000, doOneIteration)
doOneIteration()
The first time you call doOneIteration, it will do everything that was in the body of loop. Then, when it's finished it will cause itself to be called again in one second. The next time it's called, it will do the work and then again cause itself to be called in one second. It will do this for as long as the program runs.
The advantage here is that at no time are you causing the program to go to sleep. This insures that tkinter is able to process a steady stream of events without interruption.

Canvas resizing with window - good way to do it?

i created a window with a canvas inside. The canvas contains an rectangle. Both change their size through a callback together with the window size.
So my beginner question is: It works fine, but is this a good or common way to do this? Or is there a more efficient/common way?
from tkinter import* #
#creating instance of tkinter
obj = Tk()
#Set title of our window form
obj.title("MyFirst Window - WOW")
#Set dimension of form
x_size = 1200
y_size = 600
obj.geometry(str(x_size)+"x"+str(y_size))
obj.update()
w = Canvas(obj, width=x_size, height=y_size)
w.place(x=0,y=obj.winfo_height()-100)
w.create_rectangle(0, 0, obj.winfo_width(), 100, fill="#476042")
def callback(event):
print(str(obj.winfo_width())+'x'+str(obj.winfo_height()))
w.config(width=obj.winfo_width(),height=obj.winfo_height())
w.place(x=0,y=obj.winfo_height()-100)
w.create_rectangle(0, 0, obj.winfo_width(), 100, fill="#476042")
window = obj
window.bind("<Configure>", callback)
obj.mainloop()
No, this is not a good way to have the canvas resize. You should almost never use place. grid and pack make it much easier to create widgets that automatically resize.
For example, if you want the canvas to always be 100 pixels tall and fill the full width of the window, you can add it to obj like this:
w = Canvas(obj, width=x_size, height=100)
w.pack(side="bottom", fill="x")
As for the green rectangle, you have no choice but to use a binding on <Configure> if you want the rectangle to grow and shrink along with the canvas.
However, your callback creates a new rectangle every time it's called instead of modifying the coordinates of the existing rectangle. This is a memory leak, because your program will use more and more memory the longer it runs and the more often the window is resized. w.create_rectangle will return an identifier; you can use that identifier to later modify the rectangle.
Here's a simplified version of your code. I've changed the variable names to make it a bit easier to comprehend.
from tkinter import *
window = Tk()
window.title("MyFirst Window - WOW")
x_size = 1200
y_size = 600
window.geometry(str(x_size)+"x"+str(y_size))
window.update_idletasks()
canvas = Canvas(window, width=x_size, height=100, background="bisque")
canvas.pack(side="bottom", fill="x")
rect = canvas.create_rectangle(0, 0, window.winfo_width(), 100, fill="#476042")
def callback(event):
canvas.coords(rect, 0, 0, canvas.winfo_width(), 100)
canvas.bind("<Configure>", callback)
window.mainloop()

tkinter: Returning the (x,y) results of a mouse button click event to be used as a variable

I am eventually trying to run a code that's a bit more sophisticated than this, but this is the hump I'm trying to get over. I just need to be able to run a code that takes the x,y coordinates of the pixel clicked and records them as either a variable or appends them to a list. For right now I have them set as L_Click_x and L_Click_y, I would eventually like to be able to add these coordinates to a list or be able to call upon them in the function. Nothing I have tried seems to work, I'm pretty new to this so there's probably something fundamental that I'm just not getting. (Also the red lines that appear are just for visual confirmation of the click)
from tkinter import *
def motion(event):
x, y = event.x, event.y
print("Current Position = ",(x,y))
def LeftClick(event):
L_Click_x, L_Click_y = event.x, event.y
redline = canvas.create_line(0,L_Click_y,400,L_Click_y,fill = "red")
redline = canvas.create_line(L_Click_x,0,L_Click_x,300,fill = "red")
print("Left Click = ",(L_Click_x,L_Click_y))
root = Tk()
root.bind('<Motion>',motion)
root.bind('<Button-1>',LeftClick)
canvas = Canvas(root, width = "400",height = "300")
canvas.pack()
root.mainloop()
You will need to keep track of the first clicked point, until the second is known, and then draw a line segment between the two.
This simple example uses a global variable, and the points are discarded when drawn; you will probably want to use a container to keep track of the line segments, and a program construction that allows you to access it without making it global.
Please note that I bound the events to the canvas instead of root; I changed the imports to avoid cluttering the namespace (as your app grows, you'll be glad you did), and changed the camelcase names to a more pythonic convention.
import tkinter as tk
def motion(event):
x, y = event.x, event.y
print("Current Position = ", (x, y))
def left_click(event):
global line_segment
line_segment.append(event.x)
line_segment.append(event.y)
canvas.create_oval(event.x+1, event.y+1, event.x-1, event.y-1)
if len(line_segment) == 4:
canvas.create_line(*line_segment, fill="red")
line_segment = []
if __name__ == '__main__':
line_segment = []
root = tk.Tk()
canvas = tk.Canvas(root, width="400", height="300")
canvas.pack()
canvas.bind('<Motion>', motion)
canvas.bind('<Button-1>', left_click)
root.mainloop()

Python detect two keys at once in tkinter

Finally figured out how to scroll the screen using only the keyboard in tkinter, took getting on the right website to show me the answer. Now I have one other small, but rather important problem that I'm experiencing.
The program is setup to scroll the underlying image using the cursor keys. If I push two keys(up/left) at the same time it will scroll either up one and left forever or up one and left forever instead of constantly switching back and forth.
How do get it to recognize that I'm pushing both and holding them down? It only recognizes one of the two, no matter which two keys I'm holding down.
import tkinter as tk
import random
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.canvas = tk.Canvas(self, background="bisque", width=400, height=400)
self.canvas.pack(fill="both", expand=True)
self.canvas.configure(scrollregion=(-1000, -1000, 1000, 1000))
self.canvas.bind("<Left>", self.keyleft)
self.canvas.bind("<Right>", self.keyright)
self.canvas.bind("<Up>", self.keyup)
self.canvas.bind("<Down>", self.keydown)
self.canvas.focus_set()
# the following two values cause the canvas to scroll
# one pixel at a time
self.canvas.configure(xscrollincrement=1, yscrollincrement=1)
# finally, draw something on the canvas so we can watch it move
for i in range(1000):
x = random.randint(-1000, 1000)
y = random.randint(-1000, 1000)
color = random.choice(("red", "orange", "green", "blue", "violet"))
self.canvas.create_oval(x, y, x+20, y+20, fill=color)
def keyup(self,event):
self.canvas.yview_scroll(-1,'units')
def keydown(self,event):
self.canvas.yview_scroll(1,'units')
def keyleft(self,event):
self.canvas.xview_scroll(-1,'units')
def keyright(self,event):
self.canvas.xview_scroll(1,'units')
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
This isn't a tkinter problem; it's the way your OS handles the long pressing a key. Go to a text editor and press some keys (text keys, not arrows) and you'll see the same behavior. You OS probably has some settings to modify that behavior.
You could take over the press and hold behavior in tkinter and handle multiple keys that way, but that would require disabling this feature in your OS first. How you do that is OS-specific, and I doubt it's possible to disable it for your application only.
Edit: if you are ok with shutting off the OS key repeat feature either manually or programatically, you could use this code to have tkinter take over the key repeat:
import tkinter as tk
import random
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.keys = dict.fromkeys(('Left', 'Right', 'Up', 'Down'))
self.canvas = tk.Canvas(self, background="bisque", width=400, height=400)
self.canvas.pack(fill="both", expand=True)
self.canvas.configure(scrollregion=(-1000, -1000, 1000, 1000))
parent.bind("<KeyPress>", self.keypress)
parent.bind("<KeyRelease>", self.keypress)
self.canvas.focus_set()
# the following two values cause the canvas to scroll
# one pixel at a time
self.canvas.configure(xscrollincrement=1, yscrollincrement=1)
# finally, draw something on the canvas so we can watch it move
for i in range(1000):
x = random.randint(-1000, 1000)
y = random.randint(-1000, 1000)
color = random.choice(("red", "orange", "green", "blue", "violet"))
self.canvas.create_oval(x, y, x+20, y+20, fill=color)
self.looper() # start the looping
def keypress(self,event):
if event.keysym in self.keys:
# event type 2 is key down, type 3 is key up
self.keys[event.keysym] = event.type == '2'
def looper(self):
if self.keys['Up']:
self.canvas.yview_scroll(-1,'units')
if self.keys['Down']:
self.canvas.yview_scroll(1,'units')
if self.keys['Left']:
self.canvas.xview_scroll(-1,'units')
if self.keys['Right']:
self.canvas.xview_scroll(1,'units')
self.after(20, self.looper) # set the refresh rate here ... ie 20 milliseconds. Smaller number means faster scrolling
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
Edit edit: Some googling suggests that some OS's send repeated 'press' signals rather than the press - release - press - release cycle that I see in Linux Mint. If your OS does then you may be able to use this code without disabling the autorepeat.

Categories