1

I have threads that use some class's functions , and those functions print alot of stuff , that i want to display on a Text() widget .

So i tried making the window in the class as a class variable and the command : mainloop() seems to stop everything from continuing ....

Is there any solution for that ?

The general idea i want to do : (converting the console to GUI..)

from tkinter import *


root = Tk()
textbox = Text(root)
textbox.pack()

def redirector(inputStr):
    textbox.insert(INSERT, inputStr)

sys.stdout.write = redirector
root.mainloop()

the whole code :

import threading
from queue import Queue
from Spider import Spider
from domain import *
from general import *
from tkinter import *



def mmm(answer1,answer2,master):  # answer1,answer2 are user inputs from the first GUI that gets info, master is the root so i can close it

    master.destroy()
    PROJECT_NAME = answer1
    HOMEPAGE = answer2
    DOMAIN_NAME = get_domain_name(HOMEPAGE)
    QUEUE_FILE = PROJECT_NAME + '/queue.txt'
    CRAWLED_FILE = PROJECT_NAME + '/crawled.txt'
    NUMBER_OF_THREADS = 8

    queue = Queue()  # thread queue
    Spider(PROJECT_NAME, HOMEPAGE, DOMAIN_NAME) # a class where the prints happen and some other functions.

    root = Tk()
    textbox = Text(root)
    textbox.pack()

    def redirector(inputStr):
        textbox.insert(INSERT, inputStr)

    sys.stdout.write = redirector
    root.mainloop()
    # create threads (will die when exit)
    def create_threads():
        for x in range(NUMBER_OF_THREADS):
            t = threading.Thread(target=work)
            t.daemon = True
            t.start()


    # do the next link in the queue
    def work():
        while True:
            url = queue.get()
            Spider.crawl_page(threading.current_thread().name, url)
            queue.task_done()


    # each link is a new job
    def create_jobs():
        for link in file_to_set(QUEUE_FILE):
            queue.put(link)  # put the link in the thread queue
        queue.join()  # block until all processed
        crawl()


    # if there are items in the queue, crawl them
    def crawl():
        queued_links = file_to_set(QUEUE_FILE)
        if len(queued_links) > 0:
            print(str(len(queued_links)) + ' links in the queue')
            create_jobs()


    create_threads()
    crawl()
Itay Katsnelson
  • 69
  • 1
  • 2
  • 8

2 Answers2

1

As soon as you start the mainloop(), you get an event driven app that runs in a loop. Any code that is placed after the line root.mainloop() will run only after the GUI is terminated. It is expected that your GUI is more or less self contained. You populate it with tkinter widgets that will have some events bound to them, each event with its proper callback function.

Be aware, however, that tkinter is not thread safe. You will need to separate very well the theading code, ensuring that it does not call any GUI widgets, for instance. In this page you can find a Python2 example on how to do threading with tkinter.

But maybe you don't even need threads. You can for instance schedule a function to run every X seconds with after(), wich may read an updated log file or get updated values from a database, and update the GUI accordingly. You can find some examples and explanation in this page.

Victor Domingos
  • 1,003
  • 1
  • 18
  • 40
  • ye so i got this error that it need to be in the main thread ... what does it mean ? – Itay Katsnelson May 04 '17 at 11:45
  • I am not being able to reproduce your issue here with the code sample you provided... it runs and shows an empty text widget in a tkinter window. It is behaving as you coded, i believe. Now if what you want is to have a text widget that shows in realtime whatever shows up in the console and also reacts to user input, it will be a much more complex task. I am not sure if it is possible, it may have some limitations. – Victor Domingos May 04 '17 at 12:16
  • yes my bad i didnt link my main code .... that was only the example ill edit it 1 sec – Itay Katsnelson May 04 '17 at 12:24
  • You reassign `sys.stdout.write` as the function `redirector`, but it never gets called in the code above. However, by doing so, I am afraid it will interfere with tkinter. I think you can't call directly a method in a widget from a different thread. It would be safer, and probably easier, to have that output written to a file and then have another task monitoring for recent filesize changes. That way you could even do it by using 2 separate applications, running two different Python instances (one that writes the file, and the other one with a GUI that dislay it updating itself periodically). – Victor Domingos May 04 '17 at 15:24
  • so basically ill have the threads writing to a file and in the same time a diffrent thread putting it on screen ? will i have any problems with writing and reading at the same time ? and how can i avoid the error of main thread ? – Itay Katsnelson May 04 '17 at 16:48
  • The are certainly other SO members much more qualified and experienced than me (still learning about threading, in order to see if I can integrate it in one or two of my projects), and I hope someone throws some more light in. One thing I think I have learned is that we can't mix threading with calls to tkinter widgets. It must be clearly separate. I am not sure if you will get an error if you try to read and write the file at the same time. That's an important and interesting question. I would guess that it could happen as the file gets bigger, especially if I/O operations are very frequent. – Victor Domingos May 04 '17 at 16:57
1

A @Victor Domingos's mentions are really usefull in your case, but your real problem - your own code! First of all - take a look at structure of your application and understand, that it's weak, no offence (you even pass a master to a function to destroy it). So I suggest you to read about classes and inheritance in Python (if you don't already) and then take a look here.

Next stop - your redirector. You reassign sys.stdout.write, but you never preserve it - so it another weak spot. Ok, let's say that now you preserve it, but if we keeping object oriented approach - I would prefer this option.

Also, is it really necessary to destroy the master? For output you can use a Toplevel widget if you destroing master just to avoid two mainloop's. You can even hide root while Toplevel is active. Marvelous, isn't it?

Finally, to answer your question about solution. There're no straight solution, but only one: read and try. You're already answered why mainloop stops everything, but your question is really broad.

I tried to reproduce your full program (2-window app, 1st-user input, 2nd - console-like and some example printing task with thread) and here's a code:

# imports:
try:
    import tkinter as tk
except ImportError:
    import Tkinter as tk

import sys
import string
import random
import threading


# classes:
class ReStdout:
    # common stdout-redirector
    def __init__(self, target_widget, start_redirection=True):
        self.text_console = target_widget
        if start_redirection:
            self.start_redirection()

    def __del__(self):
        self.stop_redirection()

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.stop_redirection()

    def __enter__(self):
        pass

    def flush(self):
        pass

    def write(self, stdout_line):
        self.text_console.insert('1.0', stdout_line)

    def start_redirection(self):
        sys.stdout = self

    @staticmethod
    def stop_redirection():
        sys.stdout = sys.__stdout__


class App(tk.Tk):
    # common tk app
    def __init__(self):
        tk.Tk.__init__(self)
        self.resizable(width=True, height=False)
        self.minsize(width=250, height=25)
        self.some_entry = tk.Entry(self)
        self.some_entry.insert(0, 'You can pass something to Spawner!')
        self.some_entry.pack(expand=True, fill='x')
        self.start_spawner_button = tk.Button(self, text='Start Spawner', command=self.spawn_spawner)
        self.start_spawner_button.pack(expand=True, fill='x')

    def spawn_spawner(self):
        Spawner(self, self.some_entry.get())

    def close_app(self):
        self.destroy()


class Spawner(tk.Toplevel):
    # common tk app - task spawner
    def __init__(self, master, entry_string):
        tk.Toplevel.__init__(self, master)
        self.resizable(width=False, height=False)
        self.preserved_count = threading.active_count()
        self.master = master
        self.master.withdraw()

        self.spawn_task_button = tk.Button(self, text='Spawn Task', command=spawn_task)
        self.spawn_task_button.pack(expand=True, fill='x')

        self.quit_button = tk.Button(self, text='Quit', command=self.close_app)
        self.quit_button.pack(expand=True, fill='x')

        self.text = tk.Text(self, bg='black', fg='white')
        self.text.pack(expand=True, fill='both')
        self.stdout = ReStdout(self.text)
        self.protocol('WM_DELETE_WINDOW', self.close_app)

        # print what have been passed
        print('Users Input: %s' % entry_string)

        # let's spawn something right now
        # after here just for example
        # try to use it w/o after
        self.after(500, multi_spawn)

    def close_app(self):
        if threading.active_count() == self.preserved_count:
            self.stdout.stop_redirection()
            self.master.deiconify()
            self.destroy()
        else:
            # code to handle threads
            print('\n**** Cant quit right now! ****\n')


# non - class functions:
def multi_spawn(count=1):
    for _ in range(count):
        spawn_task()


def spawn_task():
    task_count = threading.active_count()
    task = threading.Thread(target=lambda: common_task(comment='%d printing task' % task_count,
                                                       iteration_count=random.randint(1, 10)))
    task.start()


def common_task(comment, iteration_count=1):
    # example task wait + print
    task_numb = comment.split(None, 1)[0]
    print('\nTask spawned:\n%s\nIteration count: %d\n' % (comment, iteration_count))

    for _ in range(iteration_count):
        threading.Event().wait(1)
        print('Task: %s \t Iteration: %d \t Generated: %s' % (task_numb, _ + 1, generate_smth()))

    print('\nTask %s completed!' % task_numb)


def generate_smth(size=6, chars=string.ascii_uppercase + string.digits):
    # generate random
    return ''.join(random.choice(chars) for _ in range(size))

# entry-point:
print('Just another example from SO')
app = App()
app.mainloop()
print('Beep')

As you see - I never get stucked (when I don't needed it) in mainloop, because I create threads on events: __init__ of "Spawner" (thanks to inheritance) and a button click event. Of course, it's just one approach from many, but I wish that now your problem is clearer to you.

Community
  • 1
  • 1
CommonSense
  • 4,232
  • 2
  • 14
  • 38
  • thanks so much for commenting :) what do you mean by TopLevel app ? what is the use of classes? , i dont really understand what for .. and i destory master so i closes... – Itay Katsnelson May 08 '17 at 09:17
  • 'Toplevel' just another tkinter widget like button or entry. [There're good material to tkinter in general](http://effbot.org/tkinterbook/). Your question about classes is really broad too. But here's an [another link](https://jeffknupp.com/blog/2014/06/18/improve-your-python-python-classes-and-object-oriented-programming/) about it. And feel free to rewrite my code if it's looks like what you want! – CommonSense May 08 '17 at 09:25
  • I understand ur code but its really hard for me to use it since its not really close to what im doing... can u maybe tell me what to delete and where to put my own code in ? because im like lost in this code – Itay Katsnelson May 10 '17 at 10:29