0

My GUI holds 8 Buttons. Each Button click calls the event handler which then forwards to another function. All of these actions take about 4 seconds. My problem is that this causes the Button to stay in SUNKEN state while the event is handled and also causes the other Buttons to be non-responsive.

What I would like is to release the Button from the SUNKEN state immediately after the click and continue the event handling in the background.

How can this be solved ? Is there a way to make the button released before the event handler finished its job ?

After Editing:

Here is my code:

from tkinter import Tk, Menu, Button
import telnetlib
from time import sleep


On_Color = '#53d411'
Off_Color = '#e78191'


def Power_On_Off (PowerSwitchPort, Action):
    '''
    Control the Power On Off Switch
    '''

    on_off = telnetlib.Telnet("10.0.5.9", 2016)

    if Action == 'On' or Action =='ON' or Action == 'on':
        StringDict = {'1': b"S00D1DDDDDDDE", '2': b"S00DD1DDDDDDE", '3': b"S00DDD1DDDDDE", '4': b"S00DDDD1DDDDE",
                         '5': b"S00DDDDD1DDDE", '6': b"S00DDDDDD1DDE", '7': b"S00DDDDDDD1DE", '8': b"S00DDDDDDDD1E"}
    elif Action == 'Off' or Action =='OFF' or Action == 'off':
        StringDict = {'1': b"S00D0DDDDDDDE", '2': b"S00DD0DDDDDDE", '3': b"S00DDD0DDDDDE", '4':  b"S00DDDD0DDDDE",
                         '5': b"S00DDDDD0DDDE", '6': b"S00DDDDDD0DDE", '7': b"S00DDDDDDD0DE", '8': b"S00DDDDDDDD0E"}

    PowerSwitchPort = str(PowerSwitchPort)

    on_off.read_eager()
    on_off.write(b"S00QLE\n")
    sleep(4)

    on_off.write(StringDict[PowerSwitchPort])

    on_off.close()

def OnButtonClick(button_id):
    if button_id == 1:
        # What to do if power_socket1 was clicked
        Power_On_Off('1', 'Off')
    elif button_id == 2:
        # What to do if power_socket2 was clicked
        Power_On_Off('1', 'On')

def main ():
    root = Tk()

    root.title("Power Supply Control")  #handling the application's Window title
    root.iconbitmap(r'c:\Users\alpha_2.PL\Desktop\Power.ico')   # Handling the application icon

    power_socket1 = Button(root, text = 'Socket 1 Off', command=lambda: OnButtonClick(1), bg = On_Color)
    power_socket1.pack()
    power_socket2 = Button(root, text = 'Socket 1 On', command=lambda: OnButtonClick(2), bg = On_Color)
    power_socket2.pack()  
    '''
    Menu Bar
    '''
    menubar = Menu(root)

    file = Menu(menubar, tearoff = 0)   # tearoff = 0 is required in order to cancel the dashed line in the menu
    file.add_command(label='Settings')
    menubar.add_cascade(label='Options', menu = file)

    root.config(menu=menubar)

    root.mainloop()

if __name__ == '__main__':
    main()

As can be seen, I create 2 Buttons one Turns a switch On and the other turns it Off. On/Off actions are delayed in about 4 seconds. This is only small part of my Application that I took for example. In my original code I use a Class in order to create the GUI and control it

Moshe S.
  • 2,834
  • 3
  • 19
  • 31
  • 1
    It depends on your code, but in general you should parallelize your "these 4 seconds actions" via threads ([worth reading](https://stackoverflow.com/questions/16745507/tkinter-how-to-use-threads-to-preventing-main-event-loop-from-freezing)). – CommonSense Jan 15 '18 at 08:24
  • Using Threads is something that I wanted to avoid as I am not experienced enough in Python Threading. This is why I asked if there is a solution within Tkinter to release the button without waiting for the handler to finish its job. – Moshe S. Jan 15 '18 at 08:51
  • Sure, it can be done without threads (**in the context of your question**), but still, 4 seconds is a long time and it leads to unresponsive GUI, hence some sort of paralleling is a must. Anyway, can you update your question with a [mcve]? It would greatly improve your question, because right now there's no answer, except "use threading, subprocess, asyncio, ets" one. – CommonSense Jan 15 '18 at 09:35

2 Answers2

1

Trivia

Your problem stems from these lines of code:

on_off.write(b"S00QLE\n")
sleep(4)

especially from sleep. It's very weak pattern, because you're expecting a completion of S00QLE in four seconds. No more and no less! And while telnet actually works for these four seconds, the GUI sleeps. Therefore your GUI is in unresponsive state and can't redraw the relief of the button.

The good alternative to sleep is the after - so you can schedule an execution:

#    crude and naive fix via closure-function and root.after
def Power_On_Off (PowerSwitchPort, Action):
    ...
    def write_switch_port_and_close():  
        on_off.write(StringDict[PowerSwitchPort])
        on_off.close()

    on_off.write(b"S00QLE\n")
    #   sleep(4)
    root.after(4000, write_switch_port_and_close)
    ...

Solution

To overcome this problem, you can use generic after self-sheduling loop.

In my example I connect to public telnet server, which broadcasts Star Wars Episode IV (not an add!), to simulate a long-running process.

Of course, your execute (write) two commands in telnet, to represent this behaviour we recive a telnet broadcast until the famous "Far away" line is found (first long-run operation (write)). After that, we update the label-counter and then we connect to the broadcast again (second long-run operation (write)).

Try this code:

import tkinter as tk
import telnetlib


class App(tk.Tk):
    def __init__(self):
        super().__init__()

        #   store telnet client without opening any host
        self.tn_client = telnetlib.Telnet()

        #   label to count "far aways"
        self.far_aways_encountered = tk.Label(text='Far aways counter: 0')
        self.far_aways_encountered.pack()

        #   start button - just an open telnet command ("o towel.blinkenlights.nl 23")
        self.button_start = tk.Button(self, text='Start Telnet Star Wars', command=self.start_wars)
        self.button_start.pack()

        #   start button - just an close telnet command ("c")
        self.button_stop = tk.Button(self, text='Stop Telnet Star Wars', command=self.close_wars, state='disabled')
        self.button_stop.pack()

        # simple counter
        self.count = 0

    def start_wars(self):
        #   "o towel.blinkenlights.nl 23"
        self.tn_client.open('towel.blinkenlights.nl', 23)

        #   enabling/disabling buttons to prevent mass start/stop
        self.button_start.config(state='disabled')
        self.button_stop.config(state='normal')

        #   scheduling
        self.after(100, self.check_wars_continiously)

    def close_wars(self):
        #   "c"
        self.tn_client.close()

        #   enabling/disabling buttons to prevent mass start/stop
        self.button_start.config(state='normal')
        self.button_stop.config(state='disabled')

    def check_wars_continiously(self):
        try:
            #   we're expect an end of a long-run proccess with a "A long time ago in a galaxy far far away...." line
            response = self.tn_client.expect([b'(?s)A long time ago in a galaxy far,.*?far away'], .01)
        except EOFError:
            #   end of file is found and no text was read
            self.close_wars()
        except ValueError:
            #   telnet session was closed by the user
            pass
        else:
            if response[1]:
                #   The "A long time ago in a galaxy far far away...." line is reached!
                self.count += 1
                self.far_aways_encountered.config(text='Far aways counter: %d' % self.count)

                #   restart via close/open commands (overhead)
                self.close_wars()
                self.start_wars()
            else:
                if response[2] != b'':
                    #   just debug-print line
                    print(response[2])

                #   self-schedule again
                self.after(100, self.check_wars_continiously)


app = App()
app.mainloop()

So the answer is: the simplest alternative to your specific sleep commands is combination of two functions: after and expect (or only expect if it's a console-application)!

Links

CommonSense
  • 4,232
  • 2
  • 14
  • 38
  • Your analysis regarding the cause for the delayed behavior of the Button is correct. The sleep is a simple enough implementation for a needed "wait" between the two write commands to the controller. I believe that it will take around the 2 seconds without the sleep. So the parallelization of the script is more than needed. Don't you also think so ? – Moshe S. Jan 15 '18 at 14:02
  • @Moshe S., yes, it's an overhead to create a thread/process just to `sleep` for 4 seconds. Since most of [`telnetlib`](https://docs.python.org/3.7/library/telnetlib.html) functions doesn't block execution - you're free from `sleep` (and from `threading`/`multiprocessing`). – CommonSense Jan 15 '18 at 14:10
  • @Moshe S., also, `>it will take around the 2 seconds` actually means that the [`socket`](https://docs.python.org/3.7/library/socket.html) (server-side) is busy for 2 seconds, not the GUI. For example, `on_off.write(b"S00QLE\n")` line is fast, because [`write`](https://docs.python.org/3.7/library/telnetlib.html#telnetlib.Telnet.write) doesn't blocks execution (only when connection is blocked, as stated in description). So pay attention to the functions that you use! – CommonSense Jan 15 '18 at 14:24
  • I am trying to use your suggestion with the after method and I can't because none of the tkinter widgets are known to this function. By the way, this function is in use also in applications where I don't utilize the tkinter. What is the alternative for the after method ? – Moshe S. Jan 15 '18 at 14:27
  • In my case the controller doesn't accept two write commands in a row this is why I added the sleep. Maybe the controller can sign when it is possible to send another write command. But, I am not aware of this ability. This is why I preferred to add sleep time. Maybe 4 seconds is more than needed. But I wanted to be on the safe side – Moshe S. Jan 15 '18 at 14:38
  • @Moshe S., there's no alternative to the `after`, so your should re-think your application design. For example, [this](https://stackoverflow.com/q/17466561/6634373) is how it usually done in class approach. In your example you can define `root` as a global variable or even pass `after` as an argument to the `Power_On_Off`. It's very difficult to tell you in the comments section what you really need to do with `telnetlib`, with design, with gui/console/universal versions of your function (maybe with `multiprocessing`, but is it worth it?) and how to couple all of this into one piece. – CommonSense Jan 15 '18 at 14:59
0

If you have to handle a computation heavy process in your Gui while allowing the rest to run you can just multi-process:

import multiprocess as mp

def OnButtonClick(bid):
    mp.Process(target=Power_On_Off,args=('1',('Off','On')[bid-1])).start()

Answers to comments

  1. mp.Process defines a Process object. Once I .start() it the process will call the function in target, with arguments args. You can see the use here for example.
  2. You can of course separate, and pass each id on with different on off choices. I implemented what you wrote.
  3. No need for a queue here if I just want it to run and do something while I continue clicking my buttons.
  4. Your problem was that the button is not released until action ends. By delegating the action to a different process you can return immediately, thus allowing the button to raise.

Your problem isn't really "button isn't rising fast enough". Rather it's the Gui is stuck until Power_On_Off completes. If I run that function in a different process, then my Gui won't be stuck for any noticeable time.

If you want something in the main process to happen when the other process ends, you may need to keep the Process in some sort of list for future pinging. Otherwise, this is fine.

kabanus
  • 24,623
  • 6
  • 41
  • 74
  • What is the args being used for ? Why do you convert "bid" to string ? How can I use it so that once I can send Off and in another chance send On command ? – Moshe S. Jan 15 '18 at 11:09
  • Aren't you missing a Queue for holding the jobs ? By the way, I don't see how is this going to return back to the Button and release while continuing to handle the Event – Moshe S. Jan 15 '18 at 11:17
  • Thanks for the answer. Why do you do [bid-1] ? – Moshe S. Jan 15 '18 at 11:36
  • @MosheS. Just a lazy hack for saving a line. `('Off','On')[bid-1]` will evaluate to `'Off'` when `bid==1`, and to `'On'` when `bid==2`. – kabanus Jan 15 '18 at 11:37
  • 1
    @MosheS. You're over thinking - `('off','on')` is just a tuple (you could use a list, `['Off','On']`). Now I'm using `bid` as an index to get what I want from the list, so `mylist[bid-1]` gets the `bid-1` cell from the list. – kabanus Jan 15 '18 at 11:56