1
import subprocess
import os 
import time
from tkinter import *
root=Tk()
textbox=Text(root)
textbox.pack()


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

def address_ping():
        '''
        DOCSTRING -> Ip addresses in servers.txt are
                     192.168.0.1     192.168.0.26
        '''
        while True:
            with open('servers.txt', 'r') as f:
                for ip in f:
                    result=subprocess.Popen(["ping", "-c", "7", "-n", "-W", "2", ip],stdout=f, stderr=f).wait()
                    if result:
                        print("ip address " + ip, "is inactive")
                        sys.stdout.write = redirector

                    else:
                        print("ip address " + ip, "is active")
                        sys.stdout.write = redirector
                    pass    

address_ping()        

root.mainloop()

I am writing a piece of code here that will send a ping to an IP address and return a result. It works fine on the CLI, however I wish to "print" it to a Text widget using Tkinter. I am at a point where it will send it to the Text widget GUI but it only makes itself visible after I interrupt the program. I want to have a rolling output to the GUI text area as the pings progress through a loop.

martineau
  • 119,623
  • 25
  • 170
  • 301
RLynch
  • 35
  • 8
  • 1
    search this site for `[tkinter] sleep` and you'll find hundreds of questions and answers related to using `sleep` in a tkinter program. Short version: you should never use `sleep` in the main thread of a tkinter application. – Bryan Oakley Feb 14 '19 at 19:00
  • Hi Bryan, thanks for the response. I wasn't aware of not including the sleep function within Tkinter. I have removed it and the program is still pretty much behaving the same - my text area is filled up with more ping entries. – RLynch Feb 14 '19 at 19:07
  • You may be able to use or adapt the code in the `errorwindow3k` module posted in this [answer](https://stackoverflow.com/a/49016673/355230) to do what you want. BTW, you can use the universal tkinter [`after()`](http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/universal.html) method to "sleep" in a tkinter app. – martineau Feb 14 '19 at 20:09
  • @martineau: it's a bad idea to ever sleep in the main thread of a GUI. If you're referring to the multi-argument form of `after`, it doesn't sleep -- it simply adds something to a queue to be executed later. – Bryan Oakley Feb 14 '19 at 20:32
  • @Bryan: I was referring to the single argument form of using `after()` (i.e. without a `callback` argument) and am aware of that it it really doesn't sleep—but think that it would do what the OP wants to happen in this scenario (i.e. since `subprocess` is being used). – martineau Feb 14 '19 at 20:37
  • Thanks guys for the info. The after(), on the face of it anyway, appears to behave the same as the call to sleep. However, my GUI window will still not appear until after I cancel/interrupt the running of the program. – RLynch Feb 14 '19 at 20:45
  • @martineau: `after(1000)` would do exactly the same as `time.sleep(1)`, which is to say it will freeze the application. – Bryan Oakley Feb 14 '19 at 20:51
  • @RLynch: Using `while True:` behave the same as using `time.sleep(...` it prevents the `root.mainloop()` from running. First you have to understand **event driven programming**. – stovfl Feb 14 '19 at 20:53
  • stovfl - thank you. You set me on the correct path. I set root.update() after if and else, and placed the while loop before the call to the function. – RLynch Feb 14 '19 at 21:08
  • 1
    *"I set root.update() after if and else"*: This is still not **event driven programming**. Read [Run your own code alongside Tkinter's event loop](https://stackoverflow.com/questions/459083/how-do-you-run-your-own-code-alongside-tkinters-event-loop) – stovfl Feb 14 '19 at 21:16
  • @Bryan: I was under the impression that using `after()` (with or without an associated callback) allowed `mainloop` to continue to run. If not, what would the point be of _ever_ using (the single argument form) of it instead of `time.sleep()`? – martineau Feb 14 '19 at 21:56
  • 1
    @martineau: in my mind there's never a reason. The canonical docs are pretty clear: _"While the command is sleeping the application does not respond to events."_ – Bryan Oakley Feb 14 '19 at 22:02
  • @Bryan: Hmm, thanks for clarifying—although given that, it seems odd it's even permissible to call `after()` without a callback argument. Wonder what they were thinking... – martineau Feb 14 '19 at 23:15
  • @martineau: you have to remember that tkinter is just a wrapper around a tcl interpreter. You can use `after` in tcl without tk, and in that context it can make sense. It was probably supported in tkinter for completeness. – Bryan Oakley Feb 14 '19 at 23:17

1 Answers1

1

Here's something that uses multithreading that seems to do what you want. The main program is split into a portion that handles the QUI and a separate workerthread that manages the ping subprocess, collecting the results from it and putting them in a Queue whose contents periodically get transferred to the GUI.

It uses time.sleep() because it's done in a separate thread that's not using tkinter so it's OK.

Note, seems likely you'll might want to add a vertical scrollbar to the GUI.

import subprocess
import queue
import threading
import time
import tkinter as tk


class GuiPart:
    def __init__(self, master, queue, end_command):
        self.queue = queue
        self.master = master
        self.textbox = tk.Text(root)
        self.textbox.pack()
        btn = tk.Button(master, text='Quit', command=end_command)
        btn.pack(expand=True)

    def process_incoming(self):
        """ Handle all messages currently in the queue. """
        while self.queue.qsize():
            try:
                info = self.queue.get_nowait()
                self.textbox.insert(tk.INSERT, info)
            except queue.Empty:  # Shouldn't happen.
                pass


class ThreadedClient:
    """ 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 keeps all the thread controls in a single place.
    """
    def __init__(self, master):
        self.master = master
        self.queue = queue.Queue()

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

        # Set up the background processing thread.
        self.running = True
        self.thread = threading.Thread(target=self.workerthread)
        self.thread.start()

        # Start periodic checking of the queue.
        self.periodic_call(200)

    def periodic_call(self, delay):
        """ Every delay ms process everything new in the queue. """
        self.gui.process_incoming()
        if not self.running:
            sys.exit(1)
        self.master.after(delay, self.periodic_call, delay)

    # Runs in separate thread - NO tkinter calls allowed.
    def workerthread(self):
        while self.running:
            with open('servers.txt', 'r') as file:
                for ip in file:
                    rc = subprocess.Popen(["ping", "-c", "7", "-n", "-W", "2", ip]).wait()
                    if rc:
                        self.queue.put('ip address {} is inactive\n'.format(ip))
                    time.sleep(1)

    def end_application(self):
        self.running = False  # Stop queue checking.
        self.master.quit()


if __name__ == '__main__':
    root = tk.Tk()
    root.title('Pinger')
    client = ThreadedClient(root)
    root.mainloop()  # Display application window and start tkinter event loop.
martineau
  • 119,623
  • 25
  • 170
  • 301