How to emit custom Events to the Event Loop in PyQt - python

I am trying to emit custom events in PyQt. One widget would emit and another would listen to events, but the two widgets would not need to be related.
In JavaScript, I would achieve this by doing
// Component 1
document.addEventListener('Hello', () => console.log('Got it'))
// Component 2
document.dispatchEvent(new Event("Hello"))
Edit: I know about signals and slots, but only know how to use them between parent and child. How would I this mechanism (or other mechanism) between arbitrary unrelated widgets?

In PyQt the following instruction:
document.addEventListener('Hello', () => console.log('Got it'))
is equivalent
document.hello_signal.connect(lambda: print('Got it'))
In a similar way:
document.dispatchEvent(new Event("Hello"))
is equivalent
document.hello_signal.emit()
But the big difference is the scope of the "document" object, since the connection is between a global element. But in PyQt that element does not exist.
One way to emulate the behavior that you point out is by creating a global object:
globalobject.py
from PyQt5 import QtCore
import functools
#functools.lru_cache()
class GlobalObject(QtCore.QObject):
def __init__(self):
super().__init__()
self._events = {}
def addEventListener(self, name, func):
if name not in self._events:
self._events[name] = [func]
else:
self._events[name].append(func)
def dispatchEvent(self, name):
functions = self._events.get(name, [])
for func in functions:
QtCore.QTimer.singleShot(0, func)
main.py
from PyQt5 import QtCore, QtWidgets
from globalobject import GlobalObject
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
button = QtWidgets.QPushButton(text="Press me", clicked=self.on_clicked)
self.setCentralWidget(button)
#QtCore.pyqtSlot()
def on_clicked(self):
GlobalObject().dispatchEvent("hello")
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
GlobalObject().addEventListener("hello", self.foo)
self._label = QtWidgets.QLabel()
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self._label)
#QtCore.pyqtSlot()
def foo(self):
self._label.setText("foo")
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w1 = MainWindow()
w2 = Widget()
w1.show()
w2.show()
sys.exit(app.exec_())

Related

PyQt5 Access MainWindow from Another Window

I am newbie and in that project I am stuck at one point. How can I access functions in MainWindow class from another Window.
For example I need to access Main.list_refresh when Child_EditAccount.btn_EDIT_ACCOUNT_3.clicked . I tried something like signal & slot but doesn't work. I maked 3 different Window with pyQt Designer and I need to link the three together.
new.py
# Designs
from design import Ui_MainWindow
from design_addAccount import Ui_MainWindow_Add
from design_editAccount import Ui_MainWindow_Edit
# Modules
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import pyqtSignal, pyqtSlot
import sqlite3
con = sqlite3.connect('shorts.sqlite3')
cur = con.cursor()
class Main(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(Main, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.account_process = AccountProcess()
self.ui.lst_accounts.itemClicked.connect(self.account_process.edit_account)
self.ui.btn_delete_account.clicked.connect(self.account_process.delete_account)
self.ui.btn_edit_account.clicked.connect(self.account_process.edit_account)
self.ui.btn_add_account.clicked.connect(self.account_process.add_account)
self.ui.btn_refresh.clicked.connect(self.list_refresh)
self.refresh_trigger = Child_EditAccount()
self.refresh_trigger.buttonClicked.connect(self.list_refresh)
def list_refresh(self):
self.ui.lst_accounts.clear()
for row in cur.execute("SELECT * FROM users"):
self.ui.lst_accounts.addItem('%s' % (str(row[3])))
class Child_AddAccount(QtWidgets.QMainWindow,Ui_MainWindow_Add):
def __init__(self, parent=None):
super(Child_AddAccount, self).__init__()
self.ui = Ui_MainWindow_Add()
self.ui.setupUi(self)
class Child_EditAccount(QtWidgets.QMainWindow,Ui_MainWindow_Edit):
buttonClicked = pyqtSignal()
def __init__(self, parent=None):
super(Child_EditAccount, self).__init__()
self.ui = Ui_MainWindow_Edit()
self.ui.setupUi(self)
def edit_infos(self, username, password, nickname, id):
self.id = id
self.ui.txtBox_username_3.insert(username)
self.ui.txtBox_password_3.insert(password)
self.ui.txtBox_nickname_3.insert(nickname)
self.ui.btn_EDIT_ACCOUNT_3.clicked.connect(self.update_by_id)
def update_by_id(self):
cur.execute('UPDATE users SET username="%s", password="%s", nickname="%s" WHERE id=%s' % (self.ui.txtBox_username_3.text(), self.ui.txtBox_password_3.text(), self.ui.txtBox_nickname_3.text(), self.id))
con.commit()
self.buttonClicked.emit()
class AccountProcess(QtCore.QObject):
def add_account(self):
self.child_add = Child_AddAccount()
self.child_add.show()
print('Pressed edit button')
def delete_account(self):
print('Pressed delete button')
def edit_account(self, item):
self.child_edit = Child_EditAccount()
for i in cur.execute(f"SELECT * FROM users WHERE nickname=\"{item.text()}\";"):
self.child_edit.edit_infos(str(i[1]), str(i[2]), str(i[3]), str(i[0]))
self.child_edit.show()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
main = Main()
main.show()
sys.exit(app.exec_())
You can use a pyqtSignal to do what you want.
in the Child_EditAccount class add a pyqtsignal
class Child_EditAccount(QtWidgets.QMainWindow,Ui_MainWindow_Add):
buttonClicked = pyqtSignal()
def __init__(self, parent=None):
before the __init__, then when you need to trigger the function use
buttonClicked.emit()
in the function where the button is used in the Child_EditAccount class
Then in the main window, you can create a connection to a function via the pyqtsignal
self.Child_EditAccount.buttonClicked.connect(self.myfunction)
in the __init__ of the main class. where self.Child_EditAccount is the instance of your Child_EditAccount class and self.function is the function you want to trigger Main.list_refresh
You can also create a signal from QtDesigner itself when you create the Ui_MainWindow_Edit
https://doc.qt.io/qt-5/designer-connection-mode.html
be carefull, Child_EditAccount inherite from Ui_MainWindow_Add instead of Ui_MainWindow_Edit probably.
____
what you can also do is link the sigal of the button directly in the main program as in this little example
# Modules
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import pyqtSignal, pyqtSlot
class Main2(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(Main2, self).__init__()
self.centralWidget = QtWidgets.QWidget()
self.setCentralWidget(self.centralWidget)
self.but= QtWidgets.QPushButton('tdvfdbt')
self.l = QtWidgets.QHBoxLayout()
self.l.addWidget(self.but)
self.centralWidget.setLayout(self.l)
class Main(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(Main, self).__init__()
self.win = Main2()
self.a = self.win.show()
self.win.but.clicked.connect(self.myfunction)
def myfunction(self):
print('called from sub window')
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
ex = Main(app)
ex.setWindowTitle('Model micro GUI')
# ex.showMaximized()
ex.show()
sys.exit(app.exec())
if you know the name of the button widget of the Child_EditAccount class you can link it via
self.Child_EditAccount.btn_EDIT_ACCOUNT_3.clicked.connect(self.myfunction)
##______
Put the pystsignal in the AccountProcess class
class AccountProcess(QtCore.QObject):
buttonClicked = pyqtSignal()
def add_account(self):
self.child_add = Child_AddAccount()
self.child_add.show()
print('Pressed edit button')
def delete_account(self):
print('Pressed delete button')
def edit_account(self, item):
self.child_edit = Child_EditAccount()
for i in cur.execute(f"SELECT * FROM users WHERE nickname=\"{item.text()}\";"):
self.child_edit.edit_infos(str(i[1]), str(i[2]), str(i[3]), str(i[0]))
self.child_edit.buttonClicked.connect(self.EmitAgain)
self.child_edit.show()
def EmitAgain(self):
self.buttonClicked.emit()
Then use it in the main class
class Main(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(Main, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.account_process = AccountProcess()
self.account_process.buttonClicked.connect(self.list_refresh)

Pyqt5 - how to go back to hided Main Window from Secondary Window?

If I click Back from the second window, the program will just exit. How do I go back to mainwindow in this case? I assume I will need some more code in that clickMethodBack function.
import os
import PyQt5
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QMainWindow, QWidget, QLabel, QPushButton
import time
from PyQt5.QtCore import QSize
class GUI_Window():
def __init__(self):
self.main_window()
return
def main_window(self):
app = PyQt5.QtWidgets.QApplication(sys.argv)
self.MainWindow = MainWindow_()
self.MainWindow.show()
app.exec_()
return
class MainWindow_(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.TestAButton = QPushButton("TestA", self)
self.TestAButton.clicked.connect(self.TestA_clickMethod)
self.TestAButton.move(20, 0)
self.CloseButton = QPushButton("Close", self)
self.CloseButton.clicked.connect(self.Close_clickMethod)
self.CloseButton.move(20, 40)
self.TestingA = TestA_MainWindow()
def TestA_clickMethod(self):
self.TestAButton.setEnabled(False)
time.sleep(0.2)
self.TestingA.show()
self.hide()
try:
if self.TestingA.back == True:
self.show()
except:
None
def Close_clickMethod(self):
self.Test_Choice = 'Exit'
self.close()
class TestA_MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setMinimumSize(QSize(980,700))
self.setWindowTitle("TestA")
self.Back_Button = False
self.closeButton = QPushButton("Close", self)
self.closeButton.clicked.connect(self.clickMethodClose)
self.returnButton = QPushButton("Back", self)
self.returnButton.clicked.connect(self.clickMethodBack)
self.returnButton.move(0,30)
def clickMethodClose(self):
self.Back_Button = False
self.close()
def clickMethodBack(self):
self.returnButton.setEnabled(False)
time.sleep(0.5)
self.back = True
self.close()
# Run if Script
if __name__ == "__main__":
main = GUI_Window() # Initialize GUI
Your code has two very important issues.
you're using a blocking function, time.sleep; Qt, as almost any UI toolkit, is event driven, which means that it has to be able to constantly receive and handle events (coming from the system or after user interaction): when something blocks the event queue, it completely freezes the whole program until that block releases control;
you're checking for the variable too soon: even assuming the sleep would work, you cannot know if the window is closed after that sleep timer has ended;
The solution is to use signals and slots. Since you need to know when the second window has been closed using the "back" button, create a custom signal for the second window that will be emitted whenever the function that is called by the button is closed.
from PyQt5 import QtCore, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
central = QtWidgets.QWidget()
layout = QtWidgets.QHBoxLayout(central)
self.testButton = QtWidgets.QPushButton('Test A')
self.closeButton = QtWidgets.QPushButton('Close')
layout.addWidget(self.testButton)
layout.addWidget(self.closeButton)
self.setCentralWidget(central)
self.testButton.clicked.connect(self.launchWindow)
self.closeButton.clicked.connect(self.close)
def launchWindow(self):
self.test = TestA_MainWindow()
self.test.backSignal.connect(self.show)
self.hide()
self.test.show()
class TestA_MainWindow(QtWidgets.QWidget):
backSignal = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
layout = QtWidgets.QHBoxLayout(self)
self.closeButton = QtWidgets.QPushButton('Close')
self.backButton = QtWidgets.QPushButton('Back')
layout.addWidget(self.closeButton)
layout.addWidget(self.backButton)
self.closeButton.clicked.connect(self.close)
self.backButton.clicked.connect(self.goBack)
def goBack(self):
self.close()
self.backSignal.emit()
def GUI_Window():
import sys
app = QtWidgets.QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.show()
sys.exit(app.exec_())
if __name__ == '__main__':
GUI_Window()
Notes:
I removed the GUI_Window class and made a function, as using a class for that is not really useful;
you should always prefer layout managers instead of setting manual geometries;
widgets should not be added to a QMainWindow as direct children, and a central widget should always be used (see the creation and use of central in the example); read more about it in the documentation;
only classes and constants should be capitalized, while variables, attributes and functions should always have names starting with a lowercase letter;

Catch the mouse event

Don't know exactly how to give the parameters of QMouseEvent class. Should I create new class to implement a QMouseEvent into my QTextEdit?
class Test(QMainWindow):
def __init__(self):
super().__init__()
self.txt = QTextEdit(self)
self.txt.setMouseTracking(True)
self.txt.mouseReleaseEvent(QMouseEvent())
class Test2(QTextEdit):
def __init__(self):
super().__init__()
def mouseReleaseEvent(self, e):
print("text edit is clicked")
ui = Test()
ui.show()
Since many times it is asked how to detect the events that affect a widget then in this answer I will detail the solution and it will be used as a canonical answer for future questions.
To detect an event from a widget there are several solutions:
- Override a method
If the widget has a method that handles that event then an option is to override that method and associate it with a signal so that other objects can be notified.
In the particular case of the mouse release event, this is handled by the mouseReleaseEvent method.
from PyQt5 import QtCore, QtWidgets
class TextEdit(QtWidgets.QTextEdit):
released = QtCore.pyqtSignal()
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
self.released.emit()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.textedit = TextEdit()
self.textedit.released.connect(self.handle_released)
self.setCentralWidget(self.textedit)
#QtCore.pyqtSlot()
def handle_released(self):
print("released")
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
- Use an event filter
Qt allows you to monitor the events using an event filter, so you can take advantage of this feature to emit a signal in a similar way to the previous solution.
In the case of classes that inherit from QAbstractScrollArea, the mouse methods are transmitted to the viewport, so that object must be monitored.
from PyQt5 import QtCore, QtWidgets
class ReleaseFilter(QtCore.QObject):
released = QtCore.pyqtSignal()
def __init__(self, widget):
super().__init__(widget)
self._widget = widget
self.widget.installEventFilter(self)
#property
def widget(self):
return self._widget
def eventFilter(self, obj, event):
if obj is self.widget and event.type() == QtCore.QEvent.MouseButtonRelease:
self.released.emit()
return super().eventFilter(obj, event)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.textedit = QtWidgets.QTextEdit()
rf = ReleaseFilter(self.textedit.viewport())
rf.released.connect(self.handle_released)
self.setCentralWidget(self.textedit)
#QtCore.pyqtSlot()
def handle_released(self):
print("released")
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())

How do I get a signal from a QWiget to change get object?

Hello I have a QWidget and If I click on it, I want to get the object (the QWidget Element I clicked on) is there anyway to do that?
I already found some code but I only get the MouseClickEvent from
self.widget_34.mouseReleaseEvent = lambda event: self.myfunction(event)
Although the solution offered by #Cin is interesting, it has a serious problem: it cancels the mousePressEvent of the widget, so the widget loses the behavior it could have when the widget is pressed, for example the button no longer emits the clicked signal, other widget also They will have the same problem.
A less intrusive solution is to use eventFilter:
import sys
import weakref
from PyQt5 import QtCore, QtWidgets
class ClickListener(QtCore.QObject):
clicked = QtCore.pyqtSignal(QtWidgets.QWidget)
def addWidget(self, widget, other_widget=None):
if not hasattr(self, "_widgets"):
self._widgets = {}
widget.installEventFilter(self)
self._widgets[widget] = widget if other_widget is None else other_widget
weakref.ref(widget, self.removeWidget)
def eventFilter(self, obj, event):
if (
obj in self._widgets
and event.type() == QtCore.QEvent.MouseButtonPress
):
self.clicked.emit(self._widgets[obj])
return super(ClickListener, self).eventFilter(obj, event)
def removeWidget(self, widget):
if hasattr(self, "_widgets"):
if widget in self._widgets:
del self._widgets[widget]
class App(QtWidgets.QWidget):
def __init__(self):
super().__init__()
button = QtWidgets.QPushButton("Press Me")
label = QtWidgets.QLabel("Stack Overflow")
spinBox = QtWidgets.QSpinBox()
te = QtWidgets.QTextEdit()
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(button)
lay.addWidget(label)
lay.addWidget(spinBox)
lay.addWidget(te)
listener = ClickListener(self)
listener.clicked.connect(self.onClicked)
listener.addWidget(button)
listener.addWidget(label)
listener.addWidget(spinBox.lineEdit(), spinBox)
listener.addWidget(te.viewport(), te)
def onClicked(self, obj):
print("Clicked, from", obj)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
ex = App()
ex.show()
sys.exit(app.exec_())
I am not sure this will be a proper solution or not but I think, you can use the partial method of functools module. A collable object can be treated as a function for the purposes of this module. Here you can see my example,
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabel
import functools
class App(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(200,200,200,200)
self.button = QPushButton('Button', self)
self.button.move(50,50)
self.label = QLabel(self)
self.label.setText("Label")
self.label.move(100,100)
self.items = [self.button, self.label]
for i in self.items:
i.mousePressEvent = functools.partial(self.getClickedItem, source_object=i)
self.show()
def getClickedItem(self, event, source_object=None):
print("Clicked, from", source_object)
#item text
#print(source_object.text())
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())

PySide (Qt) signal not reaching my slot

I have this simplified code that doesn't work and I can't understand why... I expect MySlot.slt() to be called every time i press a key in my QTextEdit but it doesn't! Could you please have a look?
#!/usr/bin/env python2
import sys
from PySide import QtGui, QtCore
class MySlot(object):
def __init__(self, qte):
qte.textChanged.connect(self.soc)
def slt(self):
print("got signal")
class MainWid(QtGui.QWidget):
def __init__(self, parent=None):
super(MainWid, self).__init__(parent)
self.initgui()
def initgui(self):
lay = QtGui.QVBoxLayout()
txt = QtGui.QTextEdit(self)
MySoc(txt)
lay.addWidget(txt)
self.setLayout(lay)
self.show()
def main():
app = QtGui.QApplication(sys.argv)
wid = MainWid()
sys.exit(app.exec_())
if __name__=="__main__":
main()
Your MySoc object in initgui has local scope and is therefore destroyed at the end of initgui.
Assign the object to a variable:
...
self.soc = MySoc(txt);
...
and you will see the "got signal" output each time you press a key.

Categories

Resources