PyQt: updating GUI from a callback - python

Using Python3 and PyQt4 I have a function (run) that takes as an input a callable to provide status updates.
class Windows(QtGui.QWidget):
# Creates a widget containing:
# - a QLineEdit (status_widget)
# - a button, connected to on_run_clicked
def on_run_clicked(self):
def update(text):
self.widget.setText(text)
threading.Thread(target=run, args=(update, )).start()
This works ok (i.e. the text updates are displayed properly in the widget). However, when I replace QLineEdit by QTextEdit and use the append method to add text, I get:
QObject::connect: Cannot queue arguments of type 'QTextCursor'
(Make sure 'QTextCursor' is registered using qRegisterMetaType().)
It still works but point to the fact that I am doing something wrong, and I am not sure that I will keep working when more threads are active. Usually, I do this type of updates using signals and slots, but the run function is not PyQt specific. The questions are:
Why does it work without a warning for QLineEdit and not for
QTextEdit?
What is the right way to deal with a situation like this?

I don't know the specific reason why one class works and the other doesn't - nor do I really know the difference between using Python threading vs. Qt's threading...however, I can tell you that it is very tempremental if you don't set it up properly. Namely, you cannot (or at the very least, should not) modify GUI objects from a thread. Again, not sure the difference of a python vs. a Qt thread on that. But, the safe way to modify your interface from a GUI is by sending signals to your window...easiest way I know to do this is via the Qt threading.
class MyThread(QtCore.QThread):
updated = QtCore.pyqtSignal(str)
def run( self ):
# do some functionality
for i in range(10000):
self.updated.emit(str(i))
class Windows(QtGui.QWidget):
def __init__( self, parent = None ):
super(Windows, self).__init__(parent)
self._thread = MyThread(self)
self._thread.updated.connect(self.updateText)
# create a line edit and a button
self._button.clicked.connect(self._thread.start)
def updateText( self, text ):
self.widget.setText(text)

Related

PyQt5 Controller cannot access children names

for a python program I created a gui with QtDesigner. Inside my program the gui is initiated and calls the .ui-file to implement content.
The gui class object is than given to a Controller, and here it gets tricky:
Instead of one Controller, there are a main controller and some sub-controller for different parts of the gui. The main Controller does one or two general things and than hands over different parts of the gui to different sub controller.
See the following example for better understanding:
import Controller # import the folder with the controller
def __init__(self, window):
self.window = window # self.window is the handed over gui
self.sub_controls()
def sub_controls(self):
Controller.Sub_Controller_1(window = self.window.part_1)
Controller.Sub_Controller_2(window = self.window.part_2)
Controller.Sub_Controller_3(window = self.window.part_3)
...
The sub-Controller is set up like this:
def __init__(self, window):
self.window = window
... # do stuff:
-----------------------------------------
by trying to call a widget in this Sub-Controller (lets say its a label called timmy), i get an error:
self.window.timmy.setVisible(False)
AttributeError: 'QWidget' object has no attribute 'timmy'
but by using the children()-Method, which returns a list of all children in the gui, I may access this label:
self.window.children()[1].setVisible(False)
This works well and hides timmy.
By trying to do this in the main Controller, it works fine as usual:
self.window.timmy.setVisible(False) # works fine here
I also tried to save the sub-controller object like this:
def sub_controls(self):
self.save_part_1 = Controller.Sub_Controller_1(window = self.window.part_1)
but this doesn't work.
Does any one have a suggestion, how I could solve this Problem?
Sure, I couldt access just all widgets with the children()-method-list, but this is laborious because of the great amount of widgets. Same thing applies to reassigning every child.
PS:
Unfortunately I cannot show you the original Code due to company guidelines.
Ok so I figured out, that the tree Structure of in each other nested widgets has nothing to do with the naming of the widgets. For example:
MainWindow
->centralWidget
->otherWidget
->Button
In this you can address "Button" with self.Button, because (how I believe) the name is saved in MainWindow-Level. If you cut out "otherWidget", the QPushButton in it still exists, but cannot addressed by name.
So it's not possible to just hand over a part of your Gui to a Handler, at least if it's build with QtDesigner.
My Solution for me to the original Problem is to hand over the complete Gui to every sub handler:
def __init__(self, window):
self.window = window # self.window is the handed over gui
self.sub_controls()
def sub_controls(self):
Controller.Sub_Controller_1(window = self.window)
Controller.Sub_Controller_2(window = self.window)
# Controller.Sub_Controller_3(window = self.window.part_3) # before

Handling QDialog's events safely in PyQT

I'm having troubles with a Qt Application (managed through PyQt5) which used to be reliable, until a bunch of updates (where I ended up with PyQt6). PyQt6 is not the culprit here, as I began to have those problems with later PyQt5 versions.
I'm suspecting my problems are instead linked to an abuse of exec() methods call. The full traceback is not available (I've only a RuntimeError with a cryptic message telling me the Dialog is not available anymore); what (I think) I'm seeing are the parent windows randomly disappearing. This behaviour is indeed mentionned in the Qt documentation:
Note: Avoid using this function; instead, use open(). Unlike exec(), open() is asynchronous, and does not spin an additional event loop. This prevents a series of dangerous bugs from happening (e.g. deleting the dialog's parent while the dialog is open via exec()). When using open() you can connect to the finished() signal of QDialog to be notified when the dialog is closed.
Initially, my app was constructed this way:
QMainWindow A > QDialog B > QDialog C > ...
Each QDialog was launched using .exec() method.
The pseudo code could be summarized this way [edit] :
class A(QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ui = Some_Ui_From_QtDesigner()
self.ui.setupUi(self)
self.ui.some_menu.triggered.connect(self.load_popup)
def load_popup():
B = QDialog(A)
if B.exec() == 1:
# Do some things (extract data from B and update A)
if C.exec() == 1:
# Do some things (extract data from C and update B)
qApp = QtWidgets.QApplication.instance()
a = A()
a.show()
qApp.setQuitOnLastWindowClosed(True)
qApp.exec()
I've tried to use .open() methods instead of .exec() and came up with something like this (using QLoopEvent's and events handling):
def process_C_qdialog(parent):
C = QDialog(parent)
# Construct your QDialog some more ...
loop = QtCore.QEventLoop()
my_result = None
def _accept():
my_result = "blah"
loop.exit(0)
def _reject():
loop.exit(-1)
f.accepted.connect(_accept)
f.rejected.connect(_reject)
f.open()
loop.exec()
return my_result
My troubles are far from over as yet. I suspect that recreating manually a QEventLoop causes the same problems than using .exec() in the first place.
So some questions:
Am I right about the QEventLoop?
Would it be safer/enough to leave the parent out of the QDialog generator?
Any advice on how to handle this the right way (keeping a modal dialog if possible)?
More generally, are they any safe ways to reproduce the static methods of Qt through a custom python function? (ie, a function which would load a customized popup, wait for the user's input and returning it)
Note: as this behaviour happens erratically (and without the full traceback available), I'm not 100% sure of this; but I suspect that even a simple QMessageBox().exec() has the same effects, even if this is not stated in the doc.

Execute method not blocked in qgis plugin

I am developing a QGIS Plugins, complex plugin with various classes and methods that call other methods.
My plugin works fine, but I'm loking to run some class method in the background (something like jobs in JAVA) to avoid frozen GUI. I mean, I have a class, this class has a method and this method I would like run in background
I tried with QThread,QTask, threading, QProcess, etc, but I did not find what I want to do.
Some idea could help me? Some plugin does works with background process? Some plugin example?
Thanks
I have been using QTask in order to run heavy processes in the background.
The useful ressources I am using are these ones :
https://gis.stackexchange.com/questions/94477/how-to-use-threads-in-pyqgis-mainly-to-keep-ui-active
https://github.com/qgis/QGIS/pull/3004
You have to specifically create a new Class that inherits from QgsTask and call it in the run() method of your GUI Plugin.
In my case this is what I have :
class MyPlugin:
"""QGIS Plugin Implementation.
... __init__ method, tr method, initGui method, ...
"""
def run(self):
"""Run method that performs the work after clicking 'OK' in the Plugin GUI"""
task = MyTask('UserFriendlyTaskName', **props)
QgsApplication.taskManager().addTask( task )
class MyTask(QgsTask):
def __init__(self, task_name, **props):
QgsTask.__init__(self, desc)
# do w/e with the props
def run(self):
# call your heavy processes
See this .gif below for the UI rendering in QGIS3, there is no freezing and I can still move on the map and edit data while the plugin is running.
QGIS GUI Launch Background Process GIF
Hope this answer finds you in time !
If i understand correctly, you want to refresh the UI to avoid QGIS freezing.
This is what worked for me:
from PyQt5.QtWidgets import QApplication
QApplication.processEvents()
This will refresh the UI of QGIS.
You probably have to call it multiple time, for example after every iteration in a for loop.
thanks you for your answer
what I would to do is run a method in background without freezing QGIS GUI
a example of my code
class Example:
def __init__(self):
code
self.createBotton()
def createButton(self):
Here I create my button
self.okbutton.clicked.connect(self.pressOkButton)
def pressOkButton(self):
here I call other methods
self.methodA()
self.methodB()
def methodA(self):
code
def methodB(self):
code
my question is--> how can i run 'pressOkButton' method in background? When i executed this method i can not ineract with QGIS GUI until finish method
NOTE:consider that it is a class that belongs to my plugin so this class is instantiated when starting the plugin

Starting a Qthread on a button click in PyQt5 [duplicate]

This question already has answers here:
QThread: Destroyed while thread is still running
(2 answers)
Closed 4 years ago.
I'm currently trying to make an API call when my button is clicked without pausing my GUI for the duration of the call. I was using threads to set the text of my Qlable to the response of the API call. This worked however it was unsafe as I was accessing GUI elements from a separate thread.
Currently, I'm attempting to use QThreads to make the API call and then emit the response to the GUI thread however, when I create the Qthread object my program finishes with exit code 3. I've simplified the problem for more clarity.
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("TestWindow")
self.setFixedSize(300,75)
self.main_layout = QtWidgets.QGridLayout()
self.setLayout(self.main_layout)
self.txt_0 = QtWidgets.QLabel()
self.btn_0 = QtWidgets.QPushButton('Press Me')
self.btn_0.clicked.connect(self.btn_0_clicked)
self.main_layout.addWidget(self.txt_0, 0, 0)
self.main_layout.addWidget(self.btn_0, 1, 0)
self.show()
def btn_0_clicked(self):
temp_thread = StringThread("name")
temp_thread.start()
class StringThread(QtCore.QThread):
str_signal = QtCore.pyqtSignal(str)
_name = ''
def __init__(self, name):
QtCore.QThread.__init__(self)
self._name = name
print("Thread Created")
def run(self):
self.str_signal.emit('Emitted message from StringThread. Name = ' + self._name)
print("Done run")
My intention is to set the text of my Qlable to the message emitted from the pyqtSignal in the StringThread class however, as soon as I click the button my program finishes with exit code 3.
edit:
I made the following changes to the btn_0_clicked method
def btn_0_clicked(self):
self.temp_thread = StringThread("hello")
self.temp_thread.str_signal.connect(self.txt_0.setText)
self.temp_thread.start()
It's working now.
You should probably read up on the Qt C++ documentation to better understand what is happening:
http://doc.qt.io/qt-5/qobject.html
http://doc.qt.io/qt-5/qthread.html
https://wiki.qt.io/QThreads_general_usage
In general you have two problems (one of which is worth listing twice, to examine it from different angles):
Your QThread is owned/associated by/with the thread that created it for the purpose of emitting events. So all code trying to emit events is actually trying to access the event loop of your main thread (UI). Read up on QObject's thread() and moveToThread() methods to understand this more fully.
You overrode the run() method of QThread which is what is invoked on the actual thread represented by QThread (i.e. the code that winds up getting called eventually once you call start() on the thread). So you are effectively trying to access the main event loop in an unsafe manner from a different thread.
You overrode the run() method of QThread. The default implementation of QThread sets up a QEventLoop, runs it and that is what gives QObject's associated/owned with/by that thread the ability to use signal/slots freely. You probably did not intend to lose that default implementation.
Basically what you want to do instead is this:
Do not subclass QThread. Instead write a custom QObject worker subclass with appropriate events
Construct a QThread with an eventloop (the default IIRC, but check the docs)
Construct an instance of your custom worker object.
Move your worker object to the newly created QThread with moveToThread().
Wire up the signals and slots, make sure you are using the QueuedConnection type (the default is DirectConnection which is not suitable for communicating across thread boundaries).
Start your thread which should just run its own QEventLoop.
Emit the signal to kickstart your worker object logic.
Receive notification of the results via signals on your main thread (eventloop), where it is safe to update the UI.
Figure out how you want to terminate the QThread and clean up associated resources. Typically you would fire a custom event wired up to the destroyLater() slot to get rid of your worker object. You may want to cache/reuse the QThread for subsequent button clicks or you may want to tear it down as well once your worker object has been cleaned up.

QTextEdit: Overwrite Ctrl-Z with undo() in PyQt4

I subclassed QTextEdit
class Q_My_TextEdit(QtGui.QTextEdit):
def __init__(self, *args):
QtGui.QTextEdit.__init__(self, *args)
def undo(self):
print("undo")
#my own undo code
In my other class:
self.textedit=Q_My_TextEdit()
def keyPressEvent(self,event):
if event.key()==(Qt.Key_Control and Qt.Key_Z):
self.textedit.undo()
If you type some text into your QTextEdit, and hit CTRL-Z, it gets undo-ed, but without calling the function "undo". So how does it work in detail?
The background is, in a second step I want to set new Text (setText()), so the undo stack is deleted. I already have running code that makes the undo itself, but I can't trigger it on CTRL-Z, because with the "Z" the keyshortcut is somehow reserved. For example, if I call my own undo with event.key()==(Qt.Key_Control and Qt.Key_Y), it works.
Ah, So additionally to the keyPressEvent which I have in my second class, you have to put it also into the Subclass!
class Q_My_TextEdit(QtGui.QTextEdit):
def __init__(self, *args):
QtGui.QTextEdit.__init__(self, *args)
def keyPressEvent(self,event):
if event.key()==(Qt.Key_Control and Qt.Key_Z):
self.undo()
#To get the remaining functionality back (e.g. without this, typing would not work):
QTextEdit.keyPressEvent(self,event)
def undo(self):
print("undo")
#my own undo code
But now I can't type into my textedit any more! How can I solve this?
--> Solved. See Properly handling a keyPressEvent in a Subclassed PyQT LineEdit
In C++ you would have to install an event filter, probably with PyQt this is similar (overwrite virtual bool eventFilter(QObject* pObject, QEvent* pEvent); in your editor class). CTRL-Z gets probably filtered by the QTextEdit event filter thus it never gets to keyPressEvent.

Categories