3

I want to get a loading dialog box while I call a API in my tkinter application I have implement a custom dialog box in it but the dialog box is not coming basically the whole application freezes and loading dialog box also does not come.

This is a sample function which run when I click on submit button and then this function runs the api call and and open the dialog box while api is making call and also changes frame/navigate to other frame.

def login_button_fuction():
    top = tk.Toplevel(self)
    top.title("loading")
    top.geometry("200x100")
    top.resizable(0, 0)
    bar = ttk.Progressbar(top, orient="horizontal",
                          length=100, mode="indeterminate")
    bar.pack(pady=25)
    bar.start()
    url = "XXXXXXXXXXXXXXXXXXX"
    payload = json.dumps(
        {"username": username_entry.get(), "password": password_entry.get()})
    headers = {'Content-Type': 'application/json'}
    response = requests.post(url+"/users/getuser", data=payload, headers=headers)

    if response.json()["resultCode"] == 100:
        print("success")
        data = {
            "realname": response.json()["data"]["realname"],
            "phone": response.json()["data"]["phone"]}
        with open("user_data.dat", "wb") as f:
            pickle.dump(data, f)
        controller.show_frame(dashboardPage.dashboard_page)

    else:
        print("error")
    bar.grid_forget()
    top.destroy()

Also if any one can tell me how can I remove the minimize and close buttons from tkinter Dialog Box it will be very helpful. The loading Dialog Box Should come while making a API call and should automatically close when the Api call has been made.

I tried this but it hangs whole application and crashes it.


class login_page(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        #functions for buttons

        def try_login():
            url = "XXXXXXXXXXXXXXX"

            payload = json.dumps(
                {"username": username_entry.get(), "password": password_entry.get()})
            headers = {'Content-Type': 'application/json'}
            response = requests.post(
                url+"/users/getuser", data=payload, headers=headers)
            return response

        def login_button_fuction():
            top = tk.Toplevel(self)
            top.title("loading")
            top.geometry("200x100")
            top.resizable(0, 0)
            bar = ttk.Progressbar(top, orient="horizontal",
                                  length=100, mode="indeterminate")
            bar.pack(pady=25)
            bar.start()

            response = threading.Thread(target=try_login)
            response.start()
            response.join()

            print(response)
            if response.json()["resultCode"] == 100:
                print("success")
                data = {
                    "realname": response.json()["data"]["realname"],
                    "phone": response.json()["data"]["phone"],
                    "email": response.json()["data"]["email"]}

                with open("user_data.dat", "wb") as f:
                    pickle.dump(data, f)
                controller.show_frame(dashboardPage.dashboard_page)
            else:
                print("error")

            bar.grid_forget()
            top.destroy()
lexi
  • 57
  • 5
  • you need to do the work in a seperate thread or process and use root.callAfter to callback when that thread is done – Joran Beasley Nov 10 '21 at 02:40
  • @JoranBeasley can explain you a bit or tell me or point out the topics i need to search – lexi Nov 10 '21 at 02:42
  • See [Freezing/Hanging tkinter GUI in waiting for the thread to complete](https://stackoverflow.com/questions/53696888/freezing-hanging-tkinter-gui-in-waiting-for-the-thread-to-complete). – martineau Nov 10 '21 at 02:48
  • In your second code sample, `response` is the result of `threading.Thread(...)` which is the thread task reference, not the *response* of a *request*. Also calling `.join()` will block the application until the thread task completes. – acw1668 Nov 10 '21 at 10:20

1 Answers1

0

you need to do any work in a different thread, however all gui updates should happen in the main thread or things can get a bit wonky

something like the following untested code should work (maybe with minor alterations)

def do_login(username,password,callback):
    # this should run in a thread so it does not block the gui
    # since it is run in a thread it SHOULD NOT update the gui
    payload = json.dumps({"username": username, "password": password})
    headers = {'Content-Type': 'application/json'}
    callback(requests.post(url+"/users/getuser", data=payload, headers=headers))

import threading
def login_button_fuction():
    # this is called by the gui when the button is clicked
    top = tk.Toplevel(self)
    top.title("loading")
    top.geometry("200x100")
    top.resizable(0, 0)
    bar = ttk.Progressbar(top, orient="horizontal",
                                  length=100, mode="indeterminate")
    bar.pack(pady=25)
    def on_response(response):
        # after the call completes it will call back
        # to this function, once again in the main thread
        # since this is run in the main thread you CAN update the gui            
        print("Got response... hide bar...or something",data)
        if response.json()["resultCode"] == 100:
            print("success")
            data = {
        "realname": response.json()["data"]["realname"],
        "phone": response.json()["data"]["phone"]
            }
            with open("user_data.dat", "wb") as f:
                pickle.dump(data, f)
            
            controller.show_frame(dashboardPage.dashboard_page)

        else:
            print("error")
        bar.grid_forget()
        top.destroy()
        # End callback function

    bar.start()
    # callback needs to run in the main thread
    # root.after does not accept args so it needs to be another lambda
    # that calls the main thread ... there may be a more elegant way to do this
    callback = lambda data:root.after(1,lambda:on_response(data))
    # trigger a new thread to actually do the fetch
    threading.Thread(target=do_login,args=[user_entry.get(),pass_entry.get(),callback]).start()

See additional information about root.after

some additional answer specifically about root.after triggering main thread

below you can find a 100% verified working example(you can just copy/paste) with some extra prints to demonstrate "thready-ness"

import functools
import threading
import time
import requests
import tkinter as tk
from tkinter import ttk

root = tk.Tk()
root.geometry('400x150')
root.title('Tkinter Login Form - pythonexamples.org')

# username label and text entry box
usernameLabel = ttk.Label(root, text="User Name").grid(row=0, column=0)
username = tk.StringVar()
usernameEntry = ttk.Entry(root, textvariable=username).grid(row=0, column=1)

# password label and password entry box
passwordLabel = ttk.Label(root, text="Password").grid(row=1, column=0)
password = tk.StringVar()
passwordEntry = ttk.Entry(root, textvariable=password, show='*').grid(row=1, column=1)

labelVar = tk.StringVar(value="Waiting For Event")
label = ttk.Label(root, textvariable=labelVar).grid(row=10,column=0)
label = ttk.Label(root, text="Valid:'eve.holt@reqres.in'").grid(row=0,column=2)
def do_actual_login(username,password,callback):
    print("current thread(doing login request):", threading.current_thread())
    url="https://reqres.in/api/login"
    data={"username":username,"password":password}
    # fake delay to show not blocking...(request is pretty fast)
    time.sleep(5)
    print("ok really do request in thread")
    return callback(requests.post(url,data))

def validateLogin(username, password):
    print("current thread(on click handler):", threading.current_thread())
    bar = ttk.Progressbar(root, orient="horizontal",
                          length=100, mode="indeterminate")
    bar.grid(row=10,column=0)
    bar.start()

    def login_complete(response):
        print("current thread(login_complete):",threading.current_thread())
        bar.grid_remove()
        if response.status_code == 200:
           print("LOGIN SUCCESS!!!")
           labelVar.set("Login Success")
        else:
           print("LOGIN FAILED:",response,response.json())
           labelVar.set(response.json().get("error","Unknown Error Occurred"))
    # setup our thread and callback
    callback = lambda data:root.after(1,lambda:login_complete(data))
    threading.Thread(
        target = do_actual_login,
        args = [username.get(),password.get(),callback]
    ).start()



validateLogin = functools.partial(validateLogin, username, password)

# login button
loginButton = ttk.Button(root, text="Login", command=validateLogin).grid(row=4, column=0)

root.mainloop()

you can test a successfull login by using the username eve.holt@reqres.in with any password you want (I think) anyother username should fail as should omitting the password

Joran Beasley
  • 110,522
  • 12
  • 160
  • 179
  • your answer is extremely confusing and complicated if you could simplify it a bit – lexi Nov 10 '21 at 03:32
  • its literally your code ... i extracted the web call to its own function that gets called threaded and returns the answer back to a function so you can act on it, it calls it threaded since that wont block the gui .... ill make you a deal if you simplify your code i will alter mine to reflect your simplified code.... i made the function in line for easy access to `bar` – Joran Beasley Nov 10 '21 at 04:14
  • i have added what i tried if you can look into it and understand what i need to achieve – lexi Nov 10 '21 at 04:21
  • I edited it to include the rest of your function (its all in the on_response nested function – Joran Beasley Nov 10 '21 at 04:23
  • what is ```root.after``` in callback? – lexi Nov 10 '21 at 04:26
  • somewhere presumably you have `root = tk.Tk()` in your case you **might** be able to get away with `top.after` instead (as im not sure if that ends up as an alias to `root` or not... i usually just have a global root when i do tk stuff..) .. .what it does is schedule the function call into tk's application loop so that it will trigger in the main thread after 1 millisecond – Joran Beasley Nov 10 '21 at 04:27
  • I added a complete working example for you to demonstrate – Joran Beasley Nov 10 '21 at 05:03
  • I guess that the `return` in `do_actual_login` is there to just explicitly show the end of the function. Anyways, I am pretty sure that `tkinter` stuff shouldn't be put in other threads either, AFAIK some queues or lists should be used, the thread would add data to those and there would be a separate updater (in the same thread as all the other `tkinter` stuff) that will "loop" using the `after` method and get data from that list or queue and then update stuff – Matiiss Nov 10 '21 at 09:22
  • you can absolutely just use root.after in a thread .. . and aything you can pickle can be sent – Joran Beasley Nov 11 '21 at 03:24