Tkinter application 'freezes' while contin

2019-03-04 10:58发布

I have two scripts:

Processor_child.py: Its purpose is to perform a number of data analysis and cleaning operations. This must perform the same operations when run alone (without Tkinter_parent.py) as it does when packaged into a GUI with Tkinter_parent.py.

Tkinter_parent.py: Its purpose is to provide a GUI for those who can't use Processor_child directly.


Within Processor_child, there are for loops that ask the user for input on each iteration. These prompts need to appear in the Tkinter app, accept the input, and send it back to Processor_child.

The code below does this, raising an Entry field whenever there's data in the Pipe (added by the loop). However, it often seems to 'freeze', getting stuck loading and not progressing through the code. Sometimes, it runs perfectly as intended. (No changes in the code in these instances.)

How can I resolve this / make it more stable? I've commented below where the 'freeze' is happening.

Tkinter_parent.py:

### Tkinter_parent.py ###
from tkinter import *
from tkinter.filedialog import askopenfilename
from tkinter import ttk
from multiprocessing import Process, Pipe
import pandas as pd
import Processor_child
import time

class GUI:
    def __init__(self, master):
        self.master = master

def gui_input(message, a_pipe = None):
    def input_done(event=None):
        entry.pack_forget()
        input_label.pack_forget()
        submit_button.pack_forget()
        a_pipe.send(entry.get())
        next_one(a_pipe)

    entry = Entry(frame)
    input_label = ttk.Label(frame, text=message)
    entry.bind("<Return>", input_done)
    submit_button = ttk.Button(frame, text="Submit", command=input_done)
    input_label.pack()
    entry.pack()
    submit_button.pack()

def file_select():
    dataset_path = askopenfilename()

    if __name__ == '__main__':
        pipe1, pipe2 = Pipe()

        some_vars = ['a var', 'another var']
        a_df = pd.read_csv(dataset_path)

        p_review = Process(target=Processor_child.review_with_user, args=(some_vars, a_df, pipe2))
        p_review.start()

        gui_input(pipe1.recv(), pipe1)

        #time.sleep(1)
def next_one(pipe1):
    while pipe1.poll() != True: ### CAUSES CONSTANT LOADING WITHOUT PROGRESSION
        time.sleep(0.1)

    gui_input(pipe1.recv(), pipe1)

if __name__ == '__main__':
    root = Tk()
    my_gui = GUI(root)
    root.style = ttk.Style()
    root.style.configure('my.TButton')
    root.style.configure('my.TLabel')

    canvas = Canvas(root)
    frame = Frame(canvas)
    frame.place()
    canvas.pack(side="left", fill="both", expand=True)
    canvas.create_window((45,50), window=frame, anchor="nw")

    ttk.Button(frame, text="Select", command=file_select).pack()

    root.mainloop()

And processor_child:

### processor_child.py ###
import pandas as pd
from multiprocessing import *
import time

def smart_print(message, a_pipe = None):
    if __name__ == "__main__":
        print(message)
    else:
        a_pipe.send(message)

def review_with_user(var_names, dataset, a_pipe = None):
    affirmed = []
    review_message = 'Yes or no?'

    if __name__ == "__main__":
        review_response = input(review_message)
    else:
        smart_print(review_message, a_pipe)
        while a_pipe.poll() != True:
            time.sleep(0.1)

        review_response = a_pipe.recv()

    if review_response in ['Yes', 'yes']:
        for v in dataset.columns:
            smart_print(dataset[v].dropna(), a_pipe)
            if __name__ == "__main__":
                local_response = input(review_message)
            else:
                while a_pipe.poll() != True:
                    time.sleep(0.1)
                local_response = a_pipe.recv()
            if local_response in ['Yes', 'yes']:
                affirmed.append(v)

        smart_print(affirmed, a_pipe)

if __name__ == "__main__":
    var_names = ['var1', 'var2']
    df = pd.read_csv('dummy.csv')
    review_with_user(var_names, df)

This is related to the broader SO question How can I implement an input method in a Tkinter parent script, with the displayed prompt and return value being sent back to a child script?, and comes from a posted, but non-functional, solution there.

As of Oct 23, 2017 there is still not a solution to this.

4条回答
啃猪蹄的小仙女
2楼-- · 2019-03-04 11:21

The simplest way is to get the input, either from the console or gui, and then send the results to the child program. When you ask for input from the console, add a statement that opens Tkinter instead if some variable is set, and gets the info there.

查看更多
聊天终结者
3楼-- · 2019-03-04 11:27

Consider writing your app in a client-server fashion.

The client, is the Tk app, which can connect to the server. the server, simply executes whatever the client requires. this way, you can detach the processing. there are several ways you can do this, like cherrypy, rabbitmq and similar.

Recently, in desktops apps, I've used Electron, to connect to a cherrypy server, and AJAX requests from Electron using Javascript. the final icon simply starts both, the server and the client. this allows me to have a richer widget set, since the web is more powerful than Tk.

That will allow you in a possible future to have a webapp.

HTH

查看更多
够拽才男人
4楼-- · 2019-03-04 11:42

Your Connection.poll() call is busy-waiting and chewing through the CPU. But note that Connection objects have a fileno() method; this means you can use select/poll calls to put your process to sleep while waiting for them to become ready for I/O. Note that the tkinter event loop supports file handlers to allow you to do this without blocking the UI.

查看更多
ゆ 、 Hurt°
5楼-- · 2019-03-04 11:43

It appears that the behaviour you are trying to achieve is to communicate with a function whilst it's running. I think that your problems could be solved by using generators. A generator lets you yield multiple values from a function, and send values to that function.

Here is some more information on generators if you want to know how they work.

I'm not entirely sure if this is exactly the behaviour you want from your program, but I have modified your code to use generators rather than multiprocessing, and it no longer freezes:

Processor_child.py:

### processor_child.py ###
import pandas as pd
import time


def review_with_user(var_names, dataset):
    affirmed = []
    review_message = 'Yes or no?'

    review_response = yield review_message

    if review_response in ['Yes', 'yes']:
        for v in dataset.columns:
            local_response = yield str(dataset[v].dropna())+"\n"+review_message

        yield affirmed

if __name__ == "__main__":
    var_names = ['var1', 'var2']
    df = pd.read_csv('dummy.csv')
    gen = review_with_user(var_names, df)
    # since it is now a generator, you need yo write some code to communicate with it via the console
    # it doesn't print to the console or recieve input unless you do this manually
    while True:
        try:
            print(next(gen))
        except StopIteration:
            break
        print(gen.send(input()))

Tkinter_parent.py:

### Tkinter_parent.py ###
from tkinter import *
from tkinter.filedialog import askopenfilename
from tkinter import ttk
import pandas as pd
import Processor_child
import time

class GUI:
    def __init__(self, master):
        self.master = master

def gui_input(message, p_review):
    def input_done(event=None):
        entry.pack_forget()
        input_label.pack_forget()
        submit_button.pack_forget()
        try:
            p_review.send(entry.get())
            next_one(p_review)
        except StopIteration:
            # this code is executed when there is no more output from Processor_child.review_with_user
            return

    entry = Entry(frame)
    input_label = ttk.Label(frame, text=message)
    entry.bind("<Return>", input_done)
    submit_button = ttk.Button(frame, text="Submit", command=input_done)
    input_label.pack()
    entry.pack()
    submit_button.pack()

def file_select():
    dataset_path = askopenfilename()

    if __name__ == '__main__':
        some_vars = ['a var', 'another var']
        a_df = pd.read_csv(dataset_path)

        p_review = Processor_child.review_with_user(some_vars, a_df)

        gui_input(next(p_review), p_review)

def next_one(p_review):
    try:
        gui_input(next(p_review), p_review)
    except StopIteration:
        # this code is executed when there is no more output from Processor_child.review_with_user
        return

if __name__ == '__main__':
    root = Tk()
    my_gui = GUI(root)
    root.style = ttk.Style()
    root.style.configure('my.TButton')
    root.style.configure('my.TLabel')

    canvas = Canvas(root)
    frame = Frame(canvas)
    frame.place()
    canvas.pack(side="left", fill="both", expand=True)
    canvas.create_window((45,50), window=frame, anchor="nw")

    ttk.Button(frame, text="Select", command=file_select).pack()

    root.mainloop()

Generators will throw a StopIteration exception when you call next() on them and they have finished, so be sure to put next(p_review) and and p_review.send(...) calls inside try blocks where appropriate.

查看更多
登录 后发表回答