-2

I am struggling to get a simple GUI to work with python (2.7) on Raspberry Pi (Buster).

I have tried Tkinter and PySimpleGUI but both of these effectively use while loops and I already have such a loop in my program, which is working without issue. Both Tkinter and PySimpleGUI block the main loop.

Attempts to run Tkinter and PySimpleGUI in a thread do not seem to work, various elements are not thread compliant and hang with various error messages. In any case it appears that once in a thread you can't interact with the GUI widgets unless you create complicated queues and they are not thread safe.

All I am trying to do is write out the print 'Alert Door statement (last line of the while loop) into a text field (something pretty / colourful) and then have an alarm sound (GPIO) and a button which clears the alarm and text field.

Any thoughts apreciated, it appears the available Pi GUIs are just not suitable. Be gentle, I am a newbie.

def vp_start_gui():
        #'''Starting point when module is the main routine.'''
                global val, w, root
                root = tk.Tk()
                top = Toplevel1 (root)
                redalert_support.init(root, top)
                root.mainloop()
                w = None


def destroy_Toplevel1():
    global w
    w.destroy()
    w = None

class Toplevel1:
    def __init__(self, top=None):
        '''This class configures and populates the toplevel window.
           top is the toplevel containing window.'''
        _bgcolor = '#d9d9d9'  # X11 color: 'gray85'
        _fgcolor = '#000000'  # X11 color: 'black'
        _compcolor = '#d9d9d9' # X11 color: 'gray85'
        _ana1color = '#d9d9d9' # X11 color: 'gray85'
        _ana2color = '#ececec' # Closest X11 color: 'gray92'
        font13 = "-family {Segoe UI} -size 22"
        font14 = "-family {Segoe UI} -size 21"

        top.geometry("480x300+267+205")
        top.minsize(120, 1)
        top.maxsize(1028, 749)
        top.resizable(1, 1)
        top.title("SecuriCode RedAlert")
        top.configure(background="#4339fb")

        self.Button1 = tk.Button(top)
        self.Button1.place(relx=0.333, rely=0.667, height=54, width=177)
        self.Button1.configure(activebackground="#ececec")
        self.Button1.configure(activeforeground="#000000")
        self.Button1.configure(background="#ffff00")
        self.Button1.configure(cursor="fleur")
        self.Button1.configure(disabledforeground="#a3a3a3")
        self.Button1.configure(font=font14)
        self.Button1.configure(foreground="#000000")
        self.Button1.configure(highlightbackground="#d9d9d9")
        self.Button1.configure(highlightcolor="black")
        self.Button1.configure(pady="0")
        self.Button1.configure(text='''Clear Alarm''')

        self.Text1 = tk.Text(top)
        self.Text1.place(relx=0.125, rely=0.133, relheight=0.423, relwidth=0.756)

        self.Text1.configure(background="white")
        self.Text1.configure(font=font13)
        self.Text1.configure(foreground="black")
        self.Text1.configure(highlightbackground="#d9d9d9")
        self.Text1.configure(highlightcolor="black")
        self.Text1.configure(insertbackground="black")
        self.Text1.configure(selectbackground="#c4c4c4")
        self.Text1.configure(selectforeground="black")
        self.Text1.configure(wrap="word")

vp_start_gui()


while 1:

    #Receive data from LAN device
    reply = s.recv(34)

    #Receive replies from SMS Gateway
    #smsreply = g.recv(34)   

    #Check for Lan device keep alive replies
    if reply.find(alivereply) != -1:
        #print 'Device alive'
        print reply

    #Check for valid Tag strings
    if reply.find(tagstring) != -1:
        print 'Tag string received'
        print reply

        #Send SMS alert message
        doorcode = reply[5:6]
        doornumber = int(doorcode, 16)
        #print doornumber

        tagcode = reply[8:9]
        tagnumber = int(tagcode, 16)
        #print tagnumber

        print 'Alert Door ' +str(doornumber) + '  Tag '  +str(tagnumber) 
Ralph
  • 115
  • 1
  • 13
  • Have you tried to thread your socket part of the code? – Deepthought Jun 19 '20 at 10:29
  • I did but my skills are limited and I suspect my syntax was wrong. There is another thread before the while 1 and it seemed to cause a conflict (but again my knowledge in this area is weak) – Ralph Jun 19 '20 at 10:40
  • Maybe reading article helps you introducing multithreaded to your code: https://realpython.com/intro-to-python-threading/ – Deepthought Jun 19 '20 at 10:44
  • Thanks, made some progress (3 threads running) but very messy code. Now have to figure out how to add a poll in Tkinter to listen for messages and presumably some sort of push to action a button event – Ralph Jun 19 '20 at 11:00
  • First you have to understand [Event-driven_programming](https://en.m.wikipedia.org/wiki/Event-driven_programming). Read through [`[tkinter] event driven programming`](https://stackoverflow.com/search?q=is%3Aanswer+%5Btkinter%5D+event+driven+programming+entry) – stovfl Jun 19 '20 at 13:02
  • PySimpleGUI does not block in its event loop if you add a timeout onto the window.read call. Look at the PSG documentation about async applications and you'll see some examples. There are also examples on running multiple threads if you must. You likely can just use a timeout and poll for whatever you're trying to do. – Mike from PSG Jun 22 '20 at 10:39

2 Answers2

1

Both Tkinter and PySimpleGUI block the main loop.

This is not true always.In my experience the best solution to stop tkinter from blocking the main program's loop is using root_window.update() instead of root_window.mainloop().

Using this will allow you to keep iterating through your while loop and also update your tkinter window.

Hope this helps.

UPDATE

Following your edit I had a look at your code myself.

I changed the function vp_start_gui() and created the root window outside the loop.The main goal of this code is to give you an idea about how to go about implementing your tkinter window correctly.Also about the buttons changing the active background when hovering over it; tkinter.tk Buttons don't have this feature by default.What they really mean by active backgroud is the background color when you click the button.Though there are ways to get this done using tkinter.tk I have showed an easy way to get what you want using tkinter.ttk.

import tkinter as tk
from tkinter import ttk
def vp_update_gui():
        #'''Starting point when module is the main routine.'''
                global val, w, root  
                #redalert_support.init(root, top)
                root.update()
                w = None


def destroy_Toplevel1():
    global w
    w.destroy()
    w = None

class Toplevel1:
    def __init__(self, top=None):
        '''This class configures and populates the toplevel window.
           top is the toplevel containing window.'''
        _bgcolor = '#d9d9d9'  # X11 color: 'gray85'
        _fgcolor = '#000000'  # X11 color: 'black'
        _compcolor = '#d9d9d9' # X11 color: 'gray85'
        _ana1color = '#d9d9d9' # X11 color: 'gray85'
        _ana2color = '#ececec' # Closest X11 color: 'gray92'
        font13 = "-family {Segoe UI} -size 22"
        font14 = "-family {Segoe UI} -size 21"

        top.geometry("480x300+267+205")
        top.minsize(120, 1)
        top.maxsize(1028, 749)
        top.resizable(1, 1)
        top.title("SecuriCode RedAlert")
        top.configure(background="#4339fb")

        s = ttk.Style()
        s.configure('Kim.TButton', foreground='maroon',activeforeground="yellow")
        self.Button1 = ttk.Button(top,style='Kim.TButton',text='''Clear Alarm''')
        self.Button1.place(relx=0.333, rely=0.667, height=54, width=177)


        self.Text1 = tk.Text(top)
        self.Text1.place(relx=0.125, rely=0.133, relheight=0.423, relwidth=0.756)

        self.Text1.configure(background="white")
        self.Text1.configure(font=font13)
        self.Text1.configure(foreground="black")
        self.Text1.configure(highlightbackground="#d9d9d9")
        self.Text1.configure(highlightcolor="black")
        self.Text1.configure(insertbackground="black")
        self.Text1.configure(selectbackground="#c4c4c4")
        self.Text1.configure(selectforeground="black")
        self.Text1.configure(wrap="word")


root = tk.Tk()
top = Toplevel1(root)

while 1:
    vp_update_gui()

    #Receive data from LAN device
    reply = 's.recv(34)'

    #Receive replies from SMS Gateway
    #smsreply = g.recv(34)   

    #Check for Lan device keep alive replies
    if reply == '-1':
        #print 'Device alive'
        print (reply)

    #Check for valid Tag strings
    if reply == '+1':
        print ('Tag string received')
        print (reply)

        #Send SMS alert message
        doorcode = reply[5:6]
        doornumber = int(doorcode, 16)
        #print doornumber

        tagcode = reply[8:9]
        tagnumber = int(tagcode, 16)
        #print tagnumber

        print( 'Alert Door ' +str(doornumber) + '  Tag '  +str(tagnumber) )
AfiJaabb
  • 316
  • 3
  • 11
  • Thanks, though I did try update and it presents the gui but is not updating it (button does not grey when hovered). 3 threads is messy and I suspect I will run into other issues (transferring data between threads) – Ralph Jun 19 '20 at 11:09
  • Well I guess the buttons may not be responding because your root.update() is not being called often enough.Can't tell you anything specific about the issues without looking at your full code...but root.update() is a very handy tool if you use it correctly. – AfiJaabb Jun 19 '20 at 11:19
  • Thanks again, I edited my post and added the Tkinter code. Using update() instead of mainloop() presents the GUI but it is not updating, but again my knowledge is mostly non-existent in this area. – Ralph Jun 19 '20 at 11:34
  • Hello AfiJaabb, I did try your code and it does display the Tkinter window but it is unresponsive – Ralph Jun 19 '20 at 12:31
  • Make sure that you apply all changes I've made, if you made the changes manually onto your script.Else try running my code directly.Also please note that I have used python 3.If you are using python 2 use ***import Tkinter as tk*** (instead of tkinter) and also ***import ttk*** ( instead of the ***from tkinter import ttk*** as ttk is its own module in python 2) – AfiJaabb Jun 19 '20 at 12:41
  • @Ralph ***... tkinter from blocking the main program's loop***: This is never the case, read up on [Tkinter understanding mainloop](https://stackoverflow.com/a/29158947/7414759) – stovfl Jun 19 '20 at 13:00
  • Hello AfiJaabb,retried your code but the Tkinter window is unresponsive, though I am basing this on the fact that the button / text etc doesn't change when I hover / click – Ralph Jun 19 '20 at 13:07
  • that is very strange...i am able to click on the button and also type when i run it...there is no reason for it to freeze when ***vp_update_gui()*** is being called in the while loop... – AfiJaabb Jun 19 '20 at 13:43
  • Thanks again. I can type in the text box but it only displays the typed text when there is data coing from the LAN device. How would I send the Alert Door string (in the while 1 loop) into the text field? – Ralph Jun 19 '20 at 14:07
0

Your "blocking" problem is easily solving by running your PySimpleGUI program in "async mode". The docs have quite a bit of info about using timeouts on your read call and you'll find tons of demo programs that demonstrate this technique.

Here's a sample bit of code that loops, updating a counter every 100 milliseconds. You could add polling for a hardware condition or some set of conditions instead of updating the text. This is where you would add code to poll something.

import PySimpleGUI as sg

layout = [  [sg.Text('My Window')],
            [sg.Text('Will output here', key='-OUT-')],
            [sg.Button('Exit')]  ]

window = sg.Window('A non-blocking window', layout)

count = 0

while True:
    event, values = window.read(timeout=100)    # returns every 100 milliseconds
    if event == sg.WIN_CLOSED or event == 'Exit':
        break
    window['-OUT-'].update(f'Count={count}')
    count += 1
window.close()

Mike from PSG
  • 5,312
  • 21
  • 39
  • I appreciate everybody's feedback. For anyone else's future reference Tkinter most certainly does block if you have a 'main' while 1 loop in your code and the only way to fix this properly is to run Tkinter in a thread. Now working almost perfectly. There are multiple bugs with Tkinter including not being able to shut down a GPIO output when it or the main python window is closed (despite various 'win' code snippets claimed to work) but this will do for now. This has been a most unpleasant experience, including not being able to automatically launch / lxterminal / Tkinter on rpi startup – Ralph Jun 23 '20 at 11:17
  • You CANNOT safely run tkinter in a thread. It is not "thread safe". This is not the way to run tkinter in a way that allows non-blocking processing. The way to run polled architectures such as processing GPIO, in tkinter you use a timer. As explained in the example this comment is on, you can add a timeout onto a PySimpleGUI window.read() call so that the call does not block. tkinter has a number of checks in the code for detection that it is not running as the mainthread. When it detects it is being run as another thread, it will raise an exception. – Mike from PSG Jun 24 '20 at 14:39
  • Unfortunately, in a real-world situation where you have a main while 1 loop running / processing at least 40 incoming network strings per second it is not practical / logical to use anything timed or polled to interact with it. Tkinter is running perfectly well in a thread (with no exceptions) and has been for many days under load. Not the best design but stable nonetheless. – Ralph Jun 25 '20 at 15:14
  • You can multi-thread with tkinter so getting the overlap you're wanting is possible. The restriction is on which components of the system can be run as a thread. tkinter wants to run as the mainthread and it may be stable for you at the moment, there is no guarantee it will remain that way. Your best course would be to keep tkinter as the mainthread and then run your other items as additional threads. – Mike from PSG Jun 26 '20 at 13:17