Terminal is busy after TKinter window is closed - python

I have written a python script that draws a GUI containing Graphs plotting various metrics such as CPU temperature, Ram usage etc. for my Rasberry pi v4 . The source code of the script is outlined below:
`
#!/usr/bin/env python3
#Import required modules
import tkinter as tk
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import time
from gpiozero import CPUTemperature
import os
import psutil
import sys
#Create application GUI
Main_Window=tk.Tk()
Main_Window.title("Monitors v1.0")
window_width = 800
window_height = 400
Main_Window.resizable(False,False)
Main_Window.geometry(f'{window_width}x{window_height}')
#Set up the layout of the graphs .There will be 4 graphs plotting:
#CPU Temperature in Celsius,RAM,CPU and Disk usage usage in %
fig,(axs)=plt.subplots(2,2,constrained_layout=True,figsize=(8, 4))
canvas=FigureCanvasTkAgg(fig, Main_Window)
canvas.get_tk_widget().place(x=0,y=0)
fig.suptitle('Performance Metrics')
axs[0,0].set_ylabel('CPU Temperature(C)')
axs[0,0].set_xlabel('Time(Sec)')
axs[0,1].set_ylabel('RAM Usage(%)')
axs[0,1].set_xlabel('Time(Sec)')
axs[1,0].set_ylabel('CPU Usage(%)')
axs[1,0].set_xlabel('Time(%)')
axs[1,1].set_ylabel('Disk Usage(%)')
axs[1,1].set_xlabel('Time(%)')
#Initialize data buffers and plots
temp=[]
RAM=[]
CPU=[]
Disk=[]
axs[0,0].plot(temp)
axs[0,1].plot(RAM)
axs[1,0].plot(CPU)
axs[1,1].plot(Disk)
#Run the script for 50 samples
for _ in range(50):
#Sampling the metrics.
temp.append(CPUTemperature().temperature)
RAM.append(psutil.virtual_memory()[2])
load1, load5, load15 = psutil.getloadavg()
cpu_usage = (load15/os.cpu_count()) * 100
CPU.append(cpu_usage)
Disk.append(psutil.disk_usage('/').percent)
#Update the Plots every 200 msec
time.sleep(0.2)
canvas.draw()
axs[0,0].clear()
axs[0,1].clear()
axs[1,0].clear()
axs[1,1].clear()
axs[0,0].set_ylabel('CPU Temperature(C)')
axs[0,0].set_xlabel('Time(Sec)')
axs[0,1].set_ylabel('RAM Usage(%)')
axs[0,1].set_xlabel('Time(Sec)')
axs[1,0].set_ylabel('CPU Usage(%)')
axs[1,0].set_xlabel('Time(%)')
axs[1,1].set_ylabel('Disk Usage(%)')
axs[1,1].set_xlabel('Time(sec)')
axs[0,0].plot(temp)
axs[0,1].plot(RAM)
axs[1,0].plot(CPU)
axs[1,1].plot(Disk)
Main_Window.mainloop()
sys.exit()
I have also created a bash command so that I can run the script with a simple command via the terminal. The script runs as supposed to be plotting 50 measurements of the metrics described before in real time but there are two issues I am facing. First, despite clicking the close button(the classic X button on the top right corner) the window wont close until the 50 measurements are done. Then if I close the window after the measurements are done, then the terminal looks busy as if the script is still running. Any ideas why these things happen?
I tried adding a sys.exit() command after mainloop() to make sure the script exits after the main window is closed but it did not help.

When you enter your for-loop, your program runs all 50 iteration of that code block until it exits and runs code below. This is toxic for different reasons. Overall, your mainloop is interrupted, events such as mouse or key input won't be processed. This includes clicking X, the window simply does not proceed this message.
However, Tkinter provides tools for such operations. You could use after for it:
Example:
def my_function():
#useful instructions
for i in range(50):
ms = 20*i
root.after(ms, my_function)
After returns immediately and won't block your application, means still responsive to input. But since you still want to sleep in the block of code, you could use something like tksleep.

Related

Using pyautogui to avoid windows session lock

I created a short script using pyautogui module with the purpose of avoid a window 10 session to lock due to user but inactivity. But it's not working.
The script is moving the cursor a little each 120 sec.
Any idea why it's not working? Sometime ago I found on web a small program that was doing exactly the same thing and worked.
Let me just clarify that the script works (moving the mouse) but the windows 10 os locks anyway.
import pyautogui
import time
import win32gui, win32con
import os
Minimize = win32gui.GetForegroundWindow() #minimize current window
win32gui.ShowWindow(Minimize, win32con.SW_MINIMIZE) #minimize current window
x = 1
print("Application running...")
while x == 1:
pyautogui.moveRel(1) # move mouse 1 pixels down
time.sleep (120)
#end of script

Scrolling Progress Bar in Tkinter

I have been trying to set up a progress bar in a python tkinter gui that shows that a process is running. The process is long and I have no way to really measure the progress, so I need to use an indeterminate progress bar. However, I really dislike the style of the ttk indeterminate progress bar that bounces back and forth. I want one that scrolls across the bar over and over again, kind of like this image
Is this possible with tkinter?
have you tried ttk's determinate Progressbar? You can make the progress just continuously scroll across the bar.
for example:
#!/usr/bin/env python3
import tkinter
import tkinter.ttk as ttk
root = tkinter.Tk()
frame = ttk.Frame()
pb = ttk.Progressbar(frame, length=300, mode='determinate')
frame.pack()
pb.pack()
pb.start(25)
root.mainloop()
I know its an old question, but I have found a way to do this for anyone else writing tkinter.
I've been working on a tkinter app for a bit now and have determined that to handle tkinter objects, you absolutely need a separate thread. Although it is apparently frowned upon to handle tkinter objects via something else than the mainloop() method, it has been working well for me. I've never had a main thread is not in main loop error and never experienced objects that didn't update correctly.
I edited Corey Goldberg's code a bit and got it working. Here's what I got (some explanations in the comments).
import tkinter
import tkinter.ttk as ttk
import threading
def mainProgram(): # secure the main program initialization in its own def
root = tkinter.Tk()
frame = ttk.Frame()
# You need to use indeterminate mode to achieve this
pb = ttk.Progressbar(frame, length=300, mode='indeterminate')
frame.pack()
pb.pack()
# Create a thread for monitoring loading bar
# Note the passing of the loading bar as an argument
barThread = threading.Thread(target=keepLooping, args=(pb,))
# set thread as daemon (thread will die if parent is killed)
barThread.daemon=True
# Start thread, could also use root.after(50, barThread.start()) if desired
barThread.start()
pb.start(25)
root.mainloop()
def keepLooping(bar):
# Runs thread continuously (till parent dies due to daemon or is killed manually)
while 1:
"""
Here's the tricky part.
The loading bar's position (for any length) is between 0 and 100.
Its position is calculated as position = value % 100.
Resetting bar['value'] to 0 causes it to return to position 0,
but naturally the bar would keep incrementing forever till it dies.
It works, but is a bit unnatural.
"""
if bar['value']==100:
bar.config(value=0) # could also set it as bar['value']=0
if __name__=='__main__':
mainProgram()
I've added if __name__=='__main__': because I feel it defines the scope a bit better.
As a side note I've found that running threads with while 1: will crank my CPU at about 20-30% usage for that one thread in particular. It's easily solvable by importing time and using time.sleep(0.05) thereafter significantly lowering the CPU usage.
Tested on Win8.1, Python 3.5.0.

Tkinter / pylab conflict?

I'm very new to Python but have started writing a few small scripts this week. I'm currently trying to write a simple program to plot some data. I'd like to do the following:
ask the user to choose the data directory using a GUI
for each file in the directory, make a plot
close each plot with a mouse click and advance to the next plot
I've mostly gotten the program to work - I can choose the directory using tkFileDialog.askdirectory, then read in the data, make the plots and advance though them using a mouse click.
My problem is with the TK root window that opens with the tkFileDialog. If I use withdraw() the extra window doesn't open, but only the first plot will appear (a mouse click closes that plot but doesn't show the next one). If I don't use withdraw(), the extra window must be manually closed after the first plot to advance to the second.
I'm wondering if there is a way to choose the directory that will avoid displaying the extra window?
I'm attaching some sample code to show my thought process. This doesn't call the actual data but still reproduces the problem (you'll need to change the .D to some file type that you have in a directory):
import numpy as np
from pylab import *
import glob
import os
import Tkinter, tkFileDialog
##################################################
#define the mouse click event
##################################################
def moveon(event):
close()
##################################################
#ask for the directory
##################################################
root = Tkinter.Tk()
#root.withdraw()
direc = tkFileDialog.askdirectory(parent=root,initialdir="/",title='Please select a directory')
os.chdir(direc)
for files in glob.glob("*.D*"):
##################################################
#Read in the data
##################################################
#assume this reads x and y from each file
x = [1, 2]
y = [3, 4]
##################################################
#loop though the plots
##################################################
fig = figure(1)
plot(x,y)
cid = fig.canvas.mpl_connect('button_press_event',moveon)
show()
Since you don't seem to be using Tkinter after your file dialog, you could do root.destroy()
to close the Tk root window right after you have the user select a file.

Detach matplotlib window from sub-process

I've got a script which creates a graph, but the script keeps running in the background until the window is closed. I'd like it to quit as soon as the window is created, so that Ctrl-C in the shell won't kill the window, and so that the user can leave the window open and continue working in the shell without bg-ing it manually. I've seen some solutions with daemons, but I'd like to avoid splitting this into two scripts. Is multiprocessing the easiest solution, or is there something shorter?
The relevant show() command is the last thing that is executed by the script, so I don't need to keep a reference to the window in any way.
Edit: I don't want to save the figure as a file, I want to be able to use the interactive window. Essentially the same as running mian ... & in bash
Just discovered this argument in plt.show(). setting block=False will pop up the figure window, continue following code, and keep you in the interpreter when the script has finished (if you are running in interactive mode -i).
plt.show(block=False)
This works for Unix:
import pylab
import numpy as np
import multiprocessing as mp
import os
def display():
os.setsid()
pylab.show()
mu, sigma = 2, 0.5
v = np.random.normal(mu,sigma,10000)
(n, bins) = np.histogram(v, bins=50, normed=True)
pylab.plot(bins[:-1], n)
p=mp.Process(target=display)
p.start()
When you run this script (from a terminal) the pylab plot is displayed. Pressing Ctrl-C kills the main script, but the plot remains.
I would suggest using os.fork() as the simplest solution. It is the trick that is used in daemons, but it doesn't require two scripts, and it's quite simple. For example:
import os
proc_num = os.fork()
if proc_num != 0:
#This is the parent process, that should quit immediately to return to the
#shell.
print "You can kill the graph with the command \"kill %d\"." % proc_num
import sys
sys.exit()
#If we've made it to here, we're the child process, that doesn't have to quit.
import matplotlib.pyplot as plt
plt.plot([1,2,3],[4,5,6])
plt.show()

Label in PyQt4 GUI not updating with every loop of FOR loop

I'm having a problem, where I wish to run several command line functions from a python program using a GUI. I don't know if my problem is specific to PyQt4 or if it has to do with my bad use of python code.
What I wish to do is have a label on my GUI change its text value to inform the user which command is being executed. My problem however, arises when I run several commands using a for loop. I would like the label to update itself with every loop, however, the program is not updating the GUI label with every loop, instead, it only updates itself once the entire for loop is completed, and displays only the last command that was executed.
I am using PyQt4 for my GUI environment. And I have established that the text variable for the label is indeed being updated with every loop, but, it is not actually showing up visually in the GUI.
Is there a way for me to force the label to update itself? I have tried the update() and repaint() methods within the loop, but they don't make any difference.
I would really appreciate any help.
Thank you.
Ronny.
Here is the code I am using:
# -*- coding: utf-8 -*-
import sys, os
from PyQt4 import QtGui, QtCore
Gui = QtGui
Core = QtCore
# ================================================== CREATE WINDOW OBJECT CLASS
class Win(Gui.QWidget):
def __init__(self, parent = None):
Gui.QWidget.__init__(self, parent)
# --------------------------------------------------- SETUP PLAY BUTTON
self.but1 = Gui.QPushButton("Run Commands",self)
self.but1.setGeometry(10,10, 200, 100)
# -------------------------------------------------------- SETUP LABELS
self.label1 = Gui.QLabel("No Commands running", self)
self.label1.move(10, 120)
# ------------------------------------------------------- SETUP ACTIONS
self.connect(self.but1, Core.SIGNAL("clicked()"), runCommands)
# ======================================================= RUN COMMAND FUNCTION
def runCommands():
for i in commands:
win.label1.setText(i) # Make label display the command being run
print win.label1.text() # This shows that the value is actually
# changing with every loop, but its just not
# being reflected in the GUI label
os.system(i)
# ======================================================================== MAIN
# ------------------------------------------------------ THE TERMINAL COMMANDS
com1 = "espeak 'senntence 1'"
com2 = "espeak 'senntence 2'"
com3 = "espeak 'senntence 3'"
com4 = "espeak 'senntence 4'"
com5 = "espeak 'senntence 5'"
commands = (com1, com2, com3, com4, com5)
# --------------------------------------------------- SETUP THE GUI ENVIRONMENT
app = Gui.QApplication(sys.argv)
win = Win()
win.show()
sys.exit(app.exec_())
The label gets updated all right, but the GUI isn't redrawn before the end of your loop.
Here's what you can do about it:
Move your long-running loop to a secondary thread, drawing the GUI is happening in the main thread.
Call app.processEvents() in your loop. This gives Qt the chance to process events and redraw the GUI.
Break up your loop and let it run using a QTimer with a timeout of 0.
Using a thread is the best option, but involves quite a bit more work than just calling processEvents. Doing it with a timer is the old fashioned way and is not recommanded anymore. (see the documentation)
You have a basic misunderstanding of how such a GUI works. A Qt GUI has to run in an event loop of its own. Your loop runs instead, and the GUI can't do its work between the executions of your loop. That is, while your for loop is running the GUI code doesn't get CPU time and won't update.
You can set up a timer with an event, and execute your code in handlers of this event a set amount of time - this will solve your problem.
Or you can just call repaint() it update the GUI instantly.

Categories

Resources