0

I have recently developed a python programme that read csv files and after it processes them, return pdfs containing graphs. However, if the csv files were big, the programme froze until the processing is done. Using this approach: Link, the programme does not freeze anymore, but it starts automatically without pressing any button.

Here is the code:

try:
    import Tkinter as tk, time, threading, random, Queue as queue
except ModuleNotFoundError:   # Python 3
    import tkinter as tk, time, threading, random, queue

class GuiPart(object):
    def __init__(self, master, queue):
        self.queue = queue
        self.button1 =  tk.Button(master, text="Command", padx=10, 
                            pady=5, fg="white", bg="#263D42", command=ThreadedClient.worker_thread1)

        self.button1.pack()

    def processIncoming(self):
        while self.queue.qsize():
        pass

class ThreadedClient(object):
    """
    Launch the main part of the GUI and the worker thread. periodic_call()
    and end_application() could reside in the GUI part, but putting them
    here means that you have all the thread controls in a single place.
    """
    def __init__(self, master):
        """
        Start the GUI and the asynchronous threads.  We are in the main
        (original) thread of the application, which will later be used by
        the GUI as well.  We spawn a new thread for the worker (I/O).
        """
        self.master = master
        # Create the queue
        self.queue = queue.Queue()

        # Set up the GUI part
        self.gui = GuiPart(master, self.queue)

        # Set up the thread to do asynchronous I/O
        # More threads can also be created and used, if necessary
        self.running = True
        self.thread1 = threading.Thread(target=self.worker_thread1)
        self.thread1.start()

        # Start the periodic call in the GUI to check the queue
        self.periodic_call()


    def periodic_call(self):
        """ Check every 200 ms if there is something new in the queue. """
        self.master.after(200, self.periodic_call)
        self.gui.processIncoming()
        if not self.running:
            # This is the brutal stop of the system.  You may want to do
            # some cleanup before actually shutting it down.
            import sys
            sys.exit(1)


    def worker_thread1(self):

        """
        This is where we handle the asynchronous I/O.  For example, it may be
        a 'select()'.  One important thing to remember is that the thread has
        to yield control pretty regularly, be it by select or otherwise.
        """
    
        while self.running:
            # To simulate asynchronous I/O, create a random number at random
            # intervals. Replace the following two lines with the real thing.
        
            time.sleep(rand.random() * 1.5)

            filenames = filedialog.askopenfilenames(initialdir="/", title="Select File", filetypes = (("comma separated file","*.csv"), ("all files", "*.*"))) #ask user to select the file

    """ based on the data from csv file I am using matplotlib to draw some graphs and then I export them as pdf """

    def end_application(self):
        self.running = False  # Stops worker_thread1 (invoked by "Done" button).

rand = random.Random()
root = tk.Tk()
client = ThreadedClient(root)
root.mainloop()

I would appreciate if someone can help me to start the processing by clicking the button as I would like to add more buttons, calling more functions.

tmg28
  • 25
  • 7
  • You shouldn't use any `tkinter` functions/methods from threads other than the one where you created the `tk.Tk` window. That includes the `filedialog.askopenfilenames`. – TheLizzard Sep 15 '21 at 12:04
  • 2
    If you don't want the thread to start immediately, don't use `thread.start()` inside `__init__`. Make it the command of the button instead. – Henry Sep 15 '21 at 15:40
  • @Henry Thank you for good suggestions. I was trying to do a new function with the self.thread1.start() or to put it under worker_thread1(self), under while self running:, but when I press the button in each case I receive the following error: TypeError: worker_thread() missing 1 required positional argument: "self". Does anyone has any idea how to fix it? – tmg28 Sep 15 '21 at 19:52

2 Answers2

2

The reason the thread was starting immediately is because it was in __init__, so was called when ThreadedClient was instantiated.
Now you need a way to start the thread, so I've added a function called start_thread1. This creates and starts the thread, and we can call this function from the button.
You need to start the thread in the instance of ThreadedClient you've already made. Currently you create a new instance, but this will not work as you want to use the one you already have (client). Therefore you need to pass a reference to it in GuiPart. I've called it client_instance.
You can then start the thread using the command client_instance.start_thread1. The rest of the program is unchanged.

class GuiPart(object):
    def __init__(self, master, queue, client_instance):
        self.queue = queue
        self.button1 =  tk.Button(master, text="Command", padx=10, 
                            pady=5, fg="white", bg="#263D42", command=client_instance.start_thread1)

        self.button1.pack()

    def processIncoming(self):
        while self.queue.qsize():
            pass

class ThreadedClient(object):
    """
    Launch the main part of the GUI and the worker thread. periodic_call()
    and end_application() could reside in the GUI part, but putting them
    here means that you have all the thread controls in a single place.
    """
    def __init__(self, master):
        """
        Start the GUI and the asynchronous threads.  We are in the main
        (original) thread of the application, which will later be used by
        the GUI as well.  We spawn a new thread for the worker (I/O).
        """
        self.master = master
        # Create the queue
        self.queue = queue.Queue()
        self.running = True

        # Set up the GUI part
        self.gui = GuiPart(master, self.queue, self)
        # Start the periodic call in the GUI to check the queue
        self.periodic_call()

        
    def start_thread1(self):
        # Set up the thread to do asynchronous I/O
        # More threads can also be created and used, if necessary
        thread1 = threading.Thread(target=self.worker_thread1)
        thread1.start()
Henry
  • 3,472
  • 2
  • 12
  • 36
  • WoW! It's working! thank you @Henry ! The only modification I have done was to move self.running=True in the __init__ otherwise the GUI wouldn't start. However, there is only a little error and that arises from the fact I am not sure where to put self.running=False. I have tried to add at the end of def working_thread1(self) and then when process is finished the GUI is closed with the error: Tcl_AsyncDelete: async handler deleted by the wrong thread. Would it be possible for the GUI to stay open but without the same call from the button to start again? as I would like to add few buttons in it. – tmg28 Sep 15 '21 at 22:30
  • I have solved the problem by changing the while statement with if statement. – tmg28 Sep 16 '21 at 08:45
  • Glad it works, I've approved your edit. – Henry Sep 16 '21 at 13:03
  • However, on a second try, it didn't work. If I try to press again the button or other button, I got the same Tcl_AsyncDelete... I have tried to test the button which generates (saves) only a simple graph, without asking for any other csv file and I can do the process several times without any issue. Do you think the problem should be related filedialog.askooenfilenames ? Thank you. – tmg28 Sep 16 '21 at 15:53
  • 1
    Yes, I missed that. `filedialog` uses tkinter, and tkinter code can only be run in the main thread. I think you'll need to open the `filedialog` from `GuiPart` and then return the value to the other thread, probably with your queue. – Henry Sep 16 '21 at 20:41
  • I have tried to define a function 'files()' that calls 'filedialog' (in 'init' under GuiPart) and then 'self.queue.put(files)'. Then under 'worker_thread1' I have done a new thread 'starting = Thread(target = files, args(self.queue))' and 'starting.start()'. However, not even the GUI is opening so most likely I am overwriting something. Any idea how to do the whole returning value? Thank you – tmg28 Sep 17 '21 at 10:56
-2

You can try this way to open file box function then you can apply or ad another function one by one this is for example purpose:

from tkinter import *
from tkinter import filedialog, Text
from tkinter import messagebox

import os
root = Tk()
root.geometry('800x400')
root.title('FILE OPENER')
obj=(root)

def openbox():
    # for i in range(5):
"""I used for loop to open automatic 5 time if you don't 
   want automatic then just use hash to comment out for loop function and 
   indent out"""
    fileOpen = filedialog.askopenfilename(initialdir="C:/", title="File Finder",
                                          filetypes=(("Executable", ".exe"), 
    ("All Files", "*.*")))

    messagebox.showinfo("Thanks for Your Patience", "YOU HAVE PATIENCE")

my_Btn = Button(root, text= "CLICK ME", command=openbox).pack()
root.mainloop()
Nimantha
  • 6,405
  • 6
  • 28
  • 69
Ehsan Rahi
  • 61
  • 7