Pyqt5--Why QApplication.processEvents() failed to update GUI - python-3.x

I am learning to use PyQT5 and QtDesign. The workflow is :Click button->Change a image. But GUI failed to update until QMessege appeared. Here is a code. Firstly, I draw a button named pass_btn, a label named fds_img, by QtDesign.
Then, code as below:
from PyQt5 import QtWidgets, uic, QtGui
import os, sys
from PyQt5.QtWidgets import *
app = QtWidgets.QApplication(sys.argv)
dlg = uic.loadUi('grading_sys.ui')
stepper = 0
img_dir_list = [] #this is a image dir list
dlg.fds_img.setPixmap(QtGui.QPixmap(img_dir_list[stepper]))
def pass_btn():
global stepper
if stepper == len(img_dir_list) - 1:
QtWidgets.QMessageBox.information(None, 'Warning', 'Warning')
return
stepper += 1
dlg.fds_img.setPixmap(QtGui.QPixmap(img_dir_list[stepper]))
print('Try to refresh')
QApplication.processEvents()
dlg.pass_btn.clicked.connect(pass_btn)
dlg.show()
sys.exit(app.exec_())
So why I could not refresh the label? Or how could I refresh image when I click the button?

Finally, I solved this by using QTimer. I know it is a waste of CPU, may not efficient enough, but it finally works. But I still confusing about why it did't work before. I solved this as codes below:
First, put all the update codes in a def, then connected it with QTimer, and ran it every second:
def refresh_ui():
print('Try to refresh UI')
print('Prepare to change image' + str(img_dir_list[stepper]))
dlg.fds_img.setPixmap(QtGui.QPixmap(img_dir_list[stepper]))
timer = QTimer()
timer.timeout.connect(refresh_ui)
timer.start(1000)
My environment: MacOS 10.14.2, PyCharm 2018.3.3 edu edition, Python 3.6, PyQt5, QtDesign (through Anaconda). Dose this happened because of MacOS?

Related

Change WM_CLASS of Dialog (tkinter)

Is there a way to change WM_CLASS of showinfo (and other dialogs)? className, class_ parameters do the job for Tk, Toplevel.
import tkinter as tk
from tkinter.messagebox import showinfo
if __name__ == '__main__':
root = tk.Tk(className='ymail')
mail_client = tk.Toplevel(root, class_='ymail')
new_message = tk.Toplevel(root, class_='ymail')
showinfo(title="Cancel sending", parent=new_message, message="""
Send is cancelled due to empty message""")
root.mainloop()
For showinfo dialog
$ xprop WM_CLASS
gives
WM_CLASS(STRING) = "__tk__messagebox", "Dialog"
I think it is convenient to cycle tkinter windows with Alt-~ (Tilde), for which their WM_CLASS shall be the same.
I did the search ("tkinter change WM_CLASS showinfo"). Some of the hits are not applicable, some don't work (xdotool), and some I'd rather use as a last resort (converting C program to python).
Using
Debian 10
python 3.7.3
GNOME 3.30.1
EDIT
Added workaround (using xdotool)
import threading
import subprocess
import time
import tkinter as tk
from tkinter.messagebox import showinfo
def change_dialog_class(from_="Dialog", to_="ymail"):
cmd = f"xdotool search --class {from_} set_window --class {to_}"
time.sleep(1)
subprocess.run(cmd.split())
if __name__ == '__main__':
root = tk.Tk(className='ymail')
mail_client = tk.Toplevel(root, class_='ymail')
new_message = tk.Toplevel(root, class_='ymail')
tk.Frame.class_ = 'ymail'
threading.Thread(target=change_dialog_class, args=("Dialog", "ymail"),
daemon=True).start()
showinfo(title="Cancel sending", parent=new_message,
message="""Send is cancelled due to empty message""")
root.mainloop()
along with ymail.desktop it works
$ cat ~/.local/share/applications/ymail.desktop
[Desktop Entry]
Type=Application
Terminal=false
Name=ymail
Icon=python
StartupWMClass=ymail
yet, the python solution would be better
Since I'm not a XSystem user it took me some time to follow up. It
seems like that you are looking for wm_group and unfortunately it isnt
possible without subclassing it, which results in pretty much the same
as writing your own class with tk.Toplevel. Anyway I hope
toplevel.wm_group(root) ease things out and works for you.
After I noticed that the SimpleDialog may has some functionality that you want to keep and can be hard to code for yourself, I decided to write an answer that you may want to use. It also provides the class_ option in case wm_group dosent work for you.
Here is the code:
import tkinter as tk
import tkinter.simpledialog as simpledialog
class MessageBox(simpledialog.SimpleDialog):
def __init__(self, master,**kwargs):
simpledialog.SimpleDialog.__init__(self,master,**kwargs)
#root.tk.call('wm', 'group', self.root._w, master)
def done(self,num):
print(num)
self.root.destroy()
root = tk.Tk()
MessageBox(root,title='Cancel',text='Im telling you!',class_='ymail',
buttons=['Got it!','Nah'], default=None, cancel=None)
root.mainloop()
and here is the source:
https://github.com/python/cpython/blob/main/Lib/tkinter/simpledialog.py#L31

How to run two process at once using kivy

I'm struggling to simultaneously run my Kivy app alongside a python script that is being locally imported.
Full python code
import Client # Locall import
import time
from threading import Thread
from kivy.app import App
from kivy.uix.button import Button
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
class MainWindow(Screen):
pass
class SecondWindow(Screen):
pass
class WindowManager(ScreenManager):#will manage navigation of windows
pass
kv = Builder.load_file("my.kv")
class Sound(App):
def build(self):
return kv
def ipconfig(self,input_ip):
if len(input_ip) == 13:
print('Address binded!!')
Client.host = input_ip #Modify ip adress
else:
print('Invalid input_ip')```
if __name__ == '__main__':
#Sound().run()#Kivy run method
Thread(target = Sound().run()).start()
time.sleep(10)
Thread(target = Client.father_main).start()
Where the threading happens
if __name__ == '__main__':
#Sound().run()#Kivy run method
Thread(target = Sound().run()).start()
time.sleep(10)
Thread(target = Client.father_main).start() #Client is locally imported
PROBLEMS
1.Only the kivy app runs but the father_main function fails to.
2.The only time father_main runs is when I close the kivy application.
3.If i try and remove the 'run()' from Sound(). I get TypeError: 'Sound' object is not callable and father_main immediately runs
4.If i only remove the parenthesis from 'run()' so it turns into 'run'. I get Segmentation fault (core dumped)
kivy does not encourage the use of time.sleep() and i still have no clue of what exactly your program is but here a solution.
create an on_start method (A method that runs when kivy app started) and add start the ipconfig method from there but you're going to start it asynchronously.
from multiprocessing.pool import ThreadPool
class Sound(App):
def on_start(self):
pool = ThreadPool(processes=1)
async_start = pool.apply_async(self.ip_config, ("value for ip_input here"))
# do some other things inside the main thread here
if __name__ == "__main__":
Sound().run()
You need to run the App on the main thread. I would suggest something like:
def start_father_main(dt):
Thread(target = Client.father_main).start() #Client is locally imported
if __name__ == '__main__':
Clock.schedule_once(start_father_main, 10)
Sound().run()
I haven't tested this code, but it should give you the idea.

PyQT5 slot with Matplotlib has no effect

I'm trying to implement a slot method that will clear a matplotlib figure that is part of QtWidget. I have tried with both Python 3.6 and 3.5 on windows 7 and 10 and i get same behaviour.
My issue is that i can perfectly clear the figure when calling the wipe method from the main block of code or within the class drawing the figure. But whenever this very same method is called as a slot to a PyQT signal, slot is actually activated but the call to fig.clf() does not clear the figure.
The code below shows the issue :
import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import QApplication,QMainWindow, QSizePolicy,QWidget,QVBoxLayout, QPushButton
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
class GraphPopulate(FigureCanvas):
def __init__(self, parent=None):
self.fig = Figure(figsize=(6,4))
self.ax = self.fig.add_axes([0.07, 0.16, 0.95, 0.95]) # fraction of figure size #
FigureCanvas.__init__(self, self.fig)
self.setParent(parent)
x = [2000, 2001, 2002, 2003, 2004]
y = [10, 20, 30, 40, 50]
self.plt = self.ax.plot(x, y)
# self.wipe() # OK This one works as well
# self.plt.close(self.fig) # close the app, does not only clear figure
def wipe(self):
print('wipe start')
# del self.fig # close the app on second call, does not only clear figure !!
# self.plt.close(fig) # close the app, does not only clear figure
rc = self.fig.clf()
print('return code from clf() '+ str(rc))
print('wipe stop')
if __name__ == '__main__':
# create main window
app = QApplication(sys.argv)
MainWindow = QMainWindow()
MainWindow.resize(800, 600)
# create widget to be used by MathPlotlib
WidgetMatplot = QWidget(MainWindow)
WidgetMatplot.setGeometry(QRect(10, 40, 500, 500))
# create Push Button with clicked signal linked to GraphPopulate.wipe() slot
button = QPushButton(MainWindow)
button.setText("Push Me !")
# add widget to vertical box layout
hbox = QVBoxLayout(MainWindow)
hbox.addWidget(WidgetMatplot)
hbox.addWidget(button)
g = GraphPopulate(WidgetMatplot)
button.pyqtConfigure(clicked=g.wipe) # NOT OK !!!!!!! g.wipe is triggered as prints statements show
# on console but self.fig.clf() has no effect
MainWindow.show()
# g.wipe() # OK
sys.exit(app.exec_())
I've put comments on statements that if uncommented are actually clearing the figure or closing the whole app.
Looks like the "matplotlib context" is not the same when called from the signa-slot connect than from other calls.
May be i miss something trivial. If so sorry for that but i couldn't find any direction to investigate by myself...so i rely on you guys to spot it out.
Thanks - Thibault
In your code the figure is successfully cleared. You just don't see it because nobody told the figure that it needs to be redrawn.
If you redraw the figure, you'll see that it is indeed empty.
def wipe(self):
self.fig.clf()
self.fig.canvas.draw_idle()

pyQt and threading application crash

I wrote simple program which has pyQt interface with 2 buttons (start and cancel). Start button runs some calculations in the background (by starting update function) and thanks to threading I can still use UI.
But the application crashes after 10sec - 2 minutes. UI just dissapears, program shutdown.
when I use pythonw to run app without console thread crashes after ~25 sec but gui still works.
#!/usr/bin/python
import threading
import sys
from PyQt4 import QtGui, QtCore
import time
import os
class Class(QtGui.QWidget):
def __init__(self):
#Some init variables
self.initUI()
def initUI(self):
#some UI
self.show()
def update(self,stop_event):
while True and not stop_event.isSet():
self.updateSpeed()
self.updateDistance()
self.printLogs()
self.saveCSV()
self.guiUpdate()
time.sleep(1)
#gui button function
def initiate(self):
self.stop_event = threading.Event()
self.c_thread = threading.Thread(target = self.update, args=(self.stop_event,))
self.c_thread.start()
#Also gui button function
def cancelTracking(self):
self.stop_event.set()
self.close()
def main():
app = QtGui.QApplication(sys.argv)
ex = Class()
sys.exit(app.exec_())
ex.update()
if __name__ == '__main__':
main()
I dont know if I'm doing threading right. I found example like this on stack. I'm quite new to python and I'm using threading for the first time.
It is most likely due to calling a GUI function in your separate thread. PyQt GUI calls like setText() on a QLineEdit are not allowed from a thread. Anything that has PyQt painting outside of the main thread will not work. One way to get around this is to have your thread emit a signal to update the GUI when data is ready. The other way is to have a timer periodically checking for new data and updating the paintEvent after a certain time.
========== EDIT ==========
To Fix this issue I created a library named qt_thread_updater. https://github.com/justengel/qt_thread_updater This works by continuously running a QTimer. When you call call_latest the QTimer will run the function in the main thread.
from qt_thread_updater import get_updater
lbl = QtWidgets.QLabel('Value: 1')
counter = {'a': 1}
def run_thread():
while True:
text = 'Value: {}'.format(counter['a'])
get_updater().call_latest(lbl.setText, text)
counter['a'] += 1
time.sleep(0.1)
th = threading.Thread(target=run_thread)
th.start()
========== END EDIT ==========
#!/usr/bin/python
import threading
import sys
from PyQt4 import QtGui, QtCore
import time
import os
class Class(QtGui.QWidget):
display_update = QtCore.pyqtSignal() # ADDED
def __init__(self):
#Some init variables
self.initUI()
def initUI(self):
#some UI
self.display_update.connect(self.guiUpdate) # ADDED
self.show()
def update(self):
while True and not self.stop_event.isSet():
self.updateSpeed()
self.updateDistance()
self.printLogs()
self.saveCSV()
# self.guiUpdate()
self.display_update.emit() # ADDED
time.sleep(1)
#gui button function
def initiate(self):
self.stop_event = threading.Event()
self.c_thread = threading.Thread(target = self.update)
self.c_thread.start()
#Also gui button function
def cancelTracking(self):
self.stop_event.set()
self.close()
def main():
app = QtGui.QApplication(sys.argv)
ex = Class()
sys.exit(app.exec_())
# ex.update() # - this does nothing
if __name__ == '__main__':
main()
The other thing that could be happening is deadlock from two threads trying to access the same variable. I've read that this shouldn't be possible in python, but I have experienced it from the combination of PySide and other Python C extension libraries.
May also want to join the thread on close or use the QtGui.QApplication.aboutToQuit signal to join the thread before the program closes.
The Qt documentation for QThreads provides two popular patterns for using threading. You can either subclass QThread (the old way), or you can use the Worker Model, where you create a custom QObject with your worker functions and run them in a separate QThread.
In either case, you can't directly update the GUI from the background thread, so in your update function, the guiUpdate call will most likely crash Qt if it tries to change any of the GUI elements.
The proper way to run background processes is to use one of the two QThread patterns and communicate with the main GUI thread via Signals and Slots.
Also, in the following bit of code,
app = QtGui.QApplication(sys.argv)
ex = Class()
sys.exit(app.exec_())
ex.update()
app.exec_ starts the event loop and will block until Qt exits. Python won't run the ex.update() command until Qt has exited and the ex window has already been deleted, so you should just delete that command.

Incorrect behaviour of print() when executed from within a QTDialog window in Spyder

I am working on a very simple interface to explore/graph csv files. My aim is ultimately to explore, not to build software as I am not a developer, more of a "desperate user" :-)
I am leveraging the code found in this example
These are my first steps both in Python and in GUI, so I tend to put print messages in my calls so that I can more or less track what is happening. And this is where I found a strange behavior if I run the code from within Spyder.
import sys
import os
from PyQt4 import QtGui
import pandas as pd
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
import matplotlib.pyplot as plt
# QtGui.QDialog
class Window(QtGui.QDialog):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
# a figure instance to plot on
self.figure = plt.figure()
# this is the Canvas Widget that displays the `figure`
# it takes the `figure` instance as a parameter to __init__
self.canvas = FigureCanvas(self.figure)
# this is the Navigation widget
# it takes the Canvas widget and a parent
self.toolbar = NavigationToolbar(self.canvas, self)
# Just some extra button to mess around
self.button= QtGui.QPushButton('Push Me')
self.button.clicked.connect(self.do_print)
# set the layout
layout = QtGui.QVBoxLayout()
layout.addWidget(self.toolbar)
layout.addWidget(self.canvas)
layout.addWidget(self.button)
self.setLayout(layout)
def do_print(self):
print('Hello World!!')
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
main = Window()
main.show()
sys.exit(app.exec_())
The strange behavior is that if I push the button once, nothing happens on the Ipython console. By the second time I push, then two "Hello World!" printouts appear.
If, on the other hand, I just launch my script from within a Windows Shell:
python my_simple_test.py
Then everything works as expected.
What am I then doing wrong from within Spyder?
Thanks,
Michele
IPython buffers stdout a bit differently from a terminal. When something is printed, it looks at how long it has been since it last flushed the buffer, and if it's longer than some threshold, it flushes it again. So the second time you click the button, it flushes stdout, and you see both outputs.
You can force it to flush immediately like this:
print('Hello World!!')
sys.stdout.flush()

Resources