Python - Tkinter fix object position - python

Im currently testing around with python GUI and have made a script that takes 2 entered numbers from 2 textfields and upon a button press generates a block of labels (e.g. i enter 4 and 5 so it generates a 4x5 field of labels)
but now i want to do this: when i generate objects, i want to prevent them to
- move
- overlap
my current objects (buttons, textfields).
i can kind-of figure something for the overlapping, but every time i generate new stuff, everything moves around. Can i set a specific field in the grid to be "reserved" so that new stuff never goes in there?
this is my current attempt - as you can see, its not overlapping anymore, but if the snowflakes are generated, the textboxes and buttons still "jump" apart for a small distance
EDIT: the "jumps" are due to the font size of the added snowflakes - that still leaves my question on how i prevent this, as i dont want to be limited to small font sizes
from tkinter import *
wide = 0
deep = 0
entrytext = "test"
window = Tk()
window.title("test")
window.geometry('1000x1000')
ent = Entry(window)
ent.grid(column=0, row=1)
def GetClicked():
global wide
wide = ent.get()
wide = int(wide)
btn2 = Button(window, text="Width", command=GetClicked)
btn2.grid(column=0, row=2)
ent2 = Entry(window)
ent2.grid(column=0, row=3)
def GetClicked2():
global deep
deep = ent2.get()
deep = int(deep)
btn = Button(window, text="Depth", command=GetClicked2)
btn.grid(column=0, row=4)
def WingBut(column,row):
lbl = Label(window, text="T", font=("Wingdings", 15))
lbl.grid(column=column, row=row)
def clicked(wide,deep):
h = 0
j = 0
while h in range (deep):
i = 0
h += 1
while i in range(wide):
if i > 2 or j > 5:
WingBut(i,j)
i += 1
if i == wide:
j += 1
btn = Button(window, text="Buttonspam",font=("Arial", 10),command=lambda: clicked(wide,deep))
btn.grid(column=0, row=0)
window.mainloop()

the textboxes and buttons still "jump" apart for a small distance
This is due to the resulting size of the dynamically added labels (those labelled "T") being taller than the current row height for each row. Because the row size must increase to accommodate the new label, the other widgets in the same row are also resized so that the overall height for the row is consistent. That resize is causing the jumping effect.
One way to correct it would be to reduce the font size of the "T" labels. Try setting it to 10 and the problem should go away.
Another way to solve it would be to set the minsize for each row to be the height of the tallest widget in the row, e.g. the "T" label widget height.
for row in range(5):
window.rowconfigure(row, minsize=36)
You can add the above code before you call window.mainloop().
I selected 36 because this makes the rows a minimum of 36 pixels high, and this is sufficient on my system to display the "T" without causing the row to resize.
If you don't want to hardcode the minsize you could calculate it dynamically.
dummy = Label(window, text="T", font=("Wingdings", 20))
dummy.grid(row=0, column=0)
dummy.update_idletasks() # seems to be required to get rendered size
height = dummy.winfo_height()
dummy.grid_forget() # we don't want users seeing this widget
for row in range(5):
window.rowconfigure(row, minsize=height)
That's one way to do it. Possibly there is a better, more direct, way using the font itself, but you can research that if you're interested.

Related

Tkinter application freezes with high number of entry/text fields

I'm trying to create a small program where I want to edit a big size table. See the example program below.
from tkinter import *
def table_generator(root):
size = 10
table_frame = Frame(root)
for i in range(size): # Rows
for j in range(size): # Columns
b = Text(table_frame, width=2, height=1)
b.grid(row=i, column=j)
table_frame.pack(side="left", anchor="nw")
if __name__ == '__main__':
application = Tk()
table_generator(application)
mainloop()
This works as I intended, but when I increase my size to 40, the program is slow to start and freezes if I try to move it. However, I want to increase this to around 200 at some point.
Does any of you have an alternative for this? I want to be able to enter a single character and change the background color in the end. Or some way of coding so that it does work with high numbers of Entry of Text fields?
Thanks

Tkinter board game

I am making a board game with Tkinter. I create a grid:
def create_grid(self):
self.grid_frame = Frame(window)
self.grid_frame.grid(row=1, column=0)
self.grid_picture = PhotoImage(file="grid.PNG")
self.grid_label = Label(self.grid_frame, image=self.grid_picture)
self.grid_label.grid(row=0, column=0, columnspan=100, rowspan=10)
Then the pawns are placed based on their distance from start:
def green_grid_translation(self, green_position):
if green_position < 10:
self.green_grid_row = 9
self.green_grid_column = green_position*10+2
elif green_position < 20:
self.green_grid_row = 8
self.green_grid_column = 92 - (green_position - 10)*10
The pawns are placed on the same frame as the grid, the frame is created again with every move:
def position_interface(self):
self.grid_frame = Frame(window)
self.grid_frame.grid(row=1, column=0)
self.grid_picture = PhotoImage(file="grid.PNG")
self.grid_label = Label(self.grid_frame, image=self.grid_picture)
self.grid_label.grid(row=0, column=0, columnspan=100, rowspan=10)
self.green_picture = PhotoImage(file="green.png")
self.green_symbol = Label(self.grid_frame, image=self.green_picture)
self.green_symbol.grid(row=self.green_grid_row, column=self.green_grid_column)
self.blue_picture = PhotoImage(file="blue.png")
self.blue_symbol = Label(self.grid_frame, image=self.blue_picture)
self.blue_symbol.grid(row=self.blue_grid_row, column=self.blue_grid_column)
The following loops are used to make them go step by step:
for x in reversed(range(green_change[0])):
run_grid.green_grid_translation(green_change[1] - x)
run_grid.blue_grid_translation(blue_change[1])
run_grid.position_interface()
window.update()
sleep(1)
for x in reversed(range(blue_change[0])):
run_grid.green_grid_translation(green_change[1])
run_grid.blue_grid_translation(blue_change[1] - x)
run_grid.position_interface()
window.update()
sleep(1)
green_change[0] is the number of steps the pawn is supposed to move,
green_change[1] is its position on the grid
It works fine with a single pawn, but when there are two, it's like the number
of rows and columns changes and the pawns sometimes land in wrong positions:
Is there a way to fix it or do I need to take a completely different approach?
Your approach is wrong. There is plenty of stuff to improve, e.g the use of sleep in a GUI application is an absolute no-no.
But for the problem at hand, you simply use the wrong abstraction. Grids are for creating widgets in regular spaced layouts. But not for stacking/rearranging them. It CAN be done, but I would advise against it.
Use instead a canvas. This allows you to simply overlay graphical elements, and even move them around (smoothly if you are so inclined!).

Fix Text widget size

I have made an application and part of it involves entering a question and answer. I have this code:
import tkinter as tk
root = tk.Tk()
root.geometry("500x250")
#Main question/answer frame
createFrm = tk.Frame(root)
createFrm.pack(expand = True) #To centre the contents in the window
#Create question entry area
cnqFrm = tk.Frame(createFrm)
cnqFrm.pack()
cnqFrm.pack_propagate(False)
#Question entry
cnqLabQ = tk.Label(cnqFrm, text = "Question")
cnqLabQ.grid(column = 0, row = 0)
#Frame for question Text
cnqTxtQFrm = tk.Frame(cnqFrm, height = 100, width = 100)
cnqTxtQFrm.grid(column = 0, row = 1)
cnqTxtQFrm.grid_propagate(False)
#Question Text
cnqTxtQ = tk.Text(cnqTxtQFrm)
cnqTxtQ.pack()
cnqTxtQ.pack_propagate(False)
#Answer entry
cnqLabA = tk.Label(cnqFrm, text = "Answer")
cnqLabA.grid(column = 1, row = 0)
#Frame for answer text
cnqTxtAFrm = tk.Frame(cnqFrm, height = 100, width = 100)
cnqTxtAFrm.grid(column = 1, row = 1)
cnqTxtAFrm.grid_propagate(False)
#Answer Text
cnqTxtA = tk.Text(cnqTxtAFrm)
cnqTxtA.pack()
cnqTxtA.pack_propagate(False)
Despite the fact the Text widget is in a Frame with grid_propagate(False) and a fixed height and width, and the Text widget itself has pack_propagate(False), it still expands to far larger than it should be. Why is this and how can I fix it?
You don't give the text widget an explicit size, so it defaults to 40x80 average-sized characters. The most common way to force it to a specific size that is determined by its parent is to give it a size that is smaller than the containing widget, and then let grid or pack expand it to fit the space given to it. So, start by giving the text widget a width and height of 1 (one).
Next, in this specific case you are calling grid_propagate(False) on the containing frame, but you are using pack to manage the window. You should call pack_propagate if you're using pack. You also need to tell pack to expand the text widget to fill its frame.
Finally, there's no point in calling cnqTxtQ.pack_propagate(False) since that only affects children of the text widget and you've given it no children.
All of that being said, I strongly encourage you to not use grid_propagate(False) and pack_propagate(False). Tkinter is really good at arranging widgets. Instead of trying to force the text widget to a specific pixel size, set the text widget to the desired size in lines and characters, and let tkinter intelligently arrange everything else to line up with them.

Programatically add and remove tkinter python labels causes IndexError: list index out of range

Sorry for the vague title but I didn't know how to explain myself better. Basically what I try to do in tkinter here is adding and removing labels. The label value gets updated so that I always have an increment of 1 even though I deleted a label in the beginning. If I generate labels and delete them from the bottom up I have no problems but it I delete one from the middle and then try to clean my list I get an error:
Exception in Tkinter callback
Traceback (most recent call last):
File "/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-tk/Tkinter.py", line 1536, in __call__
return self.func(*args)
File "/Users/XXXX/Helper/development/dynamicListLabels.py", line 21, in <lambda>
labelList[index].append(ttk.Button(root, text="Remove", command=lambda: removeLabel(labelList[index][0], index)))
IndexError: list index out of range
My python code looks like this:
#!/usr/bin/python
from Tkinter import *
import ttk
def removeLabel(labelToRemove, bla):
labelList[labelToRemove.get()][1].destroy()
labelList[labelToRemove.get()][2].destroy()
del labelList[labelToRemove.get()]
for label in labelList:
index = labelList.index(label)
label[0].set(index)
def addNewLabel():
labelList.append([IntVar()])
index = len(labelList) - 1
labelList[index][0].set(index)
labelList[index].append(ttk.Label(root, textvariable=labelList[index][0]))
labelList[index].append(ttk.Button(root, text="Remove", command=lambda: removeLabel(labelList[index][0], index)))
labelList[index][1].grid(column=0)
labelList[index][2].grid(column=1, row=labelList[index][1].grid_info()['row'])
root = Tk()
labelList = []
ttk.Button(root, text="add label", command=addNewLabel).grid(column=1, row=0)
root.mainloop()
And my GUI looks like this:
Thanks for your help!
Design
The main problem comes when dealing with different indexes. Trying to manipulate them carefully leads to complicated operations resulting in a long and inefficient code. To remedy to this problem, we simply get rid of them and take advantage of the label class variable Tkinter.IntVar() you already are using. This gives us full control of the labels and associated widgets.
An other efficient decision to take that prevents from getting lot of headache is to attach each (label, button) couple widgets to a unique Tkinter.Frame() instance. This offers the advantage of deleting the frame using destroy() method leading automatically to the destruction of the widgets it contains. In the same time, this keeps the look of your GUI and makes your it scalable as it offers you the possibility to add more widgets.
Designing addNewLabel()
There is nothing new here compared to your original code except, as I said in 2. each (label, button) couple will be drawn into a single and unique Tkinter.Frame() instance. Of course, the list frames must be declared global in this method.
Designing removeLabel()
From 1. the only argument we need to pass to removeLabel() is the Tkinter variable (var in the code below) inherent to the label we want to get rid of.
We need then to loop over list of frames (frames in the code below) using winfo_children() to seek for the label which has the text variable we are looking for.
Note that because I draw the label before the button inside individual frames, winfo_children() returns as first widget list element the label
winfo_children():
Returns a list containing the path names of all the children of window. Top-level windows are returned as children of their logical
parents. The list is in stacking order, with the lowest window first,
except for Top-level windows which are not returned in stacking order.
Use the wm stackorder command to query the stacking order of Top-level
windows.
This is why it is correct to write : if frame.winfo_children()[0].var == var and destroy the frame that contains the label which satisfies this condition.
Solution
Here is the program. I commented the lines which I think deserve to be commented:
'''
Created on Jun 25, 2016
#author: billal begueradj
'''
from Tkinter import *
import ttk
def removeLabel(var):
global frames
z = -1
# Loop over the list of rames
for frame in frames:
z = z + 1
# Check the text variable of the label of this frame
if frame.winfo_children()[0].var == var:
# Destroy the related frame
frame.destroy()
# Update the size of the list of frames
frames = frames[:z] + frames[z+1:]
# Do not forget to always rest this flag back to -1
z = -1
# Update the labels' numbers
r = 0
for frame in frames:
frame.winfo_children()[0].var.set(r)
r = r + 1
def addNewLabel():
global frames, i
var = IntVar()
frame = Frame(root)
i = i + 1
frame.grid(row=i, column=0)
var.set(len(frames))
l = ttk.Label(frame, textvariable=var)
l.grid(row=0, column=0)
l.var = var
b = ttk.Button(frame, text="Remove", command=lambda: removeLabel(var))
b.grid(row=0, column=1)
frames.append(frame)
if __name__ == '__main__':
root = Tk()
frames = []
i = 1
ttk.Button(root, text="add label", command=addNewLabel).grid(column=0, row=0)
root.mainloop()
Demo
Let us create 6 labels:
Now let us delete the label number 3. You can see that the numbering of the labels is automatically updated:
Now let us add a new label. You can see the newly added label has a number which is consecutive to the last existing label number in the list:
Note that the length of the list is updated all the time as you wanted.

Unexpected label width value

i am reading the width of a label at three different times and only one of them is producing the correct output.. code:
from tkinter import *
def getwidth(string):
print(string+str(lbl1.winfo_width()))
root = Tk()
lbl1 = Checkbutton(root, text="test text")
lbl1.grid(row=0,rowspan=2)
print("first "+str(lbl1.winfo_width()))
getwidth("second ")
btn = Button(root, text="GO", command=lambda x="third ": getwidth(x))
btn.grid(row=2)
root.mainloop()
How can i read the correct width (69) during the first two outputs without having to rely on the button command? Thanks
current outputs are:
first 1
second 1
third 69
Well, unfortunately, you can't. The first two times are done before the window is loaded (which causes it to return the default value of 1 since the label isn't drawn yet). The third time is done after the window is loaded (the label is drawn), so it returns the correct number.
You have to remember that, until you call root.mainloop and load the window, the widgets are not placed on the screen. Sure, they exist behind the scenes (otherwise a NameError would be thrown), but they are not on the screen and taking up space yet. Thus, when you try to see how much space they are taking up, you get the default number of 1.

Categories