How to efficiently use Schedule Module in tkinter without getting GUI Freeze - python-3.x

I am new to python and tkinter and I made very basic program which check IP address ping or reachability in a given time range. I used Schedule module to schedule ping but actually after clicking Start Task, the GUI freeze while the code keeps running in the background. Probably the while loop is causing the freeze and I failed to solve this issue even after reviewing all stackoverflow mentioned solutions since no one addressed how to flawlessly use Schedule module in tkinter.
I wonder if there is a work-around to implement Schedule module without using while loop or with while loop without causing the GUI to freeze.
Thank you very much.
I made this fast sample for my current case.
Using Python 3.8.5
Target OS: Windows10 pro
Tested on VS Code
import tkinter as tk
import tkinter.font as tkFont
from pythonping import ping
from tkinter import *
from win10toast import ToastNotifier
import schedule
class App:
def __init__(self, root):
root.title("Ping Check")
width=600
height=400
screenwidth = root.winfo_screenwidth()
screenheight = root.winfo_screenheight()
alignstr = '%dx%d+%d+%d' % (width, height, (screenwidth - width) / 2, (screenheight - height) / 2)
root.geometry(alignstr)
root.resizable(width=False, height=False)
self.ip_address = StringVar()
self.seconds = IntVar()
self.n = ToastNotifier()
IP_Address=tk.Entry(root)
IP_Address["borderwidth"] = "1px"
ft = tkFont.Font(family='Times',size=10)
IP_Address["font"] = ft
IP_Address["fg"] = "#333333"
IP_Address["justify"] = "center"
IP_Address["textvariable"] = self.ip_address
IP_Address.place(x=250,y=70,width=270,height=32)
ip_address_label=tk.Label(root)
ft = tkFont.Font(family='Times',size=10)
ip_address_label["font"] = ft
ip_address_label["fg"] = "#333333"
ip_address_label["justify"] = "center"
ip_address_label["text"] = "Enter IP Address"
ip_address_label.place(x=60,y=70,width=139,height=30)
seconds_label=tk.Label(root)
ft = tkFont.Font(family='Times',size=10)
seconds_label["font"] = ft
seconds_label["fg"] = "#333333"
seconds_label["justify"] = "center"
seconds_label["text"] = "Enter Seconds"
seconds_label.place(x=50,y=170,width=143,height=30)
Seconds=tk.Entry(root)
Seconds["borderwidth"] = "1px"
ft = tkFont.Font(family='Times',size=10)
Seconds["font"] = ft
Seconds["fg"] = "#333333"
Seconds["justify"] = "center"
Seconds["textvariable"] = self.seconds
Seconds.place(x=250,y=170,width=272,height=30)
start_button=tk.Button(root)
start_button["bg"] = "#efefef"
ft = tkFont.Font(family='Times',size=10)
start_button["font"] = ft
start_button["fg"] = "#000000"
start_button["justify"] = "center"
start_button["text"] = "Start Task"
start_button.place(x=100,y=310,width=178,height=30)
start_button["command"] = self.Start_Task
stop_button=tk.Button(root)
stop_button["bg"] = "#efefef"
ft = tkFont.Font(family='Times',size=10)
stop_button["font"] = ft
stop_button["fg"] = "#000000"
stop_button["justify"] = "center"
stop_button["text"] = "Stop Task"
stop_button.place(x=330,y=310,width=172,height=30)
stop_button["command"] = self.Stop_Task
def ping_ip(self):
l = list(ping(self.ip_address.get()))
if not str(l[0]).startswith('Reply'):
self.n.show_toast("Warning!", "Unreachable IP Address, Error Ping Message: Request timed out!")
else:
self.n.show_toast("Successful reply!")
def Start_Task(self):
schedule.every(self.seconds.get()).seconds.do(self.ping_ip)
while True:
schedule.run_pending()
def Stop_Task(self):
schedule.cancel_job(self.Start_Task)
if __name__ == "__main__":
root = tk.Tk()
app = App(root)
root.mainloop()

GUI toolkits like tkinter are event-driven. To work properly, the
mainloop must continuously be able to process keyboard and mouse events.
When it is not processing events, it will start scheduled idle tasks.
So tkinter programs work quite differently from normal Python scripts.
A callback is called in response to activating a control (like clicking on
a button).
An idle task is started by the system after a specified number of milliseconds
when the system is not busy processing events. You can schedule idle tasks
with the Tk.after() method.
Basically, the callbacks and idle tasks are your program.
But they are run from within the main loop.
So whatever you do in a call-back, it should not take too long. Otherwise the GUI becomes unresponsive.
So using while True in a call-back is not a good idea.
In a Python 3 tkinter program, when you want to perform a long-running task especially if it involves disk access or network activity, you should probably do that in a second thread. (In Python 3 tkinter is mostly thread-safe.)
Edit 1:
Instead of using pythonping and schedule, use subprocess.Popen to run the ping program asyncronously:
import subprocess as sp
# class App:
# et cetera...
def query_task(self):
if self.process:
if self.process.returncode:
# The ping has finished
if self.process.returncode != 0:
self.n.show_toast("Warning:", f"ping returned {self.process.returncode}")
else: # ping has finished successfully.
# self.process.stdout and self.process.stderr contain the output of the ping process...
pass
# So a new task can be started.
self.process = None
else:
# check again after 0,5 seconds.
self.after(0.5, self.query_task);
# By convention, method names should be lower case.
def start_task(self):
if self.process is None:
self.process = sp.Popen(
["ping", self.ip_address.get()], stdout=sp.Pipe, stderr=sp.Pipe
)
# Check after 0.5 seconds if the task has finished.
self.after(0.5, self.query_task);

Related

Slow while loop in tkinter after reboot

i'm trying to add some windows on the top of an existing vb6 app.
`
from threading import Thread
from tkinter import *
import pyautogui
import win32gui
import time
#coordinates of the target image that i use to decide if i have to show the tk windows
target=(83,396)
target2=(612,73)
targetp=(153,204,0)
target2p=(247,87,70)
#windows geometry
wg1=(221,229,795,160)
wg2=(154,162,11,482)
f = True
#names of the vb program's windows that have to cover the left window
ws = ["Parametri di lavorazione", "Opzioni", "Configurazione tabelle DXF"]
#windows thread
def wins():
global win10
global win20
win10 = Tk()
win10.overrideredirect(True)
win10.geometry('%dx%d+%d+%d' % wg1)
img1 = PhotoImage(file = "c:\\users\\utente\\desktop\\img1.png")
l1= Label(win10, i=img1)
l1.pack()
win20 = Toplevel(win10)
img2 = PhotoImage(file = "c:\\users\\utente\\desktop\\img2.png")
l2 = Label(win20, i=img2)
l2.pack()
win20.geometry('%dx%d+%d+%d' % wg2)
win20.overrideredirect(True)
win10.mainloop()
#thread to check if the windows have to be showed based on the first target
def cl():
global win10
global win20
s = True
while True:
if s:
if pyautogui.pixel(target[0],target[1]) != targetp:
win10.withdraw()
win20.withdraw()
s = False
else:
if pyautogui.pixel(target[0],target[1]) == targetp:
win10.deiconify()
win20.deiconify()
s = True
#thread to handle other windows on the original app
def rd():
ws2 = []
while True:
for i in ws:
ws2.append(win32gui.FindWindow(0,i))
for i in ws2:
if i:
a,b,c,d= win32gui.GetWindowRect(i)
win32gui.MoveWindow(i,1024-(c-a-8),-8,c-a,d-b,True)
while True:
if pyautogui.pixel(target2[0],target2[1]) != target2p:
win10.withdraw()
break
while True:
if pyautogui.pixel(target2[0],target2[1]) == target2p:
win10.deiconify()
break
ws2=[]
t1 = Thread(target=wins)
t2 = Thread(target=cl)
t3 = Thread(target=rd)
t1.start()
time.sleep(1)
t2.start()
t3.start()
#while loop to make my windows always on top
while True:
win10.attributes("-topmost",True)
win20.attributes("-topmost",True)
`
The old app is set to be always on top, so i had to add a while loop at the end of the code that sets the two new windows topmost.
If i don't do this, my windows disappear as soon as i click on the old program's main window.
The code runs fine on my windows 10 machine until i reboot the operating system.
After the reboot, the last while loop become very slow, so that even the operation of minimizing the main window takes about 5 seconds.
Do you have any idea on how to fix this?
I also tried to insert the while loop in a thread letting the windows mainloop outside any method, but same result.
Until the reboot it all goes fine, after i power on the machine again in becomes very slow.

TKinter problem with getting return values out of Entry from GUI Designer

First off: I am an absolute noob. I am currently trying to write a simple GUI with tkinter and I fail horribly. Most of the code below is written by an GUI designer and not by me. Maybe I would´ve been much faster if I did it myself. Most tutorials go other ways or go with Pyhton 2, so I just don´t know how to fix this damn thing.
What the program is supposed to do: This thing is meant to become a GUI for a ... Batch Script. This script takes a Source and a Target Path, filters certain File Extensions and copies them from A to B while leaving anything else alone. This GUI is supposed to provide the Data to it. Enter Source, enter target press start and the batch script starts to party hard.
The problem I have is this: My Python GUI isn´t able to even echo the variables after the button is pressed. I exactly know where the problem is but after many tries I just can´t figure out how to manage it. It is the GiveTarget.get() and GiveSource.get() along with the StartButton_command function. (Commented it out)
import tkinter as tk
import tkinter.font as tkFont
import subprocess
import os
class App:
def __init__(self, root):
GiveSource=tk.Entry(root)
GiveSource["borderwidth"] = "1px"
ft = tkFont.Font(family='Times',size=10)
GiveSource["font"] = ft
GiveSource["fg"] = "#333333"
GiveSource["justify"] = "left"
GiveSource.place(x=110,y=50,width=328,height=30)
global x
x = GiveSource.get() #####This is wrong!#####
GiveTarget=tk.Entry(root)
GiveTarget["borderwidth"] = "1px"
ft = tkFont.Font(family='Times',size=10)
GiveTarget["font"] = ft
GiveTarget["fg"] = "#333333"
GiveTarget["justify"] = "left"
GiveTarget.place(x=110,y=120,width=327,height=30)
global y
y = GiveTarget.get() #####This is also wrong!#####
StartButton=tk.Button(root)
StartButton["activebackground"] = "#90ee90"
StartButton["activeforeground"] = "#ffffff"
StartButton["bg"] = "#5fb878"
ft = tkFont.Font(family='Times',size=10)
StartButton["font"] = ft
StartButton["fg"] = "#000000"
StartButton["justify"] = "center"
StartButton["text"] = "Start"
StartButton.place(x=110,y=180,width=323,height=44)
StartButton["command"] = self.StartButton_command
def StartButton_command(self): #"Start" Button
print("Start")
print ("Source=" + x)
print ("Target=" + y)
print ("called HDD Copy")
#subprocess.Popen([r'HDD_Kopierer.bat'])
if __name__ == "__main__":
root = tk.Tk()
app = App(root)
root.mainloop()
Make the widgets available throughout the App class by using self when you create them.
import tkinter as tk
import tkinter.font as tkFont
import subprocess
import os
class App:
def __init__(self, root):
self.GiveSource=tk.Entry(root)
self.GiveSource["borderwidth"] = "1px"
ft = tkFont.Font(family='Times',size=10)
self.GiveSource["font"] = ft
self.GiveSource["fg"] = "#333333"
self.GiveSource["justify"] = "left"
self.GiveSource.place(x=110,y=50,width=328,height=30)
self.GiveTarget=tk.Entry(root)
self.GiveTarget["borderwidth"] = "1px"
ft = tkFont.Font(family='Times',size=10)
self.GiveTarget["font"] = ft
self.GiveTarget["fg"] = "#333333"
self.GiveTarget["justify"] = "left"
self.GiveTarget.place(x=110,y=120,width=327,height=30)
StartButton=tk.Button(root)
StartButton["activebackground"] = "#90ee90"
StartButton["activeforeground"] = "#ffffff"
StartButton["bg"] = "#5fb878"
ft = tkFont.Font(family='Times',size=10)
StartButton["font"] = ft
StartButton["fg"] = "#000000"
StartButton["justify"] = "center"
StartButton["text"] = "Start"
StartButton.place(x=110,y=180,width=323,height=44)
StartButton["command"] = self.StartButton_command
def StartButton_command(self): #"Start" Button
print("Start")
print ("Source=" + self.GiveSource.get())
print ("Target=" + self.GiveTarget.get())
print ("called HDD Copy")
#subprocess.Popen([r'HDD_Kopierer.bat'])
if __name__ == "__main__":
root = tk.Tk()
app = App(root)
root.mainloop()

Separating Tkinters GUI and control of the application

1) What is my goal:
I’m creating an application that should read data every 60s from ModBusServer, append those data to Graphs and then when the app is closed save the data to excel file.
Site note:
The process of reading data from ModBusServer and appending them to graphs should start after a start button is pressed.
And end after stop button is pressed OR when ModBusServer sends a request to stop.
2) What I have so far:
I created the GUI without any major problems as a class “GUI_komora”.
Everything there works just fine.
3) What is the problem:
But now I’m lost on how to approach the “read data every 60 seconds”, and overall how to control the application.
I did some research on threading but still I’m confused how to implement this to my application.
I learned how to make functions run simultaneously in this tutorial.
And also how to call a function every few seconds using this question.
But none of them helped me to learn how to control the overall flow of the application.
If you could redirect me somewhere or tell me about a better approach I would be really glad.
Some of my code:
from tkinter import *
from GUI_komora import GUI
root = Tk()
my_gui = GUI(root) #my GUI class instance
#main loop
root.mainloop()
"""
How do I achieve something like this???
whenToEnd = False
while whenToEnd:
if step == "Inicialzation":
#inicializace the app
if step == "ReadData":
#read data every 60 seconds and append them to graphs
if step == "EndApp"
#save data to excel file and exit app
whenToEnd = True
"""
Here is an example of a loop that takes a decision (every 60 sec in your case) and pushes the outcome of the decision to tkinter GUI: https://github.com/shorisrip/PixelescoPy/blob/master/base.py
Parts:
main thread - starts tkinter window
control thread - reads some data and decides what to show in GUI
GUI class - has a method "add_image" which takes input an image and displays on GUI.(add_data_to_graph maybe in your case). This method is called everytime by the control thread.
Snippets:
def worker(guiObj, thread_dict):
# read some data and make decision here
time.sleep(60)
data_to_show = <outcome of the decision>
while some_logic:
pictureObj = Picture(chosen, timer)
pictureObj.set_control_thread_obj(thread_dict["control_thread"])
guiObj.set_picture_obj(pictureObj)
pictureObj.display(guiObj)
# do post display tasks
guiObj.quit_window()
# Keep GUI on main thread and everything else on thread
guiObj = GuiWindow()
thread_list = []
thread_dict = {}
thread_for_image_control = threading.Thread(target=worker, args=(guiObj,
thread_dict))
thread_dict["control_thread"] = thread_for_image_control
thread_list.append(thread_for_image_control)
thread_for_image_control.start()
guiObj.run_window_on_loop()
# thread_for_image_control.join()
Code for Picture class:
class Picture:
def __init__(self, path, timer):
self.path = path
self.timer = timer
self.control_thread_obj = None
def set_control_thread_obj(self, thread_obj):
self.control_thread_obj = thread_obj
def display(self, guiObj):
image_path = self.path
guiObj.add_image(image_path)
time.sleep(self.timer)
Code for GUI class
class GuiWindow():
def __init__(self):
self.picture_obj = None
self.root = Tk()
self.image_label = None
self.image = None
self.folder_path = None
self.timer = None
self.root.protocol("WM_DELETE_WINDOW", self.exit_button)
def add_image(self, image_path):
resized_img = self.resize(image_path)
image_obj = ImageTk.PhotoImage(resized_img)
image_label = Label(self.root, image=image_obj,
height=resized_img.height,
width=resized_img.width)
self.image = image_obj # DO NOT REMOVE - Garbage collector error
if self.image_label is not None:
self.remove_image()
image_label.grid(row=0, column=0, columnspan=3)
self.image_label = image_label
Here based on my control loop thread I am changing image (in your case graph data) of the tkinter GUI.
Does this help?

RaspberryPi Stopwatch (TKinter) with Trigger

i would like to build a stopwatch for my carrera track which i built a little bit bigger.
So I have bought a Raspberry Pi 3 with an additional 7 "touch screen and the individual modules for triggering.
Everything works fine individually.
Now I found a great stopwatch on the net which I also used. Works really great.
In another script, that I've written by myself, the triggering with the gpios works also fantastic.
Now I want to combine both and fail.
Does anyone have an idea or suggested solution where my mistake is?
Here is my code
#!/usr/bin/python
import tkinter as tk
import RPi.GPIO as GPIO
import time
GPIO_TRIGGER_PIN = 17
GPIO.setmode(GPIO.BCM)
GPIO.setup(GPIO_TRIGGER_PIN, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setwarnings(False)
def update_timeText():
if (state):
global timer
timer[2] += 1
if (timer[2] >= 100):
timer[2] = 0
timer[1] += 1
if (timer[1] >= 60):
timer[0] += 1
timer[1] = 0
timeString = pattern.format(timer[0], timer[1], timer[2])
timeText.configure(text=timeString)
root.after(10, update_timeText)
def start():
global state
state = True
print('Pressed Start')
def stop():
global state
state = False
print('Pressed Stop')
def reset():
global timer
timer = [0, 0, 0]
timeText.configure(text='00:00:00')
print('Pressed Reset')
while GPIO.input(GPIO_TRIGGER_PIN) == True:
if GPIO.input(GPIO_TRIGGER_PIN):
print('CAR DETECTED')
time.sleep(0.1)
state = False
# BULDING TKinter GUI
root = tk.Tk()
root.wm_title('Stopwatch')
timer = [0, 0, 0]
pattern = '{0:02d}:{1:02d}:{2:02d}'
timeText = tk.Label(root, text="00:00:00", font=("Helvetica", 150))
timeText.pack()
startButton = tk.Button(root, text='Start', command=start)
startButton.pack()
stopButton = tk.Button(root, text='Stop', command=stop)
stopButton.pack()
resetButton = tk.Button(root, text='Reset', command=reset)
resetButton.pack()
update_timeText()
root.mainloop()
Currently I get the trigger in the console as output "CAR DETECTED". However, I do not get the TKinter panel.
If I remove
while GPIO.input(GPIO_TRIGGER_PIN) == True:
if GPIO.input(GPIO_TRIGGER_PIN):
print('CAR DETECTED')
time.sleep(0.1)
then the display appears and works. Without triggering.
If I put it all down, I get also the panel but it also triggers nothing more.
Any ideas ?
Thanks for help
Not really my area of expertise, but since nobody else answered, I'll give it a shot.
I think you want to use GPIO.add_event_callback()* to add a callback that gets called when an event happens.
# Callback function
def on_trigger(channel_number):
# Just update the flag
global state
state = False
# Set the callback
GPIO.add_event_callback(GPIO_TRIGGER_PIN , callback=on_trigger)
Then, delete the while loop.
*According to the docs, you need the darksidesync library to use this function. Instead of the callback, you could also create a separate thread. Personally, I would prefer the callback.
from threading import Thread
def thread_function(arg):
global state
# Listen for state change
while GPIO.input(GPIO_TRIGGER_PIN) == True:
if GPIO.input(GPIO_TRIGGER_PIN):
# Update var
state = False
print('CAR DETECTED')
time.sleep(0.1)
thread = Thread(target = thread_function)
thread.start()
Hope this helps.

Windows program not responding issue with tkinter gui

I'm having an issue with a program I created, Im trying to make a program that images a drive usind dism. When the dism command is run in cmd it gives a percentage of completion, so I am trying to take that percentage (using regex) and put it in my own progress bar gui however when I run my program, windows gives me a program is not responding error and I'm not sure why. The program continues to to run in the background and upon completion the error disappears, but the progress bar does not update. I have a feeling it is because I my run function has its own loop so maybe the tkinter gui isn't updating until that loop completes. If someone can confirm this or give me other reasoning that would be greatly appreciated. Also if you have any ideas for a solution to this I am all ears.
import shlex
from tkinter import *
from tkinter import ttk
def run(command):
progressBar['maximum'] = 100
process = subprocess.Popen(shlex.split(command), stdin= subprocess.PIPE
,stdout=subprocess.PIPE)
while True:
output = process.stdout.readline().decode()
if output == '' and process.poll() is not None:
break
if output:
matchObj = re.search(r'\d+\.\d%',output.strip())
if matchObj:
percentNum = re.search(r'\d+\.\d',matchObj.group())
progressBar["value"] =
round(float(percentNum.__getitem__(0)))
print(type(progressBar["value"]))
print(progressBar["value"])
else:
print(output.strip())
rc = process.poll()
progressBar["value"] = 0
return rc
root = Tk()
root.title('Progress Bar')
root.geometry("300x100")
buttonFrame = LabelFrame(text="Options")
buttonFrame.grid(column=0,row=0)
backupCmd = 'Dism /Capture-Image /ImageFile:F:\my-windows-partition.wim
/CaptureDir:"E:" /name:Windows \n'
button1 = Button(master=buttonFrame, text="Backup",command= lambda:
run(backupCmd))
button1.grid(column = 0, row = 0)
restoreCmd = ''
button2 =Button(master=buttonFrame, text="Restore",command= lambda:
run(restoreCmd))
button2.grid(column = 50, row = 0)
button3 =Button(master=buttonFrame, text="Exit",command= lambda: exit())
button3.grid(column = 100, row = 0)
progressBar = ttk.Progressbar(root, orient="horizontal",
length=286,mode="determinate")
progressBar.grid(column = 0, row = 3, pady=10)
root.mainloop()
I figured out the issue. For anyone struggling with this, you need to add a call to update the progress bar. For me I said, progressBar.update in my run methon
I was stuck with this issue for a long time until I found an answer on stackoverflow about using multi-threading.
Tkinter window says (not responding) but code is running You to create a thread for sql queries function, like this
from threading import Thread
Thread(target = run, args =(command, )). Start()

Resources