Why is my tkinter layout changing? - python

I made this tkinter application, and I have a bug where the layout changes and the button at the bottom disappears.
I am unable to reproduce the bug 100%. It happens at random times.
For this reason, this SSCCE may contain more than it needs. It's still a lot less than my original app.
from random import random
from threading import Thread, Lock
from time import sleep
from tkinter import Tk, LEFT, RIGHT, BOTH, X, Y, Listbox, Scrollbar, VERTICAL, END
from tkinter.ttk import Frame, Button, Style, Entry
class ListManager:
def __init__(self, ui):
self.entry_list = []
self.ui = ui # to notify of list update
self.timer = ListManager.interval + 1
self.stop = False
interval = 1 # between updates
#staticmethod
def api_request() -> dict:
new_list = ["line"]
while random() > .4:
new_list.append("line")
return {"data": new_list}
def get_entries(self):
r = self.api_request()
self.entry_list = []
for line in r["data"]:
self.entry_list.append(line)
def run(self):
self.timer = ListManager.interval + 1
while not self.stop:
if self.timer > ListManager.interval:
self.get_entries()
if self.ui is None:
print("entries:", len(self.entry_list))
for entry in self.entry_list:
print(entry)
else:
self.ui.receive(self.entry_list)
self.timer = 0
else:
self.timer += 1
sleep(1)
class UI(Frame):
def __init__(self):
super().__init__()
self.style = Style()
self.listbox = None
self.name_entry = None
self.entries = []
self.mutex = Lock()
self.init_pack()
def init_pack(self):
# TODO: investigate open button disappearing (probably need to repack in receive function)
self.master.title("list")
self.style.theme_use("default")
add_streamer_frame = Frame(self)
add_button = Button(add_streamer_frame, text="Add Line")
add_button.pack(side=RIGHT, padx=5, pady=5)
self.name_entry = Entry(add_streamer_frame)
self.name_entry.pack(fill=X, padx=5, expand=True)
add_streamer_frame.pack(fill=X)
list_frame = Frame(self)
self.listbox = Listbox(list_frame, height=20)
self.listbox.configure(font=("Courier New", 12, "bold"))
scrollbar = Scrollbar(self.listbox, orient=VERTICAL)
self.listbox.config(yscrollcommand=scrollbar.set)
scrollbar.config(command=self.listbox.yview)
scrollbar.pack(side=RIGHT, fill=Y)
self.listbox.pack(fill=BOTH, padx=5, expand=True)
list_frame.pack(fill=BOTH, expand=True)
self.pack(fill=BOTH, expand=True)
open_button = Button(self, text="Open")
open_button.pack(side=LEFT, padx=5, pady=5)
def receive(self, entries):
self.mutex.acquire()
self.listbox.delete(0, END)
self.entries = []
for entry in entries:
self.listbox.insert(END, entry)
self.entries.append(entry.split(" ")[0])
self.mutex.release()
def main():
root = Tk()
root.geometry("800x400+300+300")
app = UI()
tl = ListManager(app)
thread = Thread(target=tl.run)
thread.start()
root.mainloop()
tl.stop = True
thread.join()
if __name__ == "__main__":
main()
Here's what it normally looks like most of the time, and should look like:
Here's what it looks like after the bug:

Add:
self.ui.update_idletasks()
after:
sleep(1)
or:
Replace:
self.listbox = Listbox(list_frame, height=20)
with:
self.listbox = Listbox(list_frame)
# or self.listbox = Listbox(list_frame, height=17)
It must be that when too much is going on there's no time spent on updating the GUI. I couldn't reproduce the 2nd screenshot directly without lowering sleep argument as low as 0.001.
The root of your problem seems to be that each time something's added into the listbox its height(winfo_height which is in pixels) is recalculated and resized down from 20 character height, to 17, for which there isn't always time for before a new insertion.
Basically, your widget does not necessarily has a fixed 20 character height at all times, it resizes based on its content and other widgets, unless propagation is disabled by calling self.listbox.pack_propagate(False).
Add:
print(self.ui.listbox.winfo_height())
after:
sleep(1)
to see its height changing for yourself.

Related

Program for speed reading. Is it possible to highlight a word in the text that is displayed in the window?

Is it possible to somehow highlight a word in my program (one and the other with the same) in a different color. I thought about making a canvas in a canvas, but it didn’t work out.
class Application(object):
def __init__(self):
root = tk.Tk()
self.canvas = tk.Canvas(root,bg='black')
self.canvas.pack()
self.canvas_text = self.canvas.create_text(180, 130, text='',anchor=tk.NW,fill="white",font=('Arial',18))
self.entry = tk.Entry(root)
self.entry.bind("<Return>", self.entry_cb)
self.entry.pack()
self.entry.focus()
root.mainloop()
def animate_text(self, text, delta):
delay = 0
for i in range(len(text)+1):
update_text = lambda s=text[i - 1:-1]: self.canvas.itemconfigure(self.canvas_text, text=s)
self.canvas.after(delay, update_text)
delay += delta
def entry_cb(self, event):
# s = self.entry.get().split()
s = 'London is the capital of Great Britain'.split()
s += [s[-1]]
self.animate_text(s, skorost)
skorost = 400
app = Application()

solution communication between two windows

I want to have two tkinter windows. A button should be in the first window, and a reaction text should be in the second window.
My questions:
Must the second window have no modal?
How do I make the second window movable?
How can I give information to second window via callback function?
Thanks in advance for answers and advice!
Here is some code that may help you:
from tkinter import *
class App:
def __init__(self):
self.window1 = Tk()
self.window2 = Toplevel()
self.button = Button(self.window1, bd = 5, text = "Click Me!", command = self.update)
self.button.pack()
self.label = Label(self.window2, bd = 5, text = "Button has not been clicked.")
self.label.pack()
def update(self):
self.label.config(text = "Button has been clicked!")
self.window2.update()
app = App()
Explanation:
The first line imports tkinter
In the next line, we create a class. At the bottom of the code, we create an object using that class. This is useful because when the object is created, the functions in the class are already defined, so the function definition can be after when it is called.
After we declare our class, in __init__, we write code that will run when an object is created from that class. The code creates two windows. One contains a button, and the other one contains a label. The button has a command parameter to run the class function, update.
In update, we change the label text and update the window.
I have not next questions. My problems solution is here:
import tkinter as tk
class ViewOnMoon(tk.Toplevel):
def __init__(self, parent = None, draw = None):
tk.Toplevel.__init__(self, parent)
self.transient(parent)
self.title('View')
self.minsize(height = 300, width = 300)
fr_canvas = tk.Frame(self)
fr_canvas.place(relx=0.23, rely=0.01, anchor="nw")
self.canv_w = 200
self.canv_h = 200
self.canvas = tk.Canvas(fr_canvas, bg='white', width = self.canv_w, height=self.canv_h)
self.canvas.grid(column = 0, row = 0)
return
class GuiMoonMove(tk.Frame):
def __init__(self, master):
mon_h = 600
mon_w = 1250
tk.Frame.__init__(self, master)
self.frame = tk.Frame(master, width=1000, height=200, bd=2)
self.master.title('Move')
self.master.minsize(height = mon_h, width = mon_w)
fr_canvas = tk.Frame(self.master)
fr_canvas.place(relx=0.23, rely=0.01, anchor="nw")
fr_button = tk.Frame(self.master)
fr_button.place(relx=0.02, rely=0.06, anchor="nw")
self.canv_h = 600
self.canv_w = 950
self.lbl_view = tk.BooleanVar()
chb_view_on_moon = tk.Checkbutton(fr_button, text="Pohled na Měsíc", variable = self.lbl_view, \
onvalue=True, offvalue=False,command = self.callback)
chb_view_on_moon.grid(column= 0, row= 4,pady = 10)
self.canvas = tk.Canvas(fr_canvas, bg='white', width = self.canv_w, height=self.canv_h)
self.canvas.grid(column = 0, row = 0)
def callback(self,*args):
if self.lbl_view.get()==True:
self.view_on_moon = ViewOnMoon(parent = self.master)
else:
self.vom.destroy()
if __name__=="__main__":
root = tk.Tk()
app = GuiMoonMove(master = root)
app.mainloop()

tkinter cursor not changing until after action despite update_idletasks()

I am trying to change the cursor in my tkinter program to show the program is working but the cursor only changes to the working cursor until after the work is done, this is as compressed as I can make the code
warning: to demonstrate working it will count to 99,999,999 when you press go to page one
import tkinter as tk # python 3
from tkinter import font as tkfont # python 3
#import Tkinter as tk # python 2
#import tkFont as tkfont # python 2
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.title_font = tkfont.Font(family='Helvetica', size=18, weight="bold", slant="italic")
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
for F in (StartPage, PageOne):
page_name = F.__name__
frame = F(parent=container, controller=self)
self.frames[page_name] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame("StartPage")
def show_frame(self, page_name):
'''Show a frame for the given page name'''
frame = self.frames[page_name]
frame.tkraise()
class StartPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="This is the start page", font=controller.title_font)
label.pack(side="top", fill="x", pady=10)
button1 = tk.Button(self, text="Go to Page One",
command=self.go)
button1.pack()
def go(self):
# do something for like 5 seconds to demonstrate working
working(True)
l = [x for x in range(99999999)]
self.controller.show_frame('PageOne')
class PageOne(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="This is page 1", font=controller.title_font)
label.pack(side="top", fill="x", pady=10)
button = tk.Button(self, text="Go to the start page",
command=self.back)
button.pack()
def back(self):
working(False)
self.controller.show_frame('StartPage')
def working(yesorno):
if yesorno==True:
app.config(cursor='wait')
else:
app.config(cursor='')
app.update_idletasks()
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
Edit: I would like to thank Switch between two frames in tkinter for this app layout example
This code was tested in windows 10 and Python 3. I found the cursor would not change until control was returned to mainloop. The code here outlines how to consistently display the busy cursor during a long running task. Further, this code demonstrates how to retrieve the data from the long running task (like results from a database query).
#! python3
'''
Everything you need to run I/O in a separate thread and make the cursor show busy
Summary:
1. Set up to call the long running task, get data from windows etc.
1a. Issue a callback to the routine that will process the data
2. Do the long running task - absolutely no tkinter access, return the data
3. Get the data from the queue and process away. tkinter as you will
'''
import tkinter as tk
import tkinter.ttk as ttk
from threading import Thread
from threading import Event
import queue
class SimpleWindow(object):
def __init__(self):
self._build_widgets()
def _build_widgets(self):
# *************************************************************************************************
# * Build buttons and some entry boxes
# *************************************************************************************************
g_col = 0
g_row = 0
WaiterFrame = ttk.Frame()
WaiterFrame.pack( padx=50)
i = 0
g_row += 1
longWaitButton = ttk.Button(WaiterFrame, text='Long Wait',command=self.setup_for_long_running_task)
longWaitButton.grid(row = g_row, column = i, pady=4, padx=25)
i += 1
QuitButton = ttk.Button(WaiterFrame, text='Quit', command=self.quit)
QuitButton.grid(row = g_row, column = i,pady=4, padx=25)
i += 1
self.Parm1Label = ttk.Label(WaiterFrame, text="Parm 1 Data")
self.Parm1Label.grid(row = g_row-1, column = i, pady=4, padx=2)
self.Parm1 = ttk.Entry(WaiterFrame)
self.Parm1.grid(row = g_row, column = i, pady=4, padx=2)
i += 1
self.Parm2Label = ttk.Label(WaiterFrame, text="Parm 2 Data")
self.Parm2Label.grid(row = g_row-1, column = i, pady=4, padx=2)
self.Parm2 = ttk.Entry(WaiterFrame)
self.Parm2.grid(row = g_row, column = i, pady=4, padx=2)
i += 1
self.Parm3Label = ttk.Label(WaiterFrame, text="Parm 3 Data")
self.Parm3Label.grid(row = g_row-1, column = i, pady=4, padx=2)
self.Parm3 = ttk.Entry(WaiterFrame)
self.Parm3.grid(row = g_row, column = i, pady=4, padx=2)
i += 1
self.Parm4Label = ttk.Label(WaiterFrame, text="Parm 4 Data")
self.Parm4Label.grid(row = g_row-1, column = i, pady=4, padx=2)
self.Parm4 = ttk.Entry(WaiterFrame)
self.Parm4.grid(row = g_row, column = i, pady=4, padx=2)
def quit(self):
root.destroy()
root.quit()
def setup_for_long_running_task(self):
# ********************************************************************************************************
# * Do what needs to be done before starting the long running task in a thread
# ********************************************************************************************************
Parm1, Parm2, Parm3, Parm4 = self.Get_Parms()
root.config(cursor="wait") # Set the cursor to busy
# ********************************************************************************************************
# * Set up a queue for thread communication
# * Invoke the long running task (ie. database calls, etc.) in a separate thread
# ********************************************************************************************************
return_que = queue.Queue(1)
workThread = Thread(target=lambda q, w_self, p_1, p_2, p_3, p_4: \
q.put(self.long_running_task(Parm1, Parm2, Parm3, Parm4)),
args=(return_que, self, Parm1, Parm2, Parm3, Parm4))
workThread.start()
# ********************************************************************************************************
# * Busy cursor won't appear until this function returns, so schedule a callback to accept the data
# * from the long running task. Adjust the wait time according to your situation
# ********************************************************************************************************
root.after(500,self.use_results_of_long_running_task,workThread,return_que) # 500ms is half a second
# ********************************************************************************************************
# * This is run in a thread so the cursor can be changed to busy. NO tkinter ALLOWED IN THIS FUNCTION
# ********************************************************************************************************
def long_running_task(self, p1,p2,p3,p4):
Event().wait(3.0) # Simulate long running task
p1_out = f'New {p1}'
p2_out = f'New {p2}'
p3_out = f'New {p3}'
p4_out = f'New {p4}'
return [p1_out, p2_out, p3_out, p4_out]
# ********************************************************************************************************
# * Waits for the thread to complete, then gets the data out of the queue for the listbox
# ********************************************************************************************************
def use_results_of_long_running_task(self, workThread,return_que):
ThreadRunning = 1
while ThreadRunning:
Event().wait(0.1) # this is set to .1 seconds. Adjust for your process
ThreadRunning = workThread.is_alive()
while not return_que.empty():
return_list = return_que.get()
self.LoadWindow(return_list)
root.config(cursor="") # reset the cursor to normal
def LoadWindow(self, data_list):
self.Parm1.delete(0, tk.END)
self.Parm2.delete(0, tk.END)
self.Parm3.delete(0, tk.END)
self.Parm4.delete(0, tk.END)
i=0; self.Parm1.insert(0,data_list[i])
i+=1; self.Parm2.insert(0,data_list[i])
i+=1; self.Parm3.insert(0,data_list[i])
i+=1; self.Parm4.insert(0,data_list[i])
# ********************************************************************************************************
# * The long running task thread can't get to the tkinter self object, so pull these parms
# * out of the window and into variables in the main process
# ********************************************************************************************************
def Get_Parms(self):
p1 = self.Parm1Label.cget("text")
p2 = self.Parm2Label.cget("text")
p3 = self.Parm3Label.cget("text")
p4 = self.Parm4Label.cget("text")
return p1,p2,p3,p4
def WaitForBigData():
global root
root = tk.Tk()
root.title("Wait with busy cursor")
waitWindow = SimpleWindow()
root.mainloop()
if __name__ == '__main__':
WaitForBigData()
I suspect that all events need to be proceeded to change a cursor's look, because cursor depends on operating system and there're some events to handle (I assume that), since update_idletask has no effect - your cursor really change look only when code flow reaches a mainloop. Since you can treat an update as mainloop(1) (very crude comparison) - it's a good option if you know what you doing, because noone wants an endless loop in code.
Little snippet to represent idea:
try:
import tkinter as tk
except ImportError:
import Tkinter as tk
import time
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.button = tk.Button(self, text='Toggle cursor', command=self.toggle_business)
self.button.pack()
def toggle_business(self):
if self['cursor']:
self.config(cursor='')
else:
self.config(cursor='wait')
# self.update_idletasks() # have no effect at all
# self.update() # "local" mainloop(1)
# simulate work with time.sleep
# time.sleep(3)
# also your work can be scheduled so code flow can reach a mainloop
# self.after(500, lambda: time.sleep(3))
app = App()
app.mainloop()
To overcome this problem you can use:
update method (note warnings)
after method for scheduled work (opportunity to reach a mainloop for a code flow)
threading for "threaded" work (another opportunity, but GUI is responsive, you can handle other events and even simulate unresponsiveness, in other hand threading adds complexity, so use it if you really need it).
Note: There's no difference in behaviour between universal and native cursors on Windows platform.

How to make Tkinter button to be placed in particular position?

I am new to python so I was trying to make a GUI, in that I have to place a button in a particular position.
I tried using self.nxt_form.place(x=200,y=100) instead of self.nxt_form.pack().
But the button disappeared and only the frame appeared when it ran. Can you tell me how to place the button in a particular position?
Here is the code:
import tkinter as tk
class Main_form:
def __init__(self, root,title="Simulated MTBF"):
self.root = root
self.frame = tk.Frame(self.root)
"""Button nxt_form which moves to next form"""
self.nxt_form = tk.Button(self.frame, text = 'Next Form', width = 25,command = self.new_window)
self.nxt_form.pack()
self.frame.pack()
"""command to open new window by clicking Button """
def new_window(self):
self.newWindow = tk.Toplevel(self.root)
self.app = Demo2(self.newWindow)
class Demo2:
def __init__(self, root):
self.root = root
self.frame = tk.Frame(self.root)
self.quitButton = tk.Button(self.frame, text = 'Quit', width = 25, command = self.close_windows)
self.quitButton.pack()
self.frame.pack()
def close_windows(self):
self.root.destroy()
def main():
root = tk.Tk()
app = Main_form(root)
root.mainloop()
if __name__ == '__main__':
main()
when i am using tkinter i used column and row to position objects
self.btn = tk.Button(self, text = "button")
self.btn.grid(row = 1, column = 1)
EDIT - expanded on information in response to comment (below)
I would make an label and change its width and height to make the spacing you need (note im a beginer at python as well so this is probly a bad way but it works)
from tkinter import *
import tkinter as tk
from tkinter.ttk import Combobox,Treeview,Scrollbar
class MainMenu(Frame):
def __init__(self, master):
""" Initialize the frame. """
super(MainMenu, self).__init__(master)
self.grid()
self.create_GUI()
def create_GUI(self):
frame1 = tk.LabelFrame(self, text="frame1", width=300, height=130, bd=5)
frame1.grid(row=0, column=0, columnspan=3, padx=8)
#the frame is not needed but it is a good thing to use as can group
#parts of your interface together
self.text1 = Entry(frame1)
#note if you were not using frames would just put self here
self.text1.grid(row = 1, column = 0)
self.text2 = Label(frame1, text = "",height = 10)
self.text2.grid(row = 2 , column = 0)
self.text3 = Entry(frame1)
self.text3.grid(row = 3, column = 0)
root = Tk()
root.title("hi")
root.geometry("500x500")
root.configure(bg="white")
app = MainMenu(root)
root.mainloop()
Also note that you can not use pack and grid together what you could do is group your objects in different frames then use grid in one frame and pack in a different frame. I personally prefer to use grid to pack as it gives you more control over your object then pack does

Need help placing buttons on a PhotoImage .gif

Here is my program as of yet:
from tkinter import *
from collections import deque
class App():
def __init__(self, *images):
self.root = Tk()
self.root.title("Skin")
self.image_dict = {image: PhotoImage(file=image) for image in images}
self.image_queue = deque(images)
b = Button(self.root, text="Click here to see the diagram!", command=self.change_image)
b.pack(fill=X)
self.label = Label(self.root, image=self.image_dict["1.gif"])
self.label.image = self.image_dict["1.gif"]
self.label.pack()
def change_image(self):
self.image_queue.rotate(-1)
next_image = self.image_queue[0]
self.label.configure(image=self.image_dict[next_image])
self.label.image = self.image_dict[next_image]
if __name__ == "__main__":
app = App('1.gif', '2.gif')
app.root.mainloop()
What this does is when you run the scipt, a window comes up diplaying "1.gif", and a button. When you click the button, "1.gif" changes to "2.gif". "1.gif" is a blank diagram, "2.gif" is a diagram with labels showing what each part of the diagram is.
Now for the next stage of my program, I need some way to add multiple invisible buttons, or something like it, over each word on the diagram on "2.gif", and when you click on it, I need a seperate window to come up with text on it. Is there any way to implement that into my current program? I have no idea where to start. Thank you!
I think you will be better off using a Canvas to hold your image(s) rather
than a Label. You can then place 'hotspots' over the diagram and bind
events to them. eg. something like:
from tkinter import *
class App():
def __init__(self):
self.root = Tk()
self.messages = {}
self.canvas = Canvas(self.root, bd=0, highlightthickness=0)
self.image = PhotoImage(file='2.gif')
self.canvas.create_image(0,0, image=self.image, anchor='nw')
w,h = self.image.width(), self.image.height()
self.canvas.config(width=w, height=h)
self.canvas.pack()
self.balloon = b = Toplevel(self.root)
b.withdraw()
b.wm_overrideredirect(1)
self.msg = Label(b, bg='black', fg='yellow', bd=2, text='')
self.msg.pack()
self.set_up_hotspots()
def set_up_hotspots(self):
box1 = self.canvas.create_polygon(50,100,100,100,100,150,50,150,
outline='blue', fill='')
#note: outline can be '' also ;)
self.messages[box1] = "The machine head consists of a "\
"Flynn mechanism,\na Demmel crank, "\
"and a heavy-duty frobulator. "
box2 = self.canvas.create_polygon(150,100,200,100,200,150,150,150,
outline='red', fill='')
self.messages[box2] = "And now for something completely different"
for item in [box1, box2]:
for event, handler in [('<Enter>', self.on_hotspot_enter),
('<Leave>', self.on_hotspot_leave)]:
self.canvas.tag_bind(item, event, handler)
def on_hotspot_enter(self, event):
if not self.balloon.winfo_ismapped():
txt = self.messages[event.widget.find_withtag('current')[0]]
self.msg.config(text=txt)
x,y = event.x_root, event.y_root
self.balloon.geometry('+%d+%d' %(x+16,y))
self.balloon.deiconify()
def on_hotspot_leave(self, event):
if self.balloon.winfo_ismapped():
self.balloon.withdraw()
if __name__ == "__main__":
app = App()
app.root.mainloop()

Categories