How do you hide a canvas so it only shows when you want it displayed?
self.canvas.config(state='hidden')
just gives the error saying you can only use 'disabled' or 'normal'
In the comments you say you are using pack. In that case, you can make it hidden by using pack_forget.
import tkinter as tk
def show():
canvas.pack()
def hide():
canvas.pack_forget()
root = tk.Tk()
root.geometry("400x400")
show_button = tk.Button(root, text="show", command=show)
hide_button = tk.Button(root, text="hide", command=hide)
canvas = tk.Canvas(root, background="pink")
show_button.pack(side="top")
hide_button.pack(side="top")
canvas.pack(side="top")
root.mainloop()
However, it's usually better to use grid in such a case. pack_forget() doesn't remember where the widget was, so the next time you call pack the widget may end up in a different place. To see an example, move canvas.pack(side="top") up two lines, before show_button.pack(side="top")
grid, on the other hand, has a grid_remove method which will remember all of the settings, so that a subsequent call to grid() with no options will put the widget back in the exact same spot.
Related
I've encountered the problem, that I can not create a child widget of a child widget. I've tried using a class to create a window with widgets and alternatively a function to see if I don't understand something about how classes work, but the failure was the same.
I've boiled down the issue to the following code:
import tkinter as tk
root = tk.Tk()
root.geometry('900x400')
frame1=tk.LabelFrame(root, text="frame1",width=800, height=400, bg='blue', bd=5).pack()
labelframe1=tk.LabelFrame(frame1, text="labelframe1",width=300, height=300, bg='green', bd=15).pack()
labelframe2=tk.LabelFrame(frame1, text="labelframe2",width=300, height=300, bg='yellow', bd=15).pack()
button1 = tk.Button(labelframe1,text="text").pack()
I don't get errors with this code, but the outcome is the same. Only the child of root is created, but not the children of the root child.
How can I create "grandchild" widgets?
In python, x().y() returns the value of y(). Thus, when you do tk.LabelFrame(...).pack() it returns None. Therefore, when you do tk.Button(labelframe1, ...) it's the same as tk.Button(None, ..., which causes the button to be a child of the root window.
Bottom line, it's a best practice to separate widget creation from widget layout. I also personally find that grouping layout code together makes the code much easier to understand. I think it's also a good practice to explicitly define which side the widget is being packed into.
frame1=tk.LabelFrame(root, text="frame1",width=800, height=400, bg='blue', bd=5))
labelframe1=tk.LabelFrame(frame1, text="labelframe1",width=300, height=300, bg='green', bd=15)
labelframe2=tk.LabelFrame(frame1, text="labelframe2",width=300, height=300, bg='yellow', bd=15)
button1 = tk.Button(labelframe1,text="text")
frame1.pack(side="top")
labelframe1.pack(side="top")
labelframe2.pack(side="top")
button1.pack(side="top").
button1 = tk.Button(labelframe1,text="text").pack()
I'm making a picture gallery that exists independently in several places within the app. When any of the galleries close, the current picture displayed in it is posted to a central graphics edit area. So the pictures were gridding on top of each other and I was planning to add code to delete the one that was already there when I stumbled on some unexpected behavior that seems to make this unnecessary. When I used the name option to give the container a name, instead of gridding a new container and picture on top of the existing one, apparently the old one was destroyed and replaced automatically.
My question is, can I rely on this behavior, or is it some kind of fluke? I can't find any documentation on it. From Fredrick Lundh, one of Tkinter's authors, "If you really need to specify the name of a widget, you can use the name option when you create the widget. One (and most likely the only) reason for this is if you need to interface with code written in Tcl." So I'm worried that I'll be relying on a fluke but so far it's a good fluke because I don't have to figure out how to keep widgets from gridding on top of each other if tkinter is going to auto-destroy the old one for me. It's not that hard but this name option thing is easier.
The example shows this behavior in action. If you remove the name option from the functions that create the widgets, the widgets grid on top of each other. But with the name option used, the widgets replace each other.
import tkinter as tk
def create():
f1 = tk.Frame(root, bg='red', name='exists') #
f1.grid(column=0, row=0)
l1 = tk.Label(f1, bg='yellow', text='This is a very long sentence.')
l1.grid(column=0, row=0)
count_labels()
def replace():
f1 = tk.Frame(root, bg='green', name='exists') #
f1.grid(column=0, row=0)
l1 = tk.Label(f1, bg='tan', text='short sentence')
l1.grid(column=0, row=0)
count_labels()
def count_labels():
root.update_idletasks()
for child in root.winfo_children():
x = child
print(len(x.winfo_children()))
root = tk.Tk()
b1 = tk.Button(root, text='create', command=create)
b1.grid(column=0, row=1)
b2 = tk.Button(root, text='replace', command=replace)
b2.grid(column=1, row=1)
root.mainloop()
Imagine there are two Tkinter.Toplevel() windows, called Window_1 and Window_2, which can be opened by clicking the same button (lets called Button_0).
Button_0 is pressed and Window_1 pops up. In Window_1, I can scroll up and down using a mouse pad (MAC OS). After that, I left Window_1 open.
Button_0 is pressed again and Window_2 pops up, while Window_1 stays open. In Window_2, I can again scroll up and down.
Now, I go back to Window_1 and try to scroll using mouse pad, contents in Window_1 DO NOT MOVE, but contents in Window_2 DO MOVE.
Then I close Window_2, and try to scroll on Window_1, this time I got error messages asking for a canvas on Window_2.
I did bind function,
def on_vertical(canvas,event):
canvas.yview_scroll(-3 * event.delta, 'units')
to a canvas inside each windows. As far as I know about the error, it seems that this function could not be used twice at the same time (both windows are opened).
I would like the way that when both Windows stay open. While on each window, I can scroll up-down while the another one do not move. Is it possible to code that?
This is the code example (please do noted that the Window name is not corrected label.)
from tkinter import *
######################## FUNCTIONS (DEF) ########################
def on_vertical(canvas,event):
canvas.yview_scroll(-3 * event.delta, 'units')
######################## FUNCTIONS (CLASS) ########################
class Window(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.master = master
self.init_window()
#INITIAL WINDOW
def init_window(self):
self.master.title("Main Window")
self.pack(fill=BOTH, expand=1)
Button(self, text="Button_0",command = self.load_and_print).place(x = 7, y = 95)
# creating a button instance
Button(self, text="EXIT PROGRAM", command=self.client_exit).place(x=500, y=250)
#OPEN A NEW WINDOW CONTAINING STOCK LISTS
def load_and_print(self):
new_window = Toplevel(self)
new_window.title("Window")
canvas = Canvas(new_window, width = 800, height = 500, scrollregion = (0, 0, 0, 2500))
frame = Frame(canvas)
vbar = Scrollbar(new_window, orient = VERTICAL, command = canvas.yview)
vbar.pack(side = RIGHT,fill = Y)
canvas.create_window(0,0, window = frame, anchor = NW)
canvas.config(yscrollcommand = vbar.set)
canvas.pack(side = TOP,expand = True,fill = BOTH)
canvas.bind_all('<MouseWheel>', lambda event, canvas=canvas: on_vertical(canvas,event))
#MAKE PROGRAM EXIT
def client_exit(self):
exit()
######################## MAIN PROGRAMME ########################
#call window
root = Tk()
#size of the window
root.geometry("700x300")
app = Window(root)
root.mainloop()
root.update()
The problem is that you are using bind_all instead of bind for the mousewheel event.
Because you're using bind_all, each time you create a new window it replaces the old binding with a new binding. No matter which window gets the event, your function will always only work for the last window to be created. And, of course, when that window is destroyed then the mouse binding will throw an error since the canvas no longer exists.
Using bind
One solution is simple: use bind instead of bind_all.
canvas.bind_all('<MouseWheel>', lambda event, canvas=canvas: on_vertical(canvas,event))
Using bind_all
If you want the benefits of bind_all -- namely, that the scrolling works even if the mouse is over some other widget, you need to modify on_vertical to figure out which canvas to scroll at the time that it is running instead of having the canvas being passed in.
You can do that with a little bit of introspection. For example, the event object knows which widget received the event. From that you can figure out which window the mouse is in, and from that you can figure out which canvas to scroll.
For example, move the binding up to the __init__ and change it like this:
self.bind_all('<MouseWheel>', on_vertical)
Next, change on_vertical to figure out which canvas to scroll. In the following example I assume each toplevel has exactly one canvas and that you always want to scroll that canvas (ie: you lose the ability to scroll text widgets and listboxes)
If that's not the case, you can add whatever logic you want to figure out which widget to scroll.
def on_vertical(event):
top = event.widget.winfo_toplevel()
for child in top.winfo_children():
if child.winfo_class() == "Canvas":
child.yview_scroll(-3 * event.delta, 'units')
break
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)
So, I have a small Python 3 project that should have a window composed of 3 main areas:
Tool bar (top);
Table/treeview with database data (center) - this table should be able to grow downwards, with scrollbars, but I still do not know how to make them work with treeview;
Status bar (bottom).
I am also trying to add a quick data entry form that should apear on user demand right above the status bar. I think I have been able to create the main structure for this window, using pack(). The data entry form also shows up and goes away when the user clicks the proper buttons. But there are a couple of things that I don't know how to solve. The first one is that when the user resizes the window, making it smaller, the status bar and the data entry form both disappear under the table/treeview. How can I make them be always on top?
Here is my code:
#!/usr/local/bin/python3
# encoding: utf-8
from tkinter import *
from tkinter import ttk
entryform_visible = 0
class mainWindow:
def __init__(self, master):
self.master = master
master.title("AppTittle")
master.minsize(width=700, height=200)
def show_entryform():
global entryform_visible
if entryform_visible == 1:
hide_entryform()
return
else:
entryform_visible = 1
# Form for data entry (bottom of main window)
status_txt.set("Introducing new data...")
self.bottomframe.pack(side=BOTTOM, fill=X)
def hide_entryform():
global entryform_visible
self.bottomframe.pack_forget()
entryform_visible = 0
status_txt.set("Introduction of data was cancelled.")
def add_register():
hide_entryform()
status_txt.set("New register added!")
# Tool bar (buttons)
self.topframe = ttk.Frame(root, padding="3 3 12 12")
self.topframe.pack(side=TOP, fill=X)
btn_add = ttk.Button(self.topframe, text="Button1").pack(side=LEFT)
btn_add = ttk.Button(self.topframe, text="+", width=2, command=show_entryform).pack(side=RIGHT)
btn_add = ttk.Button(self.topframe, text="Button2").pack(side=RIGHT)
# Data table (center area of window)
self.mainframe = ttk.Frame(root)
self.mainframe.pack(side=TOP, fill=X)
tree = ttk.Treeview(self.mainframe, height=25, selectmode='extended')
tree['columns'] = ('id', 'name', 'descr')
tree.pack(side=TOP, fill=BOTH)
tree.column('#0', anchor=W, minwidth=0, stretch=0, width=0)
tree.column('id', anchor=W, minwidth=30, stretch=0, width=40)
tree.column('name', minwidth=30, stretch=1, width=30)
tree.column('descr', minwidth=100, stretch=1, width=200)
tree.heading('id', text="ID")
tree.heading('name', text="Name")
tree.heading('descr', text="Description")
# Data entry form
self.bottomframe = ttk.Frame(root, padding="3 3 12 12")
ttk.Label(self.bottomframe, text="Field 1").pack(side=LEFT)
text_input_obj = ttk.Entry(self.bottomframe, width=13)
text_input_obj.pack(side=LEFT)
text_input_obj.focus_set()
btn_add = ttk.Button(self.bottomframe, text="Add", command=add_register).pack(side=RIGHT)
# We are going to pack() this later (when the user clicks the '+' button)
# Status bar
self.statusframe = ttk.Frame(root, padding="2 2 2 2")
self.statusframe.pack(side=BOTTOM, fill=X)
status_txt = StringVar()
self.statusbar = ttk.Label(self.statusframe, textvariable=status_txt).pack(side=LEFT)
root = Tk()
appwindow = mainWindow(root)
root.mainloop()
The first one is that when the user resizes the window, making it
smaller, the status bar and the data entry form both disappear under
the table/treeview. How can I make them be always on top?
This has to do with how the packer works. It will start chopping off widgets when the space available is less than the space needed. It does this in the reverse order of how widgets were packed.
In this case, because the bottom frame was packed last, when you shrink the window to be too small to fit everything, pack first shrinks any windows that might have expanded to larger than their default size, and then starts chopping off widgets starting with the last widget that was packed. In this case it is self.statusframe that gets chopped since it was the last to be packed.
There are two ways to solve this. One is to simply reverse the order that you pack everything into the root window. The other is to initially make the tree widget very small, and then let it grow to fill the space. Since the natural size of the treeview is small, tkinter will shrink it before it starts removing widgets from view. If you do this second technique, you can use wm_geometry to make the window large when it starts out (otherwise the tree won't have any space to grow into)
My advice is to always group your pack and grid statements. It makes changes like this very easy, and it make visualizing your code much easier. For example, using the first technique I would move the packing of the main areas to the bottom of the function:
# layout the main GUI
self.topframe.pack(side=TOP, fill=X)
self.statusframe.pack(side=BOTTOM, fill=X)
self.mainframe.pack(side=TOP, fill=BOTH, expand=True)
With this, when space gets tight pack will start to chop off the tree. But since the tree is big, there's plenty to chop before it starts to disappear.
When you do this, you'll expose another problem in your layout, and that is that you don't have the treeview expanding to fill extra space in mainframe. You need to pack it like this:
tree.pack(side=TOP, fill=BOTH, expand=True)
This brings up another problem. Namely, your popup bottomframe may pop up hidden. That is because pack isn't designed to work well in this sort of situation. Again, that is because pack will start to chop off widgets in the reverse order that they were added. Since you're adding it last, it's the first widget to get chopped off. If the user resizes the window to be smaller, packing it at the bottom may cause it to never appear.
A better solution in this specific case is to not use pack for this one widget. Instead, since you want it to "pop up", you can use place. place is perfect for popping up widgets on top of other widgets. The only other thing you need to do is make sure that this frame is higher in the stacking order than the statusbar, which you can do with lift:
def show_entryform():
...
self.bottomframe.place(relx=.5, rely=1, anchor="s", relwidth=1.0)
self.bottomframe.lift()
And, of course, use place_forget instead of pack_forget:
def hide_entryform():
...
self.bottomframe.place_forget()
...