0

I'm trying to use the tksleep way of doing time.sleep in my python script, but can't figure out how to use it outside of my gui.py file without having to add root = tk.Tk() and root.tksleep(2) to each one of my scripts, which will create a new tkinter window for every time I run or add root.tksleep(2) .

I know .after is the normal way to delay events, but my scripts are in an order that they can take different amounts of time depending on what happens on the screen, which is why I can't predict how long the delay will be.

For example, if I run the script below called test1.py, it will work but will create an extra (custom)tkinter window, outside of the Toplevel window I already had from my gui.py file. I've thought of adding all the code together of my scripts into the gui.py file but that is not really the way I want to have my project organized.

test1.py:

import tkinter as tk

root = tk.Tk()

def tksleep(self, time:float) -> None:
    self.after(int(time*1000), self.quit)
    self.mainloop()

print("before waiting")
root.tksleep(1)
print("after waiting")

gui.py:

import customtkinter as ctk
import sys
import main

root = ctk.CTkToplevel()    

# --- main ---    
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("dark-blue")

root.geometry("500x350")
root.iconbitmap(r"app.ico")
root.title('dux<3#0767')

frame = ctk.CTkFrame(master=root)
frame.pack(pady=20, padx=60, fill="both", expand=True)

button = ctk.CTkButton(master=frame, text='Run', command=main.start)
button.pack(pady=12, padx=10)

root.mainloop()

1. I then tried to put the tksleep function into my gui.py file which, simplified, looked like this:

import tkinter as tk
import customtkinter as ctk
import sys
import main
root = ctk.CTkToplevel()
# --- functions ---
def tksleep(self, time:float) -> None:
    self.after(int(time*1000), self.quit)
    self.mainloop()
    
# --- main ---    
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("dark-blue")

root.geometry("500x350")
root.iconbitmap(r"app.ico")
root.title('dux<3#0767')

frame = ctk.CTkFrame(master=root)
frame.pack(pady=20, padx=60, fill="both", expand=True)

button = ctk.CTkButton(master=frame, text='Run', command=main.start)
button.pack(pady=12, padx=10)

root.mainloop()

But upon importing gui.py into main.py, it was a circular import, because I imported main.py into gui.py too, for my button to start the program. (Gave me this error: AttributeError: partially initialized module 'main' has no attribute 'start' (most likely due to a circular import))

2. I've tried to make a class called sleep with a method called tksleep in my main.py file like this (which already creates another unnecessary extra tkinter window):

import tkinter as tk

class sleep():
    root = tk.Tk()
    def tksleep(self, time:float) -> None:
        self.after(int(time*1000), self.quit)
        self.mainloop()

and made my test.py file like this:

import main

print("before waiting")
main.sleep.tksleep(1)
print("after waiting")

which gave me the error: TypeError: tksleep() missing 1 required positional argument: 'time'

I'm quite new to this and may have forgotten to add any important information, in that case please notify me!Thank you a lot.

Duxicity
  • 1
  • 1
  • Why does `gui.py` import `main.py`? It looks like bad design. From the names, I am guessing that `main.py` is the main script that needs to import `gui.py`, but it doesn't look like you need to import `main.py` from `gui.py`. Your code just has a redundant `import main`. Btw I updated the [`tksleep` code](https://stackoverflow.com/a/74162322/11106801) with an example. – TheLizzard Jan 06 '23 at 16:38
  • I don't see how `test1.py` can run, since it uses `self` but doesn't use classes. Since it doesn't use classes, you must pass two arguments to it: `self` and `time`. – Bryan Oakley Jan 06 '23 at 18:13

1 Answers1

0

First I would suggest to change the name of the first argument from self to root to avoid confusion which seems to happen here. The argument self generally refer to the instance of the class. So the suggestion is to change the function to def tksleep(root, time:float) -> None:.

The reason for the error in tksleep in the second implementation is that you call the static function, you do not call the function from an instance, which means that the first argument do not get a value (all arguments are shifted left one time).

I am not exactly sure what you are trying to accomplish here but I assume you want to continue with some scripting after the mainloop is called and you want the timing to be order dependent. One way to do this is to simply use after and keep track of which script need to run every time. Another way is to use asynchronous mainloop, see https://github.com/insolor/async-tkinter-loop for more information.

If you want to code similar to using sleep but synchonous using tkinters after, then one approach is below. The example became quite long but shows a job queue system with yield ms as sleep via the after function. In def work(): function you can see how the yield as sleep works.

import tkinter as tk

iterable_queue = []
def run_yield_sleep(root, func=None, enqueue=False):
    if func != None:
        loop_active = len(iterable_queue) > 0
        if enqueue or not loop_active:
            iterable_queue.append(iter(make_iter(func)))
        if loop_active:
            update_queue_text()
            return
    
    sleep_time = next(iterable_queue[0], None)
    while sleep_time == None or sleep_time == 0:
        if sleep_time == None:
            iterable_queue.pop(0)

        if len(iterable_queue) > 0:
            sleep_time = next(iterable_queue[0], None)
        else:
            update_queue_text()
            return
    
    if len(iterable_queue) > 0:
        root.after(sleep_time, run_yield_sleep, root)
    
    update_queue_text()

def make_iter(func):
    yield 0 # This is just a dummy yield to ensure correct order of events
    try:
        yield from func()
    except TypeError:
        yield None # This happens when a function doesn't have yields

def update_queue_text():
    queue_label.config(text=f"Current jobs {len(iterable_queue)}")

def work():
    status_label.config(text="Some initial text")
    yield 1500 # Similar to sleep for 1500 ms but with the use of after
    status_label.config(text="Waited 1500 ms")
    yield 2000
    status_label.config(text="Work work ...")
    yield 1500
    status_label.config(text="Done")

app = tk.Tk()
queue_label = tk.Label(app)
queue_label.pack(side="top")

status_label = tk.Label(app, text="Click to start")
status_label.pack(side="top")

button_frm = tk.Frame(app)
button_frm.pack(side="top")

tk.Button(master=button_frm, text='Enqueue',
    command=lambda: run_yield_sleep(app, work, True)).grid(row=0, column=0)
    
tk.Button(master=button_frm, text='Run once',
    command=lambda: run_yield_sleep(app, work, False)).grid(row=0, column=1)

update_queue_text()
app.mainloop()

Edit:

I am working on a project with similar criteria and I found that there is an easier approach to the code I posted above. I looked at how Turtle manages to draw without locking the ui, and using update() and after() then you can get a sleep behavior, after(1) is used to not overload cpu and _get_default_root() is used so the function can be called from anywhere.

import tkinter as tk
from tkinter import _get_default_root
import time

def tksleep(secs: float):
    start_time = time.time()
    root = _get_default_root()
    while time.time() - start_time < secs:
        root.update()
        root.after(1)

def work():
    status_label.config(text="Some initial text")
    tksleep(1)
    status_label.config(text="Waited 1 second")
    tksleep(3)
    status_label.config(text="Work work ...")
    tksleep(2)
    status_label.config(text="Done")

app = tk.Tk()

status_label = tk.Label(app, text="Click to start")
status_label.pack(side="top")

tk.Button(master=app, text='Run', command=work).pack(side="top")

app.mainloop()