Custom pyqtSignal implementation - python

In PyQt, you can use QtCore.pyqtSignal() to create custom signals.
I tried making my own implementation of the Observer pattern in place of pyqtSignal to circumvent some of its limitations (e.g. no dynamic creation).
It works for the most part, with at least one difference.
Here is my implementation so far
class Signal:
def __init__(self):
self.__subscribers = []
def emit(self, *args, **kwargs):
for subs in self.__subscribers:
subs(*args, **kwargs)
def connect(self, func):
self.__subscribers.append(func)
def disconnect(self, func):
try:
self.__subscribers.remove(func)
except ValueError:
print('Warning: function %s not removed from signal %s'%(func,self))
The one thing noticed was a difference in how QObject.sender() works.
I generally stay clear of sender(), but if it works differently then so may other things.
With regular pyqtSignal signals, the sender is always the widget closest in a chain of signals.
In the example at the bottom, you'll see two objects, ObjectA and ObjectB. ObjectA forwards signals from ObjectB and is finally received by Window.
With pyqtSignal, the object received by sender() is ObjectA, which is the one forwarding the signal from ObjectB.
With the Signal class above, the object received is instead ObjectB, the first object in the chain.
Why is this?
Full example
# Using PyQt5 here although the same occurs with PyQt4
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class Window(QWidget):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
object_a = ObjectA(self)
object_a.signal.connect(self.listen)
layout = QBoxLayout(QBoxLayout.TopToBottom, self)
layout.addWidget(object_a)
def listen(self):
print(self.sender().__class__.__name__)
class ObjectA(QWidget):
signal = Signal()
# signal = pyqtSignal()
def __init__(self, parent=None):
super(ObjectA, self).__init__(parent)
object_b = ObjectB()
object_b.signal.connect(self.signal.emit)
layout = QBoxLayout(QBoxLayout.TopToBottom, self)
layout.addWidget(object_b)
class ObjectB(QPushButton):
signal = Signal()
# signal = pyqtSignal()
def __init__(self, parent=None):
super(ObjectB, self).__init__('Push me', parent)
self.pressed.connect(self.signal.emit)
if __name__ == '__main__':
import sys
app = QApplication([])
win = Window()
win.show()
sys.exit(app.exec_())
More reference
Edit:
Apologies, I should have provided a use-case.
Here are some of the limitations of using pyqtSignals:
pyqtSignal:
..only works with class attributes
..cannot be used in an already instantiated class
..must be pre-specified with the data-types you wish to emit
..produces signals that does not support keyword arguments and
..produces signals that cannot be modified after instantiation
Thus my main concern is using it together with baseclasses.
Consider the following.
6 different widgets of a list-type container widget share the same interface, but look and behave slightly different. A baseclass provides the basic variables and methods, along with signals.
Using pyqtSignal, you would have to first inherit your baseclass from at least QObject or QWidget.
The problem is neither of these can be use in as mix-ins or in multiple inheritance, if for instance one of the widgets also inherits from QPushButton.
class PinkListItem(QPushButton, Baseclass)
Using the Signal class above, you could instead make baseclasses without any previously inherited classes (or just object) and then use them as mix-ins to any derived subclasses.
Careful not to make the question about whether or not multiple inheritance or mix-ins are good, or of other ways to achieve the same thing. I'd love your feedback on that as well, but perhaps this isn't the place.
I would be much more interested in adding bits to the Signal class to make it work similar to what pyqtSignal produces.
Edit 2:
Just noticed a down-vote, so here comes some more use cases.
Key-word arguments when emitting.
signal.emit(5)
Could instead be written as
signal.emit(velocity=5)
Use with a Builder or with any sort of dependency injection
def create(type):
w = MyWidget()
w.my_signal = Signal()
return w
Looser coupling
I'm using both PyQt4 and PyQt5. With the Signal class above, I could produce baseclasses for both without having it depend on either.

You can do this with a metaclass that inherits from pyqtWrapperType. Inside __new__, call pyqtSignal() as needed and set the attributes on the result class.

Related

How to access self object within class from outside

I am writing an application that uses a Qt GUI and am now trying to multithread it (first time learner). A longer task will be contained within the worker thread eventually but output logs will be required to be written as it goes. The GUI class has a method to output these logs to a plain text widget. Up until now, everything has been running within the GUI class.
I currently have the following code (included the important bits for brevity):
class Worker(QRunnable):
#pyqtSlot()
def run(self):
Ui.logentry(self, "Test")
class Ui(QtWidgets.QMainWindow):
def __init__(self):
super(Ui, self).__init__()
uic.loadUi(resourcepath('./pycdra.ui'), self)
self.outputlog = self.findChild(QtWidgets.QPlainTextEdit, 'outputlog')
self.button = self.findChild(QtWidgets.QPushButton, 'button')
self.button.clicked.connect(self.Button)
self.threadpool = QThreadPool()
self.logentry("Available threads: %d" % self.threadpool.maxThreadCount())
def Button(self):
worker = Worker()
self.threadpool.start(worker)
def logentry(self, returntext):
self.outputlog.appendPlainText(self.timestamp() + " " + str(returntext))
self.outputlog.repaint()
def timestamp(self):
import datetime
ts = datetime.datetime.now(tz=None).strftime("%Y-%m-%d %H:%M:%S"))
return ts
def applic():
app = QApplication(sys.argv)
window = Ui()
window.show()
sys.exit(app.exec_())
applic()
When I try running this, the GUI loads perfectly, but upon pushing button the Ui.logentry part of Worker returns the error: AttributeError: 'Worker' object has no attribute 'outputlog'
I attempted making logentry() and timestamp() global so they can access and be accessed by both classes, but the closest I got with the line Ui.self.outputlog.appendPlainText(self.timestamp() + " " + str(returntext)) was that 'Ui' object has no attribute 'self'.
What am I missing? Am I right in making them global or are there other means to do this?
There are various problems with your code. I'll try to address them in order of importance.
logentry is an instance method of Ui, which means that it requires an instance of Ui in order to be run; what you're trying to achieve would never work since that self refers to an instance of Worker, which clearly doesn't have any outputlog attribute (which is what logentry expects instead). Remember: the self argument is not there just for fun, it always refers to the object (the instance, for instance methods) the function is called from; self.logentry means "call the function logentry using self as first argument. Since, in your case, self refers to an instance of Ui, that explains your main issue, since that instance doesn't have that attribute.
None is exactly what its name says: nothing. The timestamp function will throw another AttributeError, since there's no strftime attribute in None. That is part of a datetime object, so you have to get a now object, then call its strftime function against it. In any case, the tz argument requires a tzinfo subclass.
pyqtSlot only works for Qt classes that inherit from QObject (such as QWidget). QRunnable isn't one of those classes. You can check the whole inheritance tree in the header of the documentation in each class: if there's a "Inherits:" field, go up until there's none. If you get a QObject inheritance at some point, then you can use slots and signals for that object, otherwise not. Consider that: QWidget also inherits from QObject; all Qt widgets inherit from QWidget; there are Qt classes that inherit from QObject but are not widgets.
Even ignoring all the above, access to UI elements is always forbidden from external threads (including QRunnable objects); while you can theoretically (but unreliably) get their properties, trying to set them will most likely cause a crash; in order to change something in a UI element, using signals is mandatory. Note that "access" also includes creation, and always results in a crash.
Calling repaint is a common (and wrong) attempt to solve the above issue; that's unnecessary, as properly setting widget properties (like using appendPlainText()) already results in a scheduled repaint on its own; there are very few cases for which repaint is actually necessary, and the rule of thumb is that if you're calling it you probably don't know what your doing or why your doing it. In any case, calling update() is always preferred, and it must always be called from the main UI thread anyway.
Using imports in a function is rarely required, as import statements should always be in the very beginning of the script; while there are cases for which imports can (or should) be done later or in a specific function, doing them in a function that is likely to be called often, makes using them there completely pointless. Also, datetime is part of the standard library, so importing it on demand will hardly affect performance (especially considering its "performance weight" against what a big library like Qt is compared to it).
When the ui is loaded from a .ui file (or a pyuic generated file), PyQt alread creates all widgets as instance attributes, so there's no need for findChild. Unfortunately there are a lot of tutorials that suggest that approach, and they are just completely and plain wrong. You can already access those widgets as self.outputlog and self.button right after uic.loadUi.
Function names (like variables and attributes) should always begin with a lower case letter, as only classes and constants should begin with upper cases (see the official Style Guide for Python Code). Also, object names should always explain what those object do (see "self-documenting code"): a function that does an "action" should have a verb; if it's named "Button" it doesn't tell me that it's going to do some processing, and that's not a very good thing.
A "main" function (like your applic) usually makes sense within the common if __name__ == '__main__': block, which ensures that that function doesn't get called in case the file gets imported instead of being directly run. (See this answer and the related question).
Since QRunnable doesn't inherit from QObject, we can create a QObject subclass that acts as a signal "proxy", and make it a member of the QRunnable instance. Then we must connect to that signal everytime we create a new Worker object.
Here is a revised version of your code, based on the above points.
import datetime
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.uic import loadUi
class WorkerSignals(QObject):
mySignal = pyqtSignal(str)
class Worker(QRunnable):
def __init__(self):
super().__init__()
self.signalProxy = WorkerSignals()
self.mySignal = self.signalProxy.mySignal
def run(self):
self.mySignal.emit("Test")
class Ui(QMainWindow):
def __init__(self):
super(Ui, self).__init__()
loadUi('./pycdra.ui', self)
self.button.clicked.connect(self.startWorker)
self.threadpool = QThreadPool()
self.logentry("Available threads: %d" % self.threadpool.maxThreadCount())
def startWorker(self):
worker = Worker()
worker.mySignal.connect(self.logentry)
self.threadpool.start(worker)
def logentry(self, returntext):
self.outputlog.appendPlainText(self.timestamp() + " " + str(returntext))
def timestamp(self):
ts = datetime.datetime.now()
return ts.strftime("%Y-%m-%d %H:%M:%S")
def applic():
import sys
app = QApplication(sys.argv)
window = Ui()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
applic()
I suggest you to carefully study the provided code and the differences with yours, and then do some careful, patient research both on the links given above and the following topics:
classes and instances, and the meaning of self;
python types (including None);
what is event driven programming and how it relates with graphical interfaces;
the most important Qt classes, QObject and QWidget, and all their properties and functions (yes, they are a lot);
general PyQt related topics
(most importantly, signals/slots and properties);
code styling and good practices;
Use the class' initialised form
class foo:pass
selfuotsideclass=foo()
Use variable selfoutsideclass as self

How do I create GUI classes for windows, which have subwindows/classes that can access to the main GUI's functions?

How do I create GUI classes for windows which have subwindows/classes that can access the main GUI's functions?
I have the below code which modifies the compiled .ui code from designer. What I want it to do is, when clicking the top-right "X", or using File -> Exit function, to close the window comprising the Window_SecondWindow class, and show the main window again--effectively calling the main window's show() from the subclass. I want to show only one window at a time.
When the code is run as-is, the Window_SecondWindow class hides, but immediately shows again, leading me to believe super is acting as self.
from PyQt5 import QtWidgets
from GUI import compiled_MainWindow
from GUI import compiled_SecondWindow
class Window_MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = compiled_MainWindow.Ui_MainWindow()
self.ui.setupUi(self)
self.ui.closeEvent = self.clicked_EXIT
# connect widgets
self.ui.Btn.clicked.connect(self.clicked_Btn)
self.ui.actionExit.triggered.connect(self.clicked_EXIT)
# add windows
self.SecondWindow = SecondWindow()
# more windows attached to main window
def clicked_Btn(self):
self.hide()
self.SecondWindow.show()
def clicked_EXIT(self):
self.close()
class Window_SecondWindow(Window_MainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
self.ui = compiled_SecondWindow.Ui_MainWindow()
self.ui.setupUi(self)
self.ui.closeEvent = self.clicked_EXIT
self.ui.actionExit.triggered.connect(self.clicked_EXIT)
def clicked_EXIT(self):
self.hide()
super().show()
Before answering your question, I'd like to address some important aspects.
First of all, never edit the generated code from pyuic to create your programs. They are intended to be used as imported modules, mostly as "resources": you import and integrate them into your code, but you should always leave them as they are. See the documentation on using Designer for more insight about this.
Be careful in overriding functions within the __init__: some functions are not "virtual" (thus, cannot be overwritten in such a way) and in some cases Qt always calls the base class function name anyway; just overwrite the method and call the base class implementation with super() if required. Also, closeEvent has the close event as a mandatory argument, and you have to add that to your overridden function, even if you don't use it (in the following examples I'm just using *args). That said, you should never use an overridden function as a slot that has a different argument, or viceversa.
Finally, you should not use capitalized names for attribute and variable names, as it is confusing and prone to errors (capitalization is mostly used for class names only, not their instancies).
Now, the answer
You are almost right, super() acts "as self", in the sense that it just calls the inherited show() method of the class against the instance. So, it calls the show method of Window_MainWindow, but since the instance is the second window, it's the same as doing Window_MainWindow.show(self), with self being the Window_SecondWindow instance; it is exactly as doing self.show().
There are two (and a half) possibilities.
The first, more obvious solution, is to give a reference of the main window instance to the second one:
class Window_MainWindow(QtWidgets.QMainWindow):
def __init__(self):
# ...
self.secondWindow = Window_SecondWindow()
self.secondWindow.mainWindow = self
class Window_SecondWindow(Window_MainWindow):
# ...
def clicked_EXIT(self, *args):
self.hide()
self.mainWindow.show()
Be aware that while, as #noras points out in the comment, you could set the main window as a parent in the init argument, but this only works as expected with QMainWindow and QDialog descendants; if the child widget is of any other kind, it will be shown inside the parent, not as a separate window.
The second (and more "Qt-wise correct") is to create a signal for the second class that is emitted when it's closed, and connect it in the main window so that it's shown again when that happens:
class Window_MainWindow(QtWidgets.QMainWindow):
def __init__(self):
# ...
self.secondWindow = Window_SecondWindow()
self.secondWindow.closed.connect(self.show)
class Window_SecondWindow(Window_MainWindow):
closed = QtCore.pyqtSignal()
def clicked_EXIT(self, *args):
self.hide()
self.closed.emit()
The second-and-a-half solution is to use an event filter:
class Window_MainWindow(QtWidgets.QMainWindow):
def __init__(self):
# ...
self.secondWindow = Window_SecondWindow()
self.secondWindow.installEventFilter(self)
def eventFilter(self, source, event):
if source == self.secondWindow and event.type() == QtCore.QEvent.Close:
self.show()
return super().eventFilter(source, event)

Creating an array of pyqtSignal

For a QThread I would like to create an array of pyqtSignal
class MyThread(QtCore.QThread):
Trigger = []
for i in range(0,10):
Trigger.append(QtCore.pyqtSignal(int))
def __init__(self, Function):
self.Function = Function
super(MyThread, self).__init__(None)
def run(self):
self.Function()
The main part of the following code looks like:
class Main(QtWidgets.QMainWindow):
def __init__(self):
self.MyQThread = MyThread(lambda: self.PrintTest(5))
def StartTestThread(self):
self.MyQThread.Trigger[0].connect(self.update_text)
self.MyQThread.start()
def PrintTest(self,InputValue):
for i in range (0,100):
print(InputValue*i)
time.sleep(0.2)
self.MyQThread.Trigger[0].emit(5)
def update_text(self, thread_no):
self.ui.MY_LISTWIDGET.addItem('123')
executing the StartTestThread leads to the following error
AttributeError: 'PyQt5.QtCore.pyqtSignal' object has no attribute
'connect'
If I initiliaize the pyqtSignal without being an array, it works.
What am I doing wrong? Thanks for the help in advance!
You cannot create a list of pyqtSignal(s).
Unfortunately the way pyqt implement signals use a bit of python magics, and the pyqtSignal objects are actually "converted" into pyqtBoundSignal when a QObject subclass (technically a class that have pyqtWrapperType as a metaclass) is loaded.
You can solve your problem in different ways:
1) Wrap the signal
I'm not 100% sure about this, but it's a modified version of your attempt:
class FooWrap(QObject):
Signal = QtCore.pyqtSignal(int)
class MyThread(QtCore.QThread):
Trigger = [FooWrap] * 10
2) Don't use a list
If the number of signals is fixed, just create them directly as signal1, signal2, signalN, then you can call them directly
If you can determine which signal to call only at runtime you can get the signal you need using the gettattr(object, name) function, for example:
getattr(self.MyQThread, 'signal' + str(n)).connect(self.update_text)
and
getattr(self.MyQThread, 'signal' + str(n)).emit(value)
3) Use only one signal
Use only one signal that emit two values, one identify the "step" in which the signal is emitted, and the second is your value, doing so the connected functions can decide what to do based on the "step" value.

first pyqt program fails

I am experience with Qt4, but now try to get into the programming of Qt with python.
It works mainly but now I come across a basic python program I did not figure out:
TypeError: setupSignalSlots() takes 1 positional argument but 2 were given
from PyQt4 import QtGui, uic
from PyQt4 import QtCore
class MainWindow(QtGui.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
uic.loadUi('MainWindow.ui', self)
self.show()
self.setupSignalSlots(self)
def setupSignalSlots(self):
self.ui.actionQuit.clicked.connect(OnQuitMainWindow)
#QtCore.pyqtSlot()
def OnQuitMainWindow():
print('quit')
QApplication.quit()
Besides that problem I wonder if the signal slot code is correct.
There are several things wrong with the code you posted.
Firstly, the OnQuitMainWindow slot needs a self argument. However, you do not need to pass this argument explicitly as python will do it automatically. Secondly, when you connect to the slot, you need to access it via self. Finally, quit is not a static method of QApplication, so you need to call it via an instance (e.g. qApp.quit()).
(And one other nit-pick: in python (and Qt, for that matter), it goes against convention to start attribute names with a capital letter).
After making these corrections, your code should look like this:
from PyQt4 import QtGui, uic
from PyQt4 import QtCore
class MainWindow(QtGui.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
uic.loadUi('MainWindow.ui', self)
self.show()
self.setupSignalSlots()
def setupSignalSlots(self):
self.actionQuit.triggered.connect(self.onQuitMainWindow)
#QtCore.pyqtSlot()
def onQuitMainWindow(self):
print('quit')
QtGui.qApp.quit()
UPDATE:
And one more thing I missed: the way you're using uic.loadUi means that the objects added in Qt Designer will end up as direct attributes of the instance of MainWindow. So it should be self.actionQuit, rather than self.ui.actionQuit. Also, since it appears that this object is a QAction, the signal should be triggered, rather than clicked.
You don't need to pass self to a method of a class; it is automatically done for you. Just do self.setupSignalSlots().

PyQt4.QtCore.pyqtSignal object has no attribute 'connect'

I'm having issues with a custom signal in a class I made.
Relevant code:
self.parse_triggered = QtCore.pyqtSignal()
def parseFile(self):
self.emit(self.parse_triggered)
Both of those belong to the class: RefreshWidget.
In its parent class I have:
self.refreshWidget.parse_triggered.connect(self.tabWidget.giveTabsData())
When I try to run the program, I get the error:
AttributeError: 'PyQt4.QtCore.pyqtSignal' object has no attribute 'connect'
Help?
Thanks in advance.
I had the same exact problem as you.
Try moving
self.parse_triggered = QtCore.pyqtSignal()
out of your constructor but inside your class declaration. So instead of it looking like this:
class Worker(QtCore.QThread):
def __init__(self, parent = None):
super(Worker, self).__init__(parent)
self.parse_triggered = QtCore.pyqtSignal()
It should look like this:
class Worker(QtCore.QThread):
parse_triggered = QtCore.pyqtSignal()
def __init__(self, parent = None):
super(Worker, self).__init__(parent)
This might not be at all what you are looking for, but it worked for me. I switched back to old-style signals anyways because I haven't found a way in new-style signals to have an undefined number or type of parameters.
You also get that error message if you fail to call super() or QObject.__init__() in your custom class.
A checklist for defining custom signals in a class in Qt in Python:
your class derives from QObject (directly or indirectly)
your class __init__ calls super() (or calls QObject.__init__() directly.)
your signal is defined as a class variable, not an instance variable
the signature (formal arguments) of your signal matches the signature of any slot that you will connect to the signal e.g. () or (int) or (str) or ((int,), (str,))
I have recently started working with PySide (Nokia's own version of PyQt), and saw the exact same behaviour (and solution) with custom new-style signals. My biggest concern with the solution was that using a class variable to hold the signal would mess things up when I have multiple instances of that class (QThreads in my case).
From what I could see, QtCore.QObject.__init__(self) finds the Signal variable in the class and creates a copy of that Signal for the instance. I have no idea what QObject.__init__() does, but the resulting Signal does proper connect(), disconnect() and emit() methods (and also a __getitem__() method), whereas the class Signal or standalone Signal variables created outside of a QObject-derived class do not have these methods and can't be used properly.
To use the signal/slot system you need to have a QObject inherited class.
Here is a simple example:
from PySide import QtCore
class LivingBeing(QtCore.QObject):
bornSignal = QtCore.Signal() # initialise our signal
def __init__(self,name):
QtCore.QObject.__init__(self) # initialisation required for object inheritance
self.bornSignal.connect(self.helloWorld) # connect the born signal to the helloworld function
self.name = name #
self.alive = False
def summonFromClay(self):
self.alive = True
self.bornSignal.emit() # emit the signal
def helloWorld(self):
print "Hello World !, my name is %s, this place is so great !" % self.name
# now try the little piece of code
if __name__ == '__main__':
firstHuman = LivingBeing('Adam')
firstHuman.summonFromClay()
I had the same problem.
I forgot that if a class uses Signals, then it must inherit from QObject. I was doing some re-factoring and did not pay attention to this.
Why do you connect directly to the signal, while you can do
self.connect(widget, SIGNAL('parse_triggered()'), listener.listening_method)?
where self is, for example, the form itself, and may be the same as listener

Categories