Search code examples
pythonwindowsuser-interfacetkinterpython-multithreading

How to use the close (X) or any other button while an infinite loop is running in Tkinter


I am writing a program for a GUI to support my Voice Assistant Python script using tkinter. But the problem is once the Voice Assistant program is activated, that is the infinite loop of the function of continuously listening to what the user says is executed, the whole window freezes and I cannot click on any other button and I am not being able to access the close (X) button also. I know I need to use threading in this case but I am not getting the idea of where to use it exactly to achieve my desire. I would be highly obliged if somebody would help me out with this. I am giving the whole code that I have written to help you out with the problem. The infinite running function in the program is Geega()

from tkinter import *
import tkinter.font as font
import tkinter.messagebox as tmsg
import pyttsx3 
import speech_recognition as sr 
import datetime
import wikipedia 
import webbrowser
import os
import time
import smtplib
import random
import wolframalpha
import apiai
import json
import _thread

#API configuration
engine = pyttsx3.init('sapi5')
client = wolframalpha.Client('AQKXVX-UVQ8TUWUVU')
voices = engine.getProperty('voices')
engine.setProperty('voice', voices[1].id)

#Conversation mode
def convo_mode():
    Text_frame1['text'] = "Geega has entered conversation mode..."
    Button_frame1 = Button(Frame1, image=Button_image, bg='#444444', borderwidth=0, height=200, width=200, state=DISABLED, activebackground='#444444').place(x=5,y=15)
    # Button_frame3 = Button(Frame3, text='Button', bg='white', borderwidth=0, cursor='hand2', state=DISABLED, activebackground='#444444').place(x=5,y=15)
    Button_frame3 = Button(Frame3, image=Button_image2, bg='#333333', borderwidth=0, height=80, width=250, cursor='hand2', command=convo_mode, state=DISABLED, activebackground='#333333').place(x=320,y=360)
    main_window.update()
    speak("Geega has entered Conversation mode...")
    query = takeCommand().lower()
    while "back" not in query:
        response = apiai_connc(query)
        speak(response)
        query = pause().lower()
    else:
        speak("Re-entering Normal Mode...")
        Button_frame1 = Button(Frame1, image=Button_image, bg='#444444', borderwidth=0, height=200, width=200, state=NORMAL, cursor= 'hand2', activebackground='#444444', command=Geega).place(x=5,y=15)
        # Button_frame3 = Button(Frame3, text="Button", bg='white', borderwidth=0, cursor='hand2', state=NORMAL, activebackground='#444444', command = convo_mode).place(x=5,y=15)
        Button_frame3 = Button(Frame3, image=Button_image2, bg='#333333', borderwidth=0, height=80, width=250, cursor='hand2', command=convo_mode, state=NORMAL, activebackground='#333333').place(x=320,y=360)
        Text_frame1['text'] = "Geega has entered Normal mode..."
        main_window.update()


#Function for instructions
def Instructions():
    Instruction = tmsg.showinfo("Instructions", "- Click the mic to activate Geega\n\n- Say \'Hello Geega\' before each command to make Geega do it\n\n- Click on \'Turn on Conversation Mode\' to enter Conversation Mode to make Geega answer your questions!\n\nNote: Geega would not execute command when running in Conversation mode. To bring back Geega to normal mode, just speak \'Back\'.")

#Function for About Geega
def About_Geega():
    copyright_symbol = u"\u00A9"
    About_Geega = tmsg.showinfo("About Geega", u"Geega v1.0.0\n\nReleased on May 16, 2020\n\n%s All rights reserved" % (copyright_symbol))


#Function for authors
def Authors():
    Author = tmsg.showinfo("Authors","Designed by Soham Chatterjee")




#Function to make Geega speak
def speak(audio):
    engine.say(audio)
    engine.runAndWait()


#Function to make Geega wish the user
def wishMe():
    hour = int(datetime.datetime.now().hour)
    if hour>=0 and hour<12:
        speak("Good Morning sir!")

    elif hour>=12 and hour<18:
        speak("Good Afternoon sir!")   

    else:
        speak("Good Evening sir!")  

    speak("I am Geega - the personal Voice Assistant. Just speak \"Hello Geega\" to activate me. I am always there for your service!")

#Function to take command from user
def takeCommand():
    r = sr.Recognizer()
    with sr.Microphone(device_index=1) as source:
        r.pause_threshold = 1
        audio = r.listen(source)
        query = ''
    try:
        query = r.recognize_google(audio, language='en-in')
    except sr.UnknownValueError:
        speak("Sorry! Couldn\'t get you!! Please say again")
        return takeCommand().lower()
    except sr.RequestError:
        print("Aw, Snap! Geega is down!! Please restart the program!")
        return takeCommand().lower()
    return query.lower()


#Function to pause when user is doing any work
def pause():
    r = sr.Recognizer()
    with sr.Microphone() as source:
        r.pause_threshold = 1
        audio = r.listen(source)
        query = ''
    try:   
        query = r.recognize_google(audio, language='en-in')

    except sr.UnknownValueError:  
        return pause().lower()
    except sr.RequestError:
        return pause().lower()
    return query


#Function for API connection
def apiai_connc(voice_data):
    CLIENT_ACCESS_TOKEN = "6b496c3a7afe409da201e7e82b3b5215"
    ai = apiai.ApiAI(CLIENT_ACCESS_TOKEN)
    request = ai.text_request()
    request.lang = "de"
    request.session_id = "<SESSION ID, UNIQUE FOR EACH USER>"
    request.query = voice_data
    source = request.getresponse()
    source_data = source.read()
    obj = json.loads(source_data)
    return obj["result"]["fulfillment"]["speech"]


#Main Geega function
def Geega():
    Button_frame3 = Button(Frame3, image=Button_image2, bg='#333333', borderwidth=0, height=80, width=250, cursor='hand2', command=convo_mode, state=DISABLED, activebackground='#333333').place(x=320,y=360)
    Text_frame1['text'] = "Geega is running..."
    main_window.update()
    wishMe()
    while True:
        query = pause().lower()
        if query.count("hello")>0:        
            speak("Hello sir! Geega is ready to be instructed...")
            query = takeCommand().lower()
            if query == '':
                speak("Sorry! I couldn't hear you! Can you please say that again?")
            elif 'youtube' in query:
                speak("Opening YouTube...")
                webbrowser.open("www.youtube.com")
            elif 'google' in query:
                speak("Opening Google...")
                webbrowser.open("www.google.com")
            elif 'meet' in query:
                speak("Opening Google Meet...")
                webbrowser.open("https://meet.google.com")
            elif 'physics class' in query:
                speak("Opening your Physics class on Google Meet...")
                webbrowser.open("https://meet.google.com/inw-ncmr-emq")
            elif 'mathematics' in query:
                speak("Opening your Mathematics class on Google Meet...")
                webbrowser.open("https://meet.google.com/inw-ncmr-emq")
            elif 'chemistry class' in query:
                speak("Opening your Chemistry class on Google Meet...")
                webbrowser.open("https://meet.google.com/inw-ncmr-emq")
            elif 'english class' in query:
                speak("Opening your English class on Google Meet...")
                webbrowser.open("https://meet.google.com/eym-ypwa-kkx")
            elif 'play music' in query:
                music_dir = 'E:\\Bony Laptop\\Music'
                songs = os.listdir(music_dir)
                speak("Playing music...")
                os.startfile(os.path.join(music_dir,songs[0]))
            elif 'day' in query:
                now = datetime.datetime.now()
                day = now.strftime("%A")
                speak(f"Today is {day}")
            elif 'date' in query:
                date = datetime.datetime.now().strftime("%B")
                speak(f"Today's date is {date}")
            elif 'time' in query:
                strTime = datetime.datetime.now().strftime("%I:%M:%S")
                speak(f"The time is {strTime}")
            elif 'browser' in query:
                speak('Sure! Opening Internet Explorer...')
                browser_path = webbrowser.open(url="google.com")
            elif 'open python' in query:
                python_path = "C:\\Users\\admin\\AppData\\Local\\Programs\\Python\\Python38-32\\Lib\\idlelib\\idle.pyw"
                speak("Opening Python...")
                os.startfile(python_path)  
            elif 'calculate' in query:
                query = query
                speak("Calculating...")
                try:
                    res = client.query(query)
                    results = next(res.results).text
                    speak(f"The results are {results}")
                except:
                    speak("Oops! Something went wrong. I couldn't calculate the problem.")
            elif 'exit' in query or "quit" in query:
                speak("Good Bye Sir! Have a nice day!")
                quit()
            elif 'search' in query:
                speak("Searching on the Web...")
                url = 'https://www.google.co.in/search?q='+query
                webbrowser.get().open(url)
            elif 'weather' in query:
                speak("Getting weather forecast results for your location on Google...")
                url = 'https://www.google.co.in/search?q=weather'
                webbrowser.get().open(url)
            elif 'i want to talk to you' in query:
                convo_mode()
            else:
                speak("Could not get you!")



main_window = Tk()

def close(event):
    global main_window
    key = event.char
    if key == "<ESCAPE>":
        main_window.destroy()

_thread.start_new_thread(main_window.bind("<Key>", lambda a: close(a)))

#Window configuration
windowWidth = 500
windowHeight = 300
screen_width = main_window.winfo_screenwidth()
screen_height = main_window.winfo_screenheight()
x_coordinate = (screen_width/2)-(windowWidth/2)
y_coordinate = (screen_height/2)-(windowHeight/2)

#Main Window Configuration
main_window.configure(bg='white')
main_window.iconbitmap('Geega_logo.ico')
main_window.title("Geega")
main_window.geometry('%dx%d+%d+%d' % (windowWidth, windowHeight, x_coordinate, y_coordinate))
main_window.minsize(500,300)
main_window.update()


#Frame 1 for Speak button
Frame1 = Frame(main_window, bg='#444444', borderwidth=5)
Frame1.place(x=0, y=0, width=500, height=300)

#Frame 2 for features
Frame2 =  Frame(main_window, bg='#555555', borderwidth=5)
Frame2.place(x=0,y=300, width=500, height=440)

#Frame 3 for conversation mode
Frame3 =  Frame(main_window, bg='#333333', borderwidth=5)
Frame3.place(x=500,y=0, width=865, height=740)

#Defining the font
Font = font.Font(family='Segoe UI', size=15, weight='bold')
Font2 = font.Font(family='Segoe UI', size=15)
Font3 = font.Font(family='Segoe UI', size=30)

#Defining the menu bar
MenuBar= Menu(main_window)
Drop_down1 = Menu(MenuBar, tearoff=0)
Drop_down1.add_command(label="Instructions", command=Instructions)
Drop_down1.add_separator()
Drop_down1.add_command(label='About Geega', command=About_Geega)
Drop_down1.add_command(label='Authors', command=Authors)

#Adding the menu bar
main_window.config(menu=MenuBar)
MenuBar.add_cascade(label="Help", menu=Drop_down1)

#Frame 1 button configuration
Button_image = PhotoImage(file="rsz_1speak_button_png.png")
Button_frame1 = Button(Frame1, image=Button_image, bg='#444444', borderwidth=0, height=200, width=200, cursor='hand2', command=Geega, activebackground='#444444').place(x=5,y=15)

#Frame 1 text configuration:
Text_frame1 = Label(Frame1, text="Click the mic to activate Geega...", bg='#444444', fg='white')
Text_frame1.place(x=43,y=225)
Text_frame1['font'] = Font

#Frame 2 text configuration
Text1_frame2 = Label(Frame2, text='Exciting things to do with Geega!', bg='#555555',fg='white')
Text1_frame2.place(x=43, y=15)
Text1_frame2['font'] = Font2

Text2_frame2 = Label(Frame2, text='- Tell her to open Google for you', bg='#555555',fg='white')
Text2_frame2.place(x=50, y=50)
Text2_frame2['font'] = Font2

Text3_frame2 = Label(Frame2, text='- Tell her to open Chrome for you', bg='#555555',fg='white')
Text3_frame2.place(x=50, y=85)
Text3_frame2['font'] = Font2

Text4_frame2 = Label(Frame2, text='- Bored? Ask Geega to play some music', bg='#555555',fg='white')
Text4_frame2.place(x=50, y=120)
Text4_frame2['font'] = Font2

Text5_frame2 = Label(Frame2, text='for you', bg='#555555',fg='white')
Text5_frame2.place(x=63, y=155)
Text5_frame2['font'] = Font2

Text4_frame2 = Label(Frame2, text='- Try out conversing with Geega in the', bg='#555555',fg='white')
Text4_frame2.place(x=50, y=185)
Text4_frame2['font'] = Font2

Text5_frame2 = Label(Frame2, text='Conversation mode', bg='#555555',fg='white')
Text5_frame2.place(x=63, y=220)
Text5_frame2['font'] = Font2

Text6_frame2 = Label(Frame2, text='Check out the other features too. Interact', bg='#555555',fg='white')
Text6_frame2.place(x=43, y=255)
Text6_frame2['font'] = Font2

Text6_frame2 = Label(Frame2, text='with Geega to find more...', bg='#555555',fg='white')
Text6_frame2.place(x=43, y=290)
Text6_frame2['font'] = Font2

Text7_frame2 = Label(Frame2, text='More new features coming soon...', bg='#555555',fg='white')
Text7_frame2.place(x=43, y=325)
Text7_frame2['font'] = Font


#Frame 3 button configuration
Button_image2 = PhotoImage(file="Convo button.png")
Button_frame3 = Button(Frame3, image=Button_image2, bg='#333333', borderwidth=0, height=80, width=250, cursor='hand2', command=convo_mode, state=NORMAL, activebackground='#333333').place(x=320,y=360)

#Frame 3 text configuration
Text_frame3 = Label(Frame3, text="Check out the all new conversation mode!", bg='#333333', fg='white')
Text_frame3.place(x=255,y=300)
Text_frame3['font'] = Font2

main_window.mainloop()

Solution

  • You are correct in thinking of threading here.

    The standard threading library is sufficient for this. You will want to create two classes, one for your GUI and one for your other processes. You could do something like the following for threading your function:

    import threading
    

    ...

    class BackgroundProcess(threading.Thread):
    
    def __init__(self, parent=None):
        super().__init__()
        self.parent = parent
    
    def run(self):
        Geega()
    

    and instead of just calling Geega() from your UI, create a new BackgroundProcess object and call its start() function as follows:

    process = BackgroundProcess()
    process.start()
    

    Hope this helps!

    Edit: Forgot to mention that seeing as this process is infinite, you should clean up the thread after closing your GUI to prevent memory leaks. Create an event listener for your window closing that kills all processes, something like

    os._exit(1)