I get more than one master from widget.master (Python 3) - python

I just wrote a class in Tkinter, that allows me to make some widgets draggable. This really works great as long as the master of the widget is the root window. But if I, for example, have a Frame with a Lable in it, I can drag the Frame but the Lable just disappears. This is cause the class places the label in relation to the main window. So for example, if the Frame has the size 100x100 and is at the position 500, 500 at the main window, and I drag the Label(0, 0) 1px to the right, it will be placed at 501, 500 instead of 1, 0 cause the class thinks the master is the root window.
So now I thought to just use the master's position to subtract it from the Label position (501-500, 500-500 > 1, 0) There's just one problem. This:
f = Frame(root, width=100, height=100, bg='grey')
f.place(x=500, y=500)
l = Label(f, text='Drag me!)
l.place(x=0, y=0)
master = l.master
print(master)
returns me not one master but two. Even if there's just one print statement, it gives me this:
>>.
>>.!frame
If I put a sleep statement in between the declaration and the print it just takes longer. But if I check the types it's not a list, this are two objects.
Can anybody explain this? I just need a way to get the master of the Label to get it's position!

Tkinter widgets exist in a tree structure, and there must be an instance of Tk at the root. If you don't create one, it will be created for you the first time you create a widget.
In your output, "." represents the root window that was presumably created for you, and ".!frame" represents the frame.
Though, given the fact that you explicitly pass root to Frame(), it looks like your code is explicitly creating the root window somewhere.

I found the problem. It's not a problem with the code. I just didn't noticed I also müde the Frame dragable. And cause the Frames parent is root I got it.

Related

When designing a desktop app to be resizable, are there any hidden drawbacks to using two sequential Tk instances?

In order to ensure that my program will be compatible with any screen size, I first have a very small 'setup' window open when the program is launched, which will then allow the user to select the desired dimensions for the main window.
Once the 'finalize' button on the 'setup' window is pressed, the setup window disappears and the main window opens. This is handled by calling .destroy() on the setup window and creating a new instance with Tk() inside the button's command function.
To make the example code more compact and highlight the part which is relevant to this question, I left out the size selector and just set the main window to be a fixed 800x800 pixels here:
import tkinter as tk
# Start out small, to fit on any screen size
startWindow = tk.Tk()
startWindow['width'] = 400
startWindow['height'] = 200
startWindow.title("Setup")
# Use lists here so that the widgets created inside 'initializeMainWindow' will be
# accessible from the global scope.
mainWindow = [None]
mainWindowButtons = [None]
# Closes the 'setup' window and opens a new window which will be the main application.
# Also initializes all widgets which will belong to the new window.
def initializeMainWindow():
startWindow.destroy()
mainWindow[0] = tk.Tk()
print("New window initialized.")
mainWindow[0]['width'] = 800
mainWindow[0]['height'] = 800
mainWindow[0].title("Main Window")
mainWindowButtons[0] = tk.Button(master=mainWindow[0], text="Test", command=testNewWindow)
mainWindowButtons[0].place(x=350, y=375, width=100, height=50)
mainWindow[0].bind('<Key>', test2)
#mainWindow[0].mainloop()
# To demonstrate that the new window is interactive
def testNewWindow():
print("Success!")
# Works whether or not 'mainloop' is called on the new window
def test2(e):
print("Also success! '" + e.keysym + "' key pressed.")
setSizeButton = tk.Button(master=startWindow, text="Resize", command=initializeMainWindow)
setSizeButton.place(x=150, y=75, width=100, height=50)
# This prints BEFORE the 'setup' window is closed, as expected
# If 'mainloop' is uncommented, it works the same except IDLE won't show the '>>>' prompt
# after the text "Not yet initialized".
if startWindow:
print("Not yet initialized.")
#startWindow.mainloop()
This works exactly as I intend it to so far. However I'm aware that when an application has more than one window, it's standard to use Toplevel() and not create multiple instances of Tk(). But this example isn't trying to run multiple instances of Tk() at once, instead, they're sequential: think of it as a separate 'launcher' program that then opens the main program, as is common on many desktop games. (This is exactly how I'm using it in the full program).
Before building on a potentially flawed foundation, I'd like to know if there are any hidden problems which could surface later with this approach. If the consensus is that it's better to switch to using Toplevel() or even have two separate Python files, I'd rather find out sooner than later!
I have already viewed this question and answer:
What's the difference between tkinter's Tk and Toplevel classes?
but they don't cover this specific question.
A related issue: I also experimented with calling .mainloop() vs. not calling it, and at least with the Mac version of IDLE and using Python 3.9.4, it seems to be optional. This was discussed here When do I need to call mainloop in a Tkinter application?
and it looks like the reason omitting it still works is that IDLE has its own event loop (credit to Ori for this solution https://stackoverflow.com/a/8684277/18248018).
If this is the case, is it advisable to explicitly call .mainloop() anyway (where I have it commented out in the example code) for reliability? I haven't tested this yet, but if it's something IDLE does, I'd guess the automatic event loop functionality might not transfer over when I convert the program to a standalone app using py2app, without explicit calls to .mainloop() in the .py file.
Here's the code in your answer with a minor change — the finalFrame doesn't get created until the openMainWindow() function is called since it's not needed until then. I think this is a little more logical instead of having creation and usage scattered about.
import tkinter as tk
window = tk.Tk()
# Start out with a small popup, so it will fit on any screen size.
# Width and height must be specified as attributes of the frame, so that the
# frame can set the window's size.
setupFrame = tk.Frame(master=window, bg='green', width=400, height=200)
# Using 'pack' will cause the window's size to be equal to setupFrame's size
# Using 'place' here would not work: the window would open with the default size
# (small and square) and cut off the frame.
setupFrame.pack()
# In the actual program, these values will be determined by user input
W = 800
H = 800
def openMainWindow(w, h):
# finalFrame # Uncomment if ever needed.
# Destroy the 'launcher' frame once it is no longer needed.
setupFrame.destroy()
# This frame will reset the size of the window and will display the
# program's main content.
finalFrame = tk.Frame(master=window, bg='lightblue', width=w, height=h)
sizeScalingExample = tk.Label(master=finalFrame, bg='purple', fg='white',
text="This label's size is set with `place` and "
"depends on the frame's size.")
finalFrame.pack()
sizeScalingExample.place(x = 0.125*w, y = 0.375*h, width=0.75*w, height=0.25*h)
resizeButton = tk.Button(master=setupFrame, text="Finalize", bg="yellow",
fg="darkblue", command=lambda: openMainWindow(W, H))
resizeButton.place(x=150, y=75, width=100, height=50)
window.mainloop()
Following martineau's suggestion in the comments of switching between two different Frame widgets, I rewrote the code in my question as the following. This achieves the same visual effect as the original code, and eliminates any need for a second Tk() instance.
This is a minimal example of the method I will be using in my resizable application:
import tkinter as tk
window = tk.Tk()
# Start out with a small popup, so it will fit on any screen size.
# Width and height must be specified as attributes of the frame, so that the
# frame can set the window's size.
setupFrame = tk.Frame(master=window, bg='green', width=400, height=200)
# Using 'pack' will cause the window's size to be equal to setupFrame's size
# Using 'place' here would not work: the window would open with the default size
# (small and square) and cut off the frame.
setupFrame.pack()
# This frame will reset the size of the window and will display the program's
# main content
finalFrame = tk.Frame(master=window, bg='lightblue')
sizeScalingExample = tk.Label(master=finalFrame, bg='purple', fg='white',
text="This label's size is set with `place` and "
"depends on the frame's size.")
# In the actual program, these values will be determined by user input
W = 800
H = 800
def openMainWindow(w, h):
# Hide the 'launcher' once it is no longer needed
setupFrame.pack_forget()
finalFrame['width'] = w
finalFrame['height'] = h
finalFrame.pack()
sizeScalingExample.place(x = 0.125*w, y = 0.375*h, width=0.75*w, height=0.25*h)
resizeButton = tk.Button(master=setupFrame, text="Finalize", bg="yellow",
fg="darkblue", command=lambda: openMainWindow(W, H))
resizeButton.place(x=150, y=75, width=100, height=50)
window.mainloop()
Although the ability to implement the same functionality using only Frame widgets means there is no practical reason to use a second Tk() instance, I'd still be interested from a theoretical perspective to learn about any unexpected outcomes which could result from using the original approach.

tkinter LabelFrame not attatching widgets

I am running into a problem with a tkinter program, I have the LabelFrame grouping a set of labels and entries, however, it is not grouping my widgets. My code for the LabelFrame is as follows:
(edit: i managed to get the Label to display, however, it is not grouping my widgets.)
root=Tk()
message_frame=LabelFrame(root,text="testing",padx=0,pady=0,width=100,height=100).grid(padx=5,pady=10)
message_label=Label(message_frame,text="Message").grid(row=1,column=0,sticky=W)
pub_label=Label(message_frame,text="Public Key").grid(row=2,column=0,sticky=W)
priv_label=Label(message_frame,text="Public Key").grid(row=3,column=0,sticky=W)
message_entry=Entry(message_frame,textvariable=message,width=50).grid(row=1,column=1,sticky=W)
pub_entry=Entry(message_frame,textvariable=pub_key,width=50).grid(row=2,column=1,sticky=W)
priv_entry=Entry(message_frame,textvariable=private_key,width=50).grid(row=3,column=1,sticky=W)
In Tkinter, the typical workflow is to create a widget and then place it using some geometry manager on two separate lines.
If I'm not mistaken, the .grid method on Tkinter widgets returns None. So if you print message_frame right after you create it, you will probably see that it is None. When you use that passed to the next widgets, they assume you want to put it on the root widget...
The easy fix is to do something like:
message_frame=LabelFrame(root,text="testing",padx=0,pady=0,width=100,height=100)
message_frame.grid(row=0,column=0)
And you probably want to do the same with all the widgets since I doubt you actually want pub_label = priv_label = None ...
Give the frame some size attributes:
from Tkinter import *
root = Tk()
message_frame = LabelFrame(root,text="testing",padx=0,pady=0,width=100,height=100).grid(row=0,column=0,padx=5,pady=10)
Once the width and height are defined, the frame shows up fine.
If you post some of your frame's contents, it might make it clearer if this is not the issue.

tkinter's .pack_propagate() method

I am experimenting with Tkinter, as I was trying to figure out is there a way to set the tkinter's window size without using canvas. I came upon this how to set frame size question on SO's Question & Answer. So I went ahead and test it by writing a very small program to display a text label. But I found out it is "missing", or disappear when I use frame.pack_propagate(0)
import tkinter as tk
root = tk.Tk()
frame = tk.Frame(root, width=400, height=400)
# Does not work at the moment, textBox is missing
# frame.pack_propagate(0)
frame.pack()
textBox = tk.Label(frame, text="(x,y): ")
textBox.pack()
root.mainloop()
So my question is, can you explain why my textBox (Label) is not appearing when I use the frame.pack_propagate(0) instead of frame.pack() method? And secondly, is there a way to set the window size without using a canvas? I want to know because I am writing a series of small programs to teach my friend about tkinter, before introducing canvas to him. It would be nice if the window size are all the same across my tkinter samples. And I am just wondering as well (curious). Thank you very much.
I am using python 3.2.2 on MAC OS 10.5.8.
pack_propagate only sets a flag, it doesn't cause the frame to be placed in the widget. It is not a substitute for calling pack.
In other words you must do this:
# put the frame in its parent
frame.pack()
# tell frame not to let its children control its size
frame.pack_propagate(0)
# put the textbox in the frame
textBox.pack()
To answer your second question: Yeah, there is a way.
tkinters Tk do have the Tk.geometry function. When you just call it without arguments, you will get the current geometry in form of 'widthxheight+x+y', so for example (on Windows 10) '200x200+26+26' when you create your first Tk window. Using that format you can resize the Tk by, e.g., writing: root.geometry('400x500+60+60') to set the width to 400, the height to 500 and place it at the coordinates (60|60).
This works for Tk alswell as for Toplevel. But Toplevel also takes the arguments height and width when initialized or configured. If you want them to keep their size when packing something inside just use root.pack_propagate(False) on them.
By the way there is something similar for the grid manager: root.grid_propagate(False)

Randomly add buttons to Tkinter GUI?

How do I randomly add buttons to a Tkinter GUI? I need it to be able to create a button, then put it anywhere on the window, is this possible? I am using Python 2.6 on Windows.
If you want random button placement (or anything not aligned along a grid, etc.), you can use the place geometry manager. Depending on platform, overlapped buttons may not behave as you expect, though, so you may want to avoid them.
Here's a simple example:
from Tkinter import *
from random import random
root = Tk()
frame = Frame(root, height=200, width=200)
for i in range(10):
Button(frame, text=str(i)).place(x=random() * 150, y=random() * 180)
frame.pack()
root.mainloop()
There are several options to choose from. For example, you could design on a grid where you have six buttons per row. Then it's just a matter of starting at row 0, incrementing the column for each button. When you get to the last column, reset the column to 0 and increment the row by one.
Another option is to use a text widget as the container, and embed your buttons in the text widget with wrapping enabled. With this trick the buttons will fill a row automatically and wrap if the user grows or shrinks the main windows. It's a tiny bit more work, but it works well if that's the behavior you want.

How to pack a tkinter widget underneath an existing widget that has been packed to the left side?

I'm attempting to write a basic Tkinter GUI that has a Text widget at the top, then a Button widget left aligned under it, then another Text widget underneath the button. The problem I'm having is, after packing the Button widget to the left, when I then go to pack the second Text widget, it puts it next to the button on the right, rather than underneath the button. This happens regardless of what I set the side argument to for the second Text widget Here's a simple piece of code that demonstrates this behaviour:
from Tkinter import *
root = Tk()
w = Text(root)
w.pack()
x = Button(root, text="Hi there!")
x.pack(side=LEFT)
y = Text(root)
y.pack(side=BOTTOM)
root.mainloop()
So how would I go about setting up the second Text widget so that it appears below the button, rather than to the right of it?
There are generally two solutions to layout problems:
switch to using grid. It becomes real easy to do layouts like what you are trying to accomplish. Grid can solve probably 95% of all layout issues (it's amazing when you think about it -- Tk does with one manager what most toolkits need half a dozen to accomplish!)
use multiple frames. If some widgets need to be stacked top-to-bottom and some left-to-right you can't always get what you want packing everything in a single frame. Use one frame for the top-to-bottom parts of the layout and additional frames for the left-to-right content.
Also realize that widgets don't have to be children of the widget in which they are packed/gridded. You can use the "in" parameter to put widgets in some other container than their parent.
For example, in your specific example you can create three frames, top, middle, bottom. Pack these top-to-bottom in your toplevel window. Then you can pack the first text widget in the top, the button or buttons horizontally in the middle, and the other text widget in the bottom.
The advantage to such an approach is that it makes it much easier to change the layout in the future (which in my experience always happens at some point). You don't have to re-parent any of your widgets, just pack/place/grid them in some other container.
In your short example it doesn't make much difference, but for complex apps this strategy can be a life saver.
My best advice is this: layout isn't an afterthought. Do a little planning, maybe even spend five minutes drawing on some graph paper. First decide on the major regions of your app and use a frame or some other container for each (paned window, notebook, etc). Once you have those, do the same divide-and-conquer approach for each section. This lets you use different types of layout for different sections of your app. Toolbars get horizontal layout, forms might get vertical layout, etc.
I was initially misunderstanding how packing worked and didn't realise that the entire left side was being "claimed" when i did x.pack(side=LEFT). What I found after reading this and the answer by Alex here is that I was not really after having x packed to the left side at all, but rather having it anchored to the left, using anchor=W (W for West) instead of side=LEFT. My revised code snippet which does what I was after looks like this:
from tkinter import *
root = Tk()
w = Text(root)
w.pack()
x = Button(root, text="Hi there!")
x.pack(anchor=W)
y = Text(root)
y.pack(side=BOTTOM)
root.mainloop()
This way x is not "claiming" the left side anymore, it's just aligned to the left (or West) within its block of space.
Packing happens in the order the .pack methods are called, so once x has "claimed" the left side, that's it -- it will take up the left portion of its parent and everything else within its parent will be to its right. You need a Frame to "mediate", e.g....:
from Tkinter import *
root = Tk()
w = Button(root, text="Mysterious W")
w.pack()
f = Frame(root)
x = Button(f, text="Hi there!")
x.pack()
y = Button(f, text="I be Y")
y.pack(side=BOTTOM)
f.pack(side=LEFT)
root.mainloop()
(changed Texts to Buttons for more immediate visibility of layout only -- the Tkinter on this Mac doesn't show Texts clearly until they have focus, but Buttons are quite clear;-).
Do it the same way that WebView does using the Mosaic Canvas Widget Sets internals(which are very similar to Tk). The trick is that the second identical named Frame Object works as a Block Level Float(inline:block;) for everything placed after it and everything that calls "fr" already will automatically begin over inside of it.
You can have many doing this of TOP aligned widgets and simply add another identical named Frame where you want to break between side=LEFT's. Works after Bottom also.
fr=Frame(root)
fr.pack(fill=X, side=TOP)
block1=Label(fr)
block1.pack(side=LEFT)
block2=Label(fr)
block2.pack(side=LEFT)
block3=Button(fr)
block3.pack(side=LEFT)
# NAME IT THE SAME ID NAME AS THE FIRST MAIN FRAME...
fr=Frame(root)
fr.pack(fill=X, side=TOP)
# These NOW jump into the second Frame breaking the side=LEFT in new Frame
block4=Label(fr)
block4.pack(side=LEFT)
block5=Label(fr)
block5.pack(side=LEFT)
# AND THEY CONTINUE GOING side=LEFT AFTERWARDS.

Categories

Resources