PySide signals alternative using events not working - python

I did something similar to the answer here.
The GUI is simple, you click a button which starts a thread that instead of emitting signals it sends events. The events cause a label to change text.
Here's the code:
from PySide.QtGui import *
from PySide.QtCore import *
import sys, time
class MyEvent(QEvent):
def __init__(self, message):
super().__init__(QEvent.User)
self.message = message
class MyThread(QThread):
def __init__(self, widget):
super().__init__()
self._widget = widget
def run(self):
for i in range(10):
app.sendEvent(self._widget, MyEvent("Hello, %s!" % i))
time.sleep(.1)
class MyReceiver(QWidget):
def __init__(self, parent=None):
super().__init__()
layout = QHBoxLayout()
self.label = QLabel('Test!')
start_button = QPushButton('Start thread')
start_button.clicked.connect(self.startThread)
layout.addWidget(self.label)
layout.addWidget(start_button)
self.setLayout(layout)
def event(self, event):
if event.type() == QEvent.User:
self.label.setText(event.message)
return True
return False
def startThread(self):
self.thread = MyThread(self)
self.thread.start()
app = QApplication(sys.argv)
main = MyReceiver()
main.show()
sys.exit(app.exec_())
The problem is that, only the first event get processed by MyReceiver, then the widget freezes!.
Any clues?. Thanks

The behaviour of the event method of QWidget is altered in your code: you should
let the base class decide on what to do with events, not returning False if it is
not a custom event. Do it like this:
def event(self, event):
if event.type() == QEvent.User:
self.label.setText(event.message)
return True
return QWidget.event(self, event)
This fixes your problem.
Also, you may prefer to emit a Qt signal from the thread, and have it connected in
your widget to some method to change the label - signals and slots are thread-safe
in Qt 4 (contrary to Qt 3). It will achieve the same result.

Related

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_())

PyQt Keypress signal to multiple slots in different classes

I have a custom table class, where I want to tie the Return key to advancing the edit focus to the next row, as follows:
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class App(QWidget):
def __init__(self):
super().__init__()
# self.shortcut = QShortcut(QKeySequence('Return'), self)
# self.shortcut.activated.connect(self.plotter)
table = Table(1, 1)
layout = QHBoxLayout()
layout.addWidget(table)
self.setLayout(layout)
self.show()
def plotter(self):
print('I am plotting')
class Table(QTableWidget):
def __init__(self, rows, cols):
super().__init__(1, 1)
self.shortcut = QShortcut(QKeySequence('Return'), self)
self.shortcut.activated.connect(self.advance)
def advance(self):
print('I am advancing')
if __name__ == '__main__':
app = QApplication(sys.argv)
window = App()
sys.exit(app.exec_())
Which works fine. But I also want to tie that Return key to a plotting routine in the application in which the table resides. The net effect of this would be hitting the Return key to accept the table edit and advance the table cell focus, then updating the plot determined by those table values.
Implementing the above code in the same fashion for the plotting routine in the main application class also works well. But I can't implement them both at the same time. When I tie the Return key to two separate slots in two separate classes, nothing happens when I hit Return. Any ideas? Thanks.
The activated signal does not trigger since the sequence is ambiguous as the docs points out, so you should use the activatedAmbiguously signal
void QShortcut::activatedAmbiguously() When a key sequence is
being typed at the keyboard, it is said to be ambiguous as long as it
matches the start of more than one shortcut.
When a shortcut's key sequence is completed, activatedAmbiguously() is
emitted if the key sequence is still ambiguous (i.e., it is the start
of one or more other shortcuts). The activated() signal is not emitted
in this case.
See also activated().
(emphasis mine)
But unfortunately it will not work since it shoots one at a time, that is to say first the shortcut of the parent(QWidget) and then the shortcut of the children(QTableWidget), who does not meet your requirement.
So in this case a possible solution is to connect the same signal to several slots:
class App(QWidget):
def __init__(self):
super().__init__()
table = Table(1, 1)
layout = QHBoxLayout(self)
layout.addWidget(table)
self.show()
self.shortcut = QShortcut(QKeySequence("Return"), self)
self.shortcut.activated.connect(self.plotter)
self.shortcut.activated.connect(table.advance)
def plotter(self):
print("I am plotting")
class Table(QTableWidget):
def __init__(self, rows, cols):
super().__init__(1, 1)
def advance(self):
print("I am advancing")
Another alternative is to use an eventFilter instead of the QShortcut, since the eventfilter will not consume the event as the QShorcut so all the filters will be notified although it may be inconvenient that the widget does not have the focus and therefore does not receive the event of the keyboard:
class ReturnListener(QObject):
pressed = 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, o, e):
if self.widget is o and e.type() == QEvent.KeyPress:
if e.key() == Qt.Key_Return:
self.pressed.emit()
return super().eventFilter(o, e)
class App(QWidget):
def __init__(self):
super().__init__()
table = Table(1, 1)
layout = QHBoxLayout(self)
layout.addWidget(table)
listener = ReturnListener(self)
listener.pressed.connect(self.plotter)
self.show()
def plotter(self):
print("I am plotting")
class Table(QTableWidget):
def __init__(self, rows, cols):
super().__init__(1, 1)
listener = ReturnListener(self)
listener.pressed.connect(self.advance)
def advance(self):
print("I am advancing")
In conclusion, the best solution is to only have a QShorcut connecting all the slots to the same signal.

Signals or events for QMenu tear-off

Is there a signal/event I can make use of for the QMenu tear-off?
I have a QMenu subclass in which it has .setTearOffEnabled(True), but I would like to set this tear-off to always be on the top if the user clicks on the tear-off 'bar'.
I am unable to utilise QtCore.Qt.WindowStaysOnTopHint, as this will result in my menu already being in the tear-off state.
For example: if my main tool area is bigger than the tear-off, and I click on my main tool, the tear-off window will be behind it.
In the following code the clicked signal is emitted when the tear off (dotted lines) is pressed:
import sys
from PyQt5 import QtCore, QtWidgets
class Menu(QtWidgets.QMenu):
clicked = QtCore.pyqtSignal()
def mouseReleaseEvent(self, event):
if self.isTearOffEnabled():
tearRect = QtCore.QRect(
0,
0,
self.width(),
self.style().pixelMetric(
QtWidgets.QStyle.PM_MenuTearoffHeight, None, self
),
)
if tearRect.contains(event.pos()):
self.clicked.emit()
QtCore.QTimer.singleShot(0, self.after_clicked)
super(Menu, self).mouseReleaseEvent(event)
#QtCore.pyqtSlot()
def after_clicked(self):
tornPopup = None
for tl in QtWidgets.QApplication.topLevelWidgets():
if tl.metaObject().className() == "QTornOffMenu":
tornPopup = tl
break
if tornPopup is not None:
print("This is the tornPopup: ", tornPopup)
tornPopup.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QMainWindow(parent=None)
menu = Menu("Menu", tearOffEnabled=True)
menu.clicked.connect(lambda: print("clicked"))
w.menuBar().addMenu(menu)
for i in range(5):
action = QtWidgets.QAction("action{}".format(i), w)
menu.addAction(action)
w.show()
sys.exit(app.exec_())
When a menu is torn off, it is hidden and Qt replaces it with a copy created from an internal subclass of QMenu. So to set the WindowStaysOnTopHint on a torn off menu, you would first need to find a way to get a reference to it. One way to do this would be to set an event-filter on the application object and watch for child events of the right type:
class MenuWatcher(QtCore.QObject):
def __init__(self, parent=None):
super().__init__(parent)
QtWidgets.qApp.installEventFilter(self)
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.ChildAdded and
event.child().metaObject().className() == 'QTornOffMenu'):
event.child().setWindowFlag(QtCore.Qt.WindowStaysOnTopHint)
return super().eventFilter(source, event)
This class will operate on all torn-off menus created in the application.
However, if the event-filtering was done by the source menu class, its own torn-off menu could be identified by comparing menu-items:
class Menu(QtWidgets.QMenu):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setTearOffEnabled(True)
QtWidgets.qApp.installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.ChildAdded:
child = event.child()
if (child.metaObject().className() == 'QTornOffMenu' and
all(a is b for a, b in zip(child.actions(), self.actions()))):
child.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint)
return super().eventFilter(source, event)

How to override isclick() sigal in Pyqt?

I have a class BooleanButton contains extra boolean flag to toggle every click. After each click, I want it to emit a signal and the slot will receive the boolean flag. I just write the following, but of course, it won't work.
class BooleanButton(QPushButton):
def __init__(self, name):
QPushButton.__init__(self, name)
self.bool = False
def clicked(self, bool):
self.bool = not self.bool
self.emit(self.bool)
After creating the object, it connects to a slot. When I click this button, a swapping true-false signal will send to the slot.
bool_btn.isclicked[bool].connect(widget.func)
Thanks.
First, don't call a method clicked, that will hide the buttons clicked signal.
If you want to define a new signal, you need to do so using QtCore.pyqtSignal, then you can connect the clicked singal to a slot that will in turn emit your custom signal. Example:
class BooleanButton(QPushButton):
isclicked = pyqtSignal(bool)
def __init__(self, name):
QPushButton.__init__(self, name)
self.bool = False
self.clicked.connect(self.on_clicked)
def on_clicked(self, bool):
self.bool = not self.bool
self.isclicked.emit(self.bool)
As three_pineapples said, QPushButton comes with this feature built-in. Here's a simple example illustrating this behaviour.
from PyQt4 import QtGui, QtCore
class MyWidget(QtGui.QWidget):
def __init__(self, parent=None):
super(MyWidget, self).__init__(parent)
self.button = QtGui.QPushButton("Click me", self)
self.button.setCheckable(True)
self.lineEdit = QtGui.QLineEdit(self)
self.button.clicked.connect(self.onClicked)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.button)
layout.addWidget(self.lineEdit)
def onClicked(self, checked):
if checked:
self.lineEdit.setText("Button checked")
else:
self.lineEdit.setText("Button unchecked")
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
widget = MyWidget()
widget.show()
sys.exit(app.exec_())
So your BooleanButton is actually just a QPushButton.

Categories

Resources