This has to be the biggest nuisance I've encountered with PyQT: I've hacked together a thumbnailing thread for my application (I have to thumbnail tons of big images), and it looks like it would work (and it almost does). My main problem is this error message whenever I send a SIGNAL from my thread:
QPixmap: It is not safe to use pixmaps outside the GUI thread
I can't figure out how to get around this. I've tried passing a QIcon through my SIGNAL, but that still generates the same error. If it helps, here's the code blocks which deal with this stuff:
The Thumbnailer class:
class Thumbnailer(QtCore.QThread):
def __init__(self, ListWidget, parent = None):
super(Thumbnailer, self).__init__(parent)
self.stopped = False
self.completed = False
self.widget = ListWidget
def initialize(self, queue):
self.stopped = False
self.completed = False
self.queue = queue
def stop(self):
self.stopped = True
def run(self):
self.process()
self.stop()
def process(self):
for i in range(self.widget.count()):
item = self.widget.item(i)
icon = QtGui.QIcon(str(item.text()))
pixmap = icon.pixmap(72, 72)
icon = QtGui.QIcon(pixmap)
item.setIcon(icon)
The part which calls the thread (it occurs when a set of images is dropped onto the list box):
self.thread.images.append(f)
item = QtGui.QListWidgetItem(f, self.ui.pageList)
item.setStatusTip(f)
self.thread.start()
I'm not sure how to handle this kind of stuff, as I'm just a GUI newbie ;)
Thanks to all.
After many attempts, I finally got it. I can't use a QIcon or QPixmap from within a non-GUI thread, so I had to use a QImage instead, as that transmits fine.
Here's the magic code:
Excerpt from the thumbnailer.py QThread class:
icon = QtGui.QImage(image_file)
self.emit(QtCore.SIGNAL('makeIcon(int, QImage)'), i, icon)
makeIcon() function:
def makeIcon(self, index, image):
item = self.ui.pageList.item(index)
pixmap = QtGui.QPixmap(72, 72)
pixmap.convertFromImage(image) # <-- This is the magic function!
icon = QtGui.QIcon(pixmap)
item.setIcon(icon)
Hope this helps anyone else trying to make an image thumbnailing thread ;)
Related
I am currently trying to implement some threading functionality in my PySide6 GUI application. I followed a tutorial to try to get started (link is here), and I cannot seem to get it to work. Although that tutorial uses PyQt not PySide, the classes and structure is still similar, and it does seem to launch on another thread. Still though, it freezes the main GUI, which is not desired when this actually faces users.
Here is a sample of my code:
class Worker(QObject):
finished = Signal(str)
progress = Signal(int)
def run(self, file):
"""Long-running task." that calls a separate class for computation""
b = SeparateClass()
b.doComputation()
self.finished.emit()
class DataPlotting(QMainWindow):
def __init__(self):
self.thread = QThread()
self.worker = Worker()
self.report_builder = QPushButton('Call class that threads')
self.report_builder.setEnabled(False)
self.report_builder.clicked.connect(self.qthread_test)
def qthread_test(self):
file = 'some_file.txt'
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.run(file))
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.thread.start()
return
This does accomplish the work that is in the Worker class and spit out the desired results, but it freezes the GUI. I am not really sure what I am doing wrong, as this approach is what has been suggested to prevent freezing GUIs for heavy computation.
Is there something that I am straight up missing? Or am I going about this the wrong way? Any help or guidance is appreciated
I am assuming that you make the appropriate calls to the super class during __init__ for your subclasses of QMainWindow and the QObject.
When your code executes self.thread.started.connect(self.worker.run(file)) that line it runs the function self.worker.run(file) immediately and assigns the result of that function, which is None, as the connected slot to the thread.started signal. Instead of passing the file path as a parameter you can assign it to the worker instance and have the run method grab the path from self during execution.
For example you can try something like this:
class Worker(QObject):
finished = Signal(str)
progress = Signal(int)
def run(self):
"""Long-running task." that calls a separate class for computation"""
file = self.some_file
b = SeparateClass()
b.doComputation()
self.finished.emit()
class DataPlotting(QMainWindow):
def __init__(self):
self.report_builder = QPushButton('Call class that threads')
self.report_builder.setEnabled(False)
self.report_builder.clicked.connect(self.qthread_test)
self.threads = []
def qthread_test(self):
worker = Worker()
thread = QThread()
worker.some_file = 'some_file.txt'
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.finished.connect(thread.quit)
worker.finished.connect(worker.deleteLater)
thread.finished.connect(thread.deleteLater)
thread.start()
self.threads.append(thread)
return
I am building a GUI on python and pyqt.
The GUI has a lot of pushbuttons, generated through class LED, meaning each led has 3 buttons, for an n number of leds.
In a few of the buttons, I want an effect that changes the opacity of the pushbutton, in a loop from 0 to 1 and back again, so it disappears and appears. I need only one process to manage all, so the effect starts at same time for every button and all blink at the same time.
I've managed to achieve that, through qgraphicseffect in a thread, iterating through a list.
The problem is that after a few minutes, the effect stops, although the thread is still running (print(opacity_level)). more pushbuttons with the effect makes even shorter duration. Clicking any button, even others without effect, restarts the gui animation.
My small research in threading on pyqt made me implement this thread manager, although I do not fully understand it.
class WorkerSignals(QtCore.QObject):
finished = QtCore.pyqtSignal()
error = QtCore.pyqtSignal(tuple)
result = QtCore.pyqtSignal(object)
progress = QtCore.pyqtSignal(tuple)
class Worker(QtCore.QRunnable):
'''
Worker thread
Inherits from QRunnable to handler worker thread setup, signals and wrap-up.
'''
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
# Add the callback to our kwargs
self.kwargs['progress_callback'] = self.signals.progress
#pyqtSlot()
def run(self):
'''
Initialise the runner function with passed args, kwargs.
'''
# Retrieve args/kwargs here; and fire processing using them
try:
result = self.fn(*self.args, **self.kwargs)
except:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
self.signals.result.emit(result) # Return the result of the processing
finally:
self.signals.finished.emit() # Done
Next the leds class
class LEDs:
def __init__(self,name,group,frame):
self.opacity_effect = QtWidgets.QGraphicsOpacityEffect()
self.button_auto = QtWidgets.QPushButton()
self.button_auto.setObjectName("button_auto_neutral")
self.button_auto.clicked.connect(lambda state, x=self: self.AutoMode())
def AutoMode(self):
print(self.name,"Automode")
if len(settings.blink) ==0: # start thread only if no previous thread, both thread and
this reference the size of settings.blink, so not ideal.
print("start thread")
settings.ledAutomode()
settings.blink.append(self)
And finally the settings class, which has the thread with the effect performing action. There is a second thread, which handles the icon of the button, accordingly with a timetable.
class Settings:
def __init__(self):
self.blink=[]
def ledAutomode(self):
def blink(progress_callback):
print("opacity")
op_up=[x/100 for x in range(0,101,5)]
op_down=op_up[::-1]; op_down=op_down[1:-1]; opacity=op_up+op_down
while len(self.blink) !=0:
for i in opacity:
print(i)
QtCore.QThread.msleep(80)
for led in self.blink:
led.opacity_effect.setOpacity(i)
def timeCheck(progress_callback):
while len(self.blink) != 0:
QtCore.QThread.msleep(500)
for led in self.blink:
matrix = [v for v in settings.leds_config[led.group][led.name]["Timetable"]]
matrix_time=[]
...
# some code
...
if sum(led_on_time):
led.button_auto.setObjectName("button_auto_on")
led.button_auto.setStyleSheet(ex.stylesheet)
else:
led.button_auto.setObjectName("button_auto_off")
led.button_auto.setStyleSheet(ex.stylesheet)
QtCore.QThread.msleep(int(30000/len(self.blink)))
worker = Worker(blink) # Any other args, kwargs are passed to the run function
ex.threadpool.start(worker)
worker2 = Worker(timeCheck) # Any other args, kwargs are passed to the run function
ex.threadpool.start(worker2)
So, perhaps a limitation on qgraphicseffect, or some problem with the thread (although its keeps printing), or I made some error.
I've read about subclassing the qgraphicseffect but I don't know if that solves the problem.
If anyone has another implementation, always eager to learn.
Grateful for your time.
Widgets are not thread-safe.
They cannot be created nor accessed from external threads. While it "sometimes" works, doing it is wrong and usually leads to unexpected behavior, drawing artifacts and even fatal crash.
That said, you're making the whole process incredibly and unnecessarily convoluted, much more than it should be, most importantly because Qt already provides both timed events (QTimer) and animations.
class FadeButton(QtWidgets.QPushButton):
def __init__(self):
super().__init__()
self.effect = QtWidgets.QGraphicsOpacityEffect(opacity=1.0)
self.setGraphicsEffect(self.effect)
self.animation = QtCore.QPropertyAnimation(self.effect, b'opacity')
self.animation.setStartValue(1.0)
self.animation.setEndValue(0.0)
self.animation.setDuration(1500)
self.animation.finished.connect(self.checkAnimation)
self.clicked.connect(self.startAnimation)
def startAnimation(self):
self.animation.stop()
self.animation.setDirection(self.animation.Forward)
self.animation.start()
def checkAnimation(self):
if not self.animation.value():
self.animation.setDirection(self.animation.Backward)
self.animation.start()
else:
self.animation.setDirection(self.animation.Forward)
If you want to synchronize opacity amongst many widgets, there are various possibilities, but a QVariantAnimation that updates all opacities is probably the easier choice:
class LEDs(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
layout = QtWidgets.QHBoxLayout(self)
self.animation = QtCore.QVariantAnimation()
self.animation.setStartValue(1.0)
self.animation.setEndValue(0.0)
self.animation.setDuration(1500)
self.animation.valueChanged.connect(self.updateOpacity)
self.animation.finished.connect(self.checkAnimation)
self.buttons = []
for i in range(3):
button = QtWidgets.QPushButton()
self.buttons.append(button)
layout.addWidget(button)
effect = QtWidgets.QGraphicsOpacityEffect(opacity=1.0)
button.setGraphicsEffect(effect)
button.clicked.connect(self.startAnimation)
# ... as above ...
def updateOpacity(self, opacity):
for button in self.buttons:
button.graphicsEffect().setOpacity(opacity)
Note that you shouldn't change the object name of a widget during runtime, and doing it only because you want to update the stylesheet is wrong. You either use a different stylesheet, or you use the property selector:
QPushButton {
/* default state */
background: #ababab;
}
QPushButton[auto_on="true"] {
/* "on" state */
background: #dadada;
}
class FadeButton(QtWidgets.QPushButton):
def __init__(self):
super().__init__()
# ...
self.setProperty('auto_on', False)
def setAuto(self, state):
self.setProperty('auto_on', state)
self.setStyleSheet(self.styleSheet())
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?
I want to implement GUI using threading and tkinter (python 3.6).
When I run GUIExecution.py, the following error occurs.
"RuntimeError: Calling Tcl from different appartment" on self.root.mainloop() in base_gui_class.py
I am implementing it on a class basis, and the three code files are as follows.
The executable file is GUIExecution.py.
I spent a lot of time trying to fix the error, but I have not been able to fix it yet.
Please give a lot of advice.
Additionally, if I run the following code in a python2 environment, it works fine without error.
GUIExecution.py
from base_gui_class import *
from base_class import *
speed = 1000
height = 500
width = 700
base_model = base_class()
gui = base_gui_class(base_model, speed, height, width)
base_model.visualize()
base_class.py
class base_class():
genes = []
dicLocations = {}
gui = ''
best = ''
time = 0
def __init__(self):
pass
def visualize(self):
if self.gui != '':
self.gui.start()
def registerGUI(self, gui):
self.gui = gui
base_gui_class.py
import threading
import tkinter as tk
import math
import threading
import time
class base_gui_class(threading.Thread):
root = ''
canvas = ''
speed = 0
base_model = ''
def __init__(self, base_model, speed, h, w):
threading.Thread.__init__(self)
self.base_model = base_model
base_model.registerGUI(self)
self.root = tk.Tk()
self.canvas = tk.Canvas(self.root, height=h, width=w)
self.canvas.pack()
self.root.title("Test")
self.speed = 1 / speed
def run(self):
self.root.mainloop()
def update(self):
time.sleep(self.speed)
width = int(self.canvas.cget("width"))
height = int(self.canvas.cget("height"))
self.canvas.create_rectangle(0, 0, width, height, fill='white')
def stop(self):
self.root.quit()
To a very good first and second approximation, the core of Tk is single threaded. It can be used from multiple threads, but only by initialising it separately in each of those threads. Internally, it uses thread-specific variables extensively to avoid the need for major locking (that is, it has nothing like a big Global Interpreter Lock) but that means you must not cheat. Whatever thread initialises a Tk context must be the only thread that interacts with that Tk context. This includes loading the Tkinter module so you are effectively restricted to using Tkinter from your main thread only; working around this is serious expert's-only stuff.
I recommend that you make your worker threads make changes to your GUI by posting events to it using a queue (or otherwise interlock with critical sections and condition variables, though I find queues easier in practice).
pip install tkthread
#call the function which shows error like this:
tkthread.call_nosync(yourfunction)
this tkthread library handles all the threading internally by itself.
I recommend you reading the documentation of this library:https://pypi.org/project/tkthread/ .
We are working on bringing our application up to date with wxPython 3.0.2, however this is one of two major bug that is still around.
Background: on program start, we spawn a custom dialog telling the user that some things are loading. This dialog has an animation, so it's important to keep the main thread with the GUI clear while we load data in the background thread. When that is done, we sent a command to a callback that Destroy()s the dialog, and the program is able to function as normal.
This works well in 2.8, but it seems to hang our app in 3.0. The dialog message disappears, but we cannot close the program or interact with the GUI, almost as if the GUI was still locked under Modal.
Here's a test script that demonstrates, being as close to the original program as possible in the logic path it takes:
import wxversion
wxversion.select('3.0')
import wx
import time
import threading
class OpenThread(threading.Thread):
def __init__(self, callback):
threading.Thread.__init__(self)
self.callback = callback
self.start()
def run(self):
time.sleep(0.5) # Give GUI some time to finish drawing
for i in xrange(5):
print i
time.sleep(.3)
print "ALL DONE"
wx.CallAfter(self.callback)
class WaitDialog(wx.Dialog):
def __init__(self, parent, title = "Processing"):
wx.Dialog.__init__ (self, parent, id=wx.ID_ANY, title = title, size=(300,30),
style=wx.NO_BORDER)
mainSizer = wx.BoxSizer( wx.HORIZONTAL )
self.SetBackgroundColour(wx.WHITE)
txt = wx.StaticText(self, wx.ID_ANY, u"Waiting...", wx.DefaultPosition, wx.DefaultSize, 0)
mainSizer.Add( txt, 1, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 0 )
self.SetSizer( mainSizer )
self.Layout()
self.Bind(wx.EVT_CLOSE, self.OnClose)
self.CenterOnParent()
def OnClose(self, event):
pass
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "Test")
self.waitDialog = None
mainSizer = wx.BoxSizer( wx.HORIZONTAL )
choice = wx.Choice(self, wx.ID_ANY, style=0)
choice.Append("No Selection", 0)
choice.Append("Selection 1", 1)
choice.Append("Selection 2", 2)
mainSizer.Add( choice , 1, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 0 )
self.SetSizer( mainSizer )
self.Show()
self.doThing()
def doThing(self):
self.waitDialog = WaitDialog(self, title="Opening previous fits")
OpenThread(self.closeWaitDialog)
self.waitDialog.ShowModal()
def closeWaitDialog(self):
self.waitDialog.Destroy()
test = wx.App(False)
MainFrame()
test.MainLoop()
You can comment out the self.waitDialog bits and see that it is the dialogs giving trouble. There are other places in the program that this happens in, always after we close out of a Dialog. Is there something I'm missing? Is there a workaround? We also have a few more dialogs that we utilize, so a workaround would ideally be a small fix rather than a huge refactoring
wx.CallAfter basically just puts the event into a queue, so I wonder if the event queue is getting blocked somehow such that it isn't processing the event that would call your event handler to destroy the dialog.
I found the following that might help:
wxPython wx.CallAfter - how do I get it to execute immediately?
Basically you could use wx.WakeUpIdle() or wx.GetApp().ProcessIdle() or maybe even ProcessPendingEvents.
This might also be helpful:
http://wxpython.org/Phoenix/docs/html/window_deletion_overview.html
I also found this useful StackOverflow answer to a similar problem in that you may just need to call the dialog's EndModal method before Destroying it.