PyQt5: How do I "collect" or "receive" an emitted signal? - python

In my code, I have 2 classes in 2 separate files. I have a signal testSignal, a button pushButton and I connected the button like this:
testSignal = QtCore.pyqtSignal()
pushButton.pressed.connect(buttonPressed)
def buttonPressed(self):
testSignal.emit()
Now what I want to do is "receive" the emitted signal in the class/file, but I'm somewhat in the dark on how emit() actually works. Does anyone have any links for guides for the emit() function or can help somehow?
Thanks

[Py]Qt signals must be declared at the class level, as they become "active" only whenever a new class instance is created. Also, they can only be used for classes that inherit from QObject (including QWidget subclasses), and using them with standard python object classes will not work.
class SomeWindow(QtWidgets.QWidget):
testSignal = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
# ...
pushButton.pressed.connect(self.buttonPressed)
# connect the custom signal with an *instance method*
self.testSignal.connect(self.someInstanceFunction)
def buttonPressed(self):
# emit the signal
self.testSignal.emit()
def someInstanceFunction(self):
print('hello from the instance!')
def someAnonymousFunction():
print('hello from outside!')
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = SomeWindow()
# connect the signal with an anonymous function
window.testSignal.connect(someAnonymousFunction)
sys.exit(app.exec_())
Obviously the example above doesn't make a lot of sense, as custom signals are normally used to communicate between instances of (possibly different) classes, but that's just for explanation purposes. Also, you could "concatenate" signals (as long as their signature is compatible):
class SomeWindow(QtWidgets.QWidget):
testSignal = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
# ...
# emit the testSignal when the button emits its pressed signal
pushButton.pressed.connect(self.testSignal)
self.testSignal.connect(self.someInstanceFunction)
# ...
Note that if you want to do something when the user clicks a button, you should use clicked(), not the pressed() one and the difference is very important: it's common convention that a button is considered clicked only when the user presses and releases the mouse button while still in the button area, so that the "click" can be avoided if the mouse button is released outside it. If you connect to pressed(), the signal will be emitted as soon as the mouse button is just pressed down on the button, which is not considered a standard behavior.

Related

How do I get the text of an QLineEdit into a method?

I'm still not able to understand how to properly connect Qt_pushButton or Qt_LineEdit to methods. I would be so glad to get a explanation which even I do truly understand...
I've put together a pretty basic UI with Qt Designer. It contains a lineEdit called "lineEditTest" which I can indeed change by typing
self.lineEditTest.setText("changed Text")
However, I'm totally stuck with getting the text which the user entered back into my program. I would like to automatically submit the entered text into my function which sets a var to this value and returns it into my UI class. QLineEdit's signal editingFinished sounds perfect for that I guess? But it won't pass the text which the user entered into my function.
QLineEdit does have a property called "text" right? So it seems logical to me that I just have to pass another arg - apart from self - to my function called text.
My code does look like this but it obviously won't work at all:
from PyQt5 import QtWidgets, uic
import sys
class Ui(QtWidgets.QMainWindow):
def __init__(self):
super(Ui, self).__init__()
uic.loadUi('test.ui', self)
self.lineEditTest.setText("test")
self.lineEditTest.editingFinished.connect(self.changeText(text))
self.show()
def changeText(self):
currentText = text
print (currentText)
return currentText
app = QtWidgets.QApplication(sys.argv)
window = Ui()
app.exec_()
This will just crash with:
NameError: name 'text' is not defined`
The problem seems to be that the OP doesn't understand the logic of the signals and slots (I recommend you check here). The signals are objects that emit information, and the slots are functions (generally callable) that are connected to the signals to receive that information. And the information transmitted by the signal depends on each case, for example if you check the docs of editingFinished signal:
void QLineEdit::editingFinished() This signal is emitted when the
Return or Enter key is pressed or the line edit loses focus. Note that
if there is a validator() or inputMask() set on the line edit and
enter/return is pressed, the editingFinished() signal will only be
emitted if the input follows the inputMask() and the validator()
returns QValidator::Acceptable.
That signal does not send any information so do not expect to receive any information except knowing that the edition has ended by the user. So how can I get the text? Well, through the text() method of QLineEdit:
class Ui(QtWidgets.QMainWindow):
def __init__(self):
super(Ui, self).__init__()
uic.loadUi("test.ui", self)
self.lineEditTest.setText("test")
self.lineEditTest.editingFinished.connect(self.changeText)
self.show()
def changeText(self):
text = self.lineEditTest.text()
print(text)
And how to do if the signal sends information? Then the slot (the function) that this connected will have as arguments to that information, for example if you use the textChanged signal that is emitted every time the text of the QLineEdit is changed, it should be as follows:
class Ui(QtWidgets.QMainWindow):
def __init__(self):
super(Ui, self).__init__()
uic.loadUi("test.ui", self)
self.lineEditTest.setText("test")
self.lineEditTest.textChanged.connect(self.changeText)
self.show()
def changeText(self, text):
print(text)
# or
# text = self.lineEditTest.text()
# print(text)
The way you're binding your callback isn't correct. When you bind a callback (and this is true for other frameworks, not just for binding callbacks in PyQt), you want to bind the function which should be triggered when a certain event occurs. That's not what you're doing - you're explicitly invoking the callback self.changeText(text) (note: the parentheses invoke the function). So, you're invoking (calling) the function, evaluating it and placing the return value in ...editingFinished.connect().
Don't explicitly invoke the function. Just pass the function's name. This makes sense if you think about it: we're passing a callable object to connect - this is the function which should be called at a later point in time, when the editingFinished event occurs.
You also don't need to pass lineEditTest's text into the callback, since you can just get whatever text is in the widget via self.lineEditTest.text() from inside the callback.
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import QMainWindow
class MainWindow(QMainWindow):
def __init__(self):
from PyQt5 import uic
super(MainWindow, self).__init__()
uic.loadUi("mainwindow.ui", self)
self.lineEditTest.setPlaceholderText("Type something...")
self.lineEditTest.editingFinished.connect(self.on_line_edit_finished_editing)
#pyqtSlot()
def on_line_edit_finished_editing(self):
text = self.lineEditTest.text()
print(f"You finished editing, and you entered {text}")
def main():
from PyQt5.QtWidgets import QApplication
application = QApplication([])
window = MainWindow()
window.show()
return application.exec()
if __name__ == "__main__":
import sys
sys.exit(main())

Access items and attributes across multiple windows

i have a main GUI-Window from which i open a new Window (FCT-popup) with a buttonclick:
class MainWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.ui = Ui_MainWindow() # sets ui = to the main window from the ui-file
self.ui.setupUi(self)
[...]
def enter_fct_results(self):
self.FCTpopup = FCT_Window()
self.FCTpopup.show()
In the Window i have a QTable to fill and a button to submit the data and close the window:
class FCT_Window(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.ui = Ui_FCT_Window()
self.ui.setupUi(self)
[...]
self.ui.pushButton_submitFCT.clicked.connect(lambda: MainWindow.store_fct_data(MainWindow, self.on_submit()[0]))
def on_submit(self): # event when user clicks
fct_nparray = np.zeros((self.ui.tableFCTinputs.rowCount(), self.ui.tableFCTinputs.columnCount()))
for j in range(self.ui.tableFCTinputs.columnCount()):
for i in range(self.ui.tableFCTinputs.rowCount()):
fct_nparray[i, j] = float(self.ui.tableFCTinputs.item(i, j).text())
return fct_nparray, lambda: self.close()
self.ui.pushButton_submitFCT.clicked.connect(lambda: MainWindow.store_fct_data(MainWindow, self.on_submit()[0]))
The receiving function iin the main window looks like ths:
def store_fct_data(self, data):
self.fct_data = data
Now i just want to understand how i can make either the mainwindow or the pushbutton which opens the 2nd window disabled. Disabling inside enter_fct_results() works, but if i want to enable it again with either store_fct_data or on_submit provides errors like this:
self.ui.pushButton_FCTresults.setEnabled(1)
self.ui.pushButton_submitFCT.clicked.connect(lambda: MainWindow.store_fct_data(MainWindow, self.on_submit()[0]))
AttributeError: type object 'MainWindow' has no attribute 'ui'
I dont think i have understood it here how to deal with multiple windows and stuff. For example how would i change a the color of a button in the main window by using a button in window2. How do i access the widgets? if i am inside the same Window i do that easily by
self.ui.pushbutton.setText("New Text")
I dont get how to access items and attributes across Windows :( Can you help me?
Access to attributes of another instance
There is a fundamental difference between disabling the button of the second window in enter_fct_results and what you tried in the lambda: in the first case, you're accessing an instance attribute (for instance, self.FCTpopup.ui.pushButton), while in the second you're trying to access a class attribute.
The self.ui object is created in the __init__ (when the class instance is created), so the instance will have an ui attribute, not the class:
class Test:
def __init__(self):
self.value = True
test = Test()
print(test.value)
>>> True
print(Test.value)
>>> AttributeError: type object 'Test' has no attribute 'value'
Provide a reference
The simple solution is to create a reference of the instance of the first window for the second:
def enter_fct_results(self):
self.FCTpopup = FCT_Window(self)
self.FCTpopup.show()
class FCT_Window(QMainWindow):
def __init__(self, mainWindow):
QMainWindow.__init__(self)
self.mainWindow = mainWindow
self.ui.pushButton_submitFCT.clicked.connect(self.doSomething)
def doSomething(self):
# ...
self.mainWindow.ui.pushButton.setEnabled(True)
Using modal windows (aka, dialogs)
Whenever a window is required for some temporary interaction (data input, document preview, etc), a dialog is preferred: the main benefit of using dialogs is that they are modal to the parent, preventing interaction on that parent until the dialog is closed; another benefit is that (at least on Qt) they also have a blocking event loop within their exec() function, which will only return as soon as the dialog is closed. Both of these aspects also make unnecessary disabling any button in the parent window. Another important reason is that QMainWindow is not intended for this kind of operation, also because it has features that generally unnecessary for that (toolbars, statusbars, menus, etc).
def enter_fct_results(self):
self.FCTpopup = FCT_Window(self)
self.FCTpopup.exec_()
class FCT_Window(QDialog):
def __init__(self, parent):
QMainWindow.__init__(self, parent)
self.ui.pushButton_submitFCT.clicked.connect(self.doSomething)
def doSomething(self):
# ...
self.accept()
The above makes mandatory to recreate the ui in designer using a QDialog (and not a QMainWindow) instead. You can just create a new one and drag&drop widgets from the original one.
i finally found my mistake: It was the place of the signal connection. It has to be right before the 2nd window is opened:
def enter_fct_results(self):
self.FCTpopup = Dialog(self.fct_data)
self.FCTpopup.submitted.connect(self.store_fct_data)
self.FCTpopup.exec_()
With this now i can send the stored data from the mainwindow to the opened window, import into the table, edit it and send it back to the main window on submit.

Programmatically press the `X` button on the toobar?

I know I can intercept pressing the X button with protocol("WM_DELETE_WINDOW", do_something) however I am having a hard time figuring out how to activate this button or at least the protocol that triggers when this button is pressed.
Here is the situation. I have 2 classes. My main Tk class and my Menu class. When I am setting up the command to close the program with an exit button from the menu I want this button to do exactly the same thing as the X button on the Tk class.
Now I know I could simply call the controller that was passed to the menu class and then call the method I built to handle the close event however I am trying to build this menu class in such a way that I do not need to do this from the menu class. This will allow me to use the menu class on any app I build with little to no editing.
I have not been able to find a post or some documentation that tells me how I can programmatically activate the "WM_DELETE_WINDOW" protocol.
Here is an image if it is unclear what I want. Simply I want the exit button to do exactly what the X button does.
Main class:
import tkinter as tk
import PIP_MENU
class PIP(tk.Tk):
def __init__(self):
super().__init__()
PIP_MENU.start(self)
self.protocol("WM_DELETE_WINDOW", self.handle_close)
def handle_close(self):
print("Closing")
self.quit()
if __name__ == '__main__':
PIP().mainloop()
Menu class on separate .py file:
import tkinter as tk
class Menu(tk.Menu):
def __init__(self, controller):
super().__init__()
self.controller = controller
controller.config(menu=self)
file_menu = tk.Menu(self, tearoff=0)
self.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="Exit", command=self.handle_exit)
def handle_exit(self):
# What can I do here that will be handled by
# protocol "WM_DELETE_WINDOW" of the main class?
# All I can find is destroy() and quit()
# But both of these do not get handled by "WM_DELETE_WINDOW".
def start(controller):
Menu(controller)
I have not been able to find a post or some documentation that tells me how I can programmatically active the "WM_DELETE_WINDOW" protocol.
You can't. By definition, the WM_DELETE_WINDOW protocol comes from the window manager.
Catching the protocol handler is designed to give you an opportunity to override its behavior. It is not designed to be a way to trigger some code no matter how the application is destroyed. If you want to run some code when the window is destroyed, whether that is by the user clicking the control on the window frame or through some other way, the correct way to do that is to bind to the <Destroy> event on the root window.
You have to be careful, in that any binding on the root window will be triggered for every widget. Therefore, your binding should only run when event.widget is the same as the root window.
The following example illustrates the technique. There is a method handle_close which is called whenever the window is destroyed. Whether you close the window by clicking on the control on the window frame, or whether you click on the "Close me!" button, the code still runs.
import tkinter as tk
class Example(tk.Tk):
def __init__(self):
super().__init__()
self.bind("<Destroy>", self.handle_close)
button = tk.Button(self, text="Close me!", command=self.destroy)
button.pack()
def handle_close(self, event):
if event.widget == self:
print("Closing")
self.quit()
example = Example()
example.mainloop()
I don't believe there's a method that invokes a specific protocol, since protocol seems to be a specific event watch. Here's a snippet from the module's class Tk:
class Tk(Misc, Wm):
"""Toplevel widget of Tk which represents mostly the main window
of an application. It has an associated Tcl interpreter."""
def _loadtk(self):
...
self.protocol("WM_DELETE_WINDOW", self.destroy)
As you can see, by default the module itself sets the protocol to destroy(). The protocol() method only seeks to replace the default function (at the absence of a function, it just removes the function):
def wm_protocol(self, name=None, func=None):
"""Bind function FUNC to command NAME for this widget.
Return the function bound to NAME if None is given. NAME could be
e.g. "WM_SAVE_YOURSELF" or "WM_DELETE_WINDOW"."""
if callable(func):
command = self._register(func)
else:
command = func
return self.tk.call(
'wm', 'protocol', self._w, name, command)
protocol = wm_protocol
but to achieve what you want you should be able to reference back to the same handling method with this:
def handle_exit(self):
self.controller.handle_close()
Of course, this is not as versatile since you must explicitly know the handler in your main window.
Thought I have accepted Bryan's answer I did manage to come to a workaround I think is fine here.
If I pass the method that is being used to deal with window closing to my menu class and then check if something has been passed I can then decide on weather or not to use the exit method I made or self.controller.destroy() with an if statement.
Here is my solution.
Main file:
import tkinter as tk
from tkinter import messagebox
import PIP_MENU
class PIP(tk.Tk):
def __init__(self):
super().__init__()
PIP_MENU.start(self, self.handle_close)
self.protocol("WM_DELETE_WINDOW", self.handle_close)
def handle_close(self):
x = messagebox.askquestion("DERP", "Do you want to close without saving?")
if x == "yes":
self.destroy()
if __name__ == '__main__':
PIP().mainloop()
Menu file:
import tkinter as tk
class Menu(tk.Menu):
def __init__(self, controller, exit_handler=None):
super().__init__()
self.controller = controller
self.exit_handler = exit_handler
controller.config(menu=self)
file_menu = tk.Menu(self, tearoff=0)
self.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="Exit", command=self.handle_exit)
def handle_exit(self):
if self.exit_handler != None:
self.exit_handler()
else:
self.controller.quit()
def start(controller, exit_handler=None):
return Menu(controller, exit_handler)

Simulate user clicking in QSystemTrayIcon

Even through the activated slot is being executed, the menu is still not showing. I traced through manually clicking the tray icon and the simulated click, and its going through the same execution logic.
Currently I have
class MyClass(QObject):
def __init__():
self._testSignal.connect(self._test_show)
self.myTrayIcon.activated.connect(lambda reason: self._update_menu_and_show(reason))
def show():
self._testSignal.emit()
#pyqtSlot()
def _test_show():
self._trayIcon.activated.emit(QtWidgets.QSystemTrayIcon.Trigger)
#QtCore.pyqtSlot()
def _update_menu_and_show(reason):
if reason in (QtWidgets.QSystemTrayIcon.Trigger):
mySystemTrayIcon._update_menu()
...
class MySystemTrayIcon(QSystemTrayIcon):
def _update_menu(self):
# logic to populate menu
self.setContextMenu(menu)
...
MyClass().show()
Here is how I made the context menu associated with the tray icon pop up
class MyClass(QObject):
def __init__():
self._testSignal.connect(self._test_show)
self.myTrayIcon.activated.connect(lambda reason: self._update_menu_and_show(reason))
def show():
self._testSignal.emit()
#pyqtSlot()
def _test_show():
self._trayIcon.activated.emit(QSystemTrayIcon.Context)
#QtCore.pyqtSlot()
def _update_menu_and_show(reason):
if reason in (QSystemTrayIcon.Trigger, QSystemTrayIcon.Context):
mySystemTrayIcon._update_menu()
# Trigger means user initiated, Context used for simulated
# if simulated seems like we have to tell the window to explicitly show
if reason == QSystemTrayIcon.Context:
mySystemTrayIcon.contextMenu().setWindowFlags(QtCore.Qt.WindowStaysOnTopHint|QtCore.Qt.FramelessWindowHint)
pos = mySystemTrayIcon.geometry().bottomLeft()
mySystemTrayIcon.contextMenu().move(pos)
mySystemTrayIcon.contextMenu().show()
...
class MySystemTrayIcon(QSystemTrayIcon):
def _update_menu(self):
# logic to populate menu
self.setContextMenu(menu)
...
MyClass().show()
It seems you have to set the WindowStaysOnTopHint on the context menu so that it will appear.
This solution is specific to mac since it assumes the taskbar is on the top.
One side effect is that the context menu is always on top, even if the user clicks somewhere else. I placed an event filter on the context menu, the only useful event that it registered was QEvent.Leave

Strange emit behavior. Unable to get slot called when emit is performed

I have this strange situation with PySide/Qt.
EDIT: I added a small "can run" code to test the experienced behavior, at the end. If you want, you can skip the babbling and see what I am talking about through actual code. It should behave unexpectedly according to the signal/slot mechanism
Quick presentation of the design. I have a MainWindow, which holds two views, two controllers and two models.
The two views are created in the MainWindow, like this
ui.listView = views.ListView(ui.hSplitter)
ui.threeDView = views.ThreeDView(ui.hSplitter)
Nothing fancy. The models are created after the ui
listModel = models.ListModel()
listSelectionModel = models.ListSelectionModel()
The first one contains the actual data. The second one contains the selection, so that the 3D view can check if the selection has changed (normally by the user clicking on the listView) and plot the entity in 3d.
Finally, the controller are created like this
threeDController = ThreeDController(threeDView=self._ui.threeDView,
listModel=listModel,
listSelectionModel=listSelectionModel)
listController = ListController(listView = self._ui.listView,
listModel = listModel,
listSelectionModel=listSelectionModel)
Then I add some mock items in the listModel to have something to click.
The idea is that the list should show the items, and when you click, the 3d view will display them. I did as follows: ListView is defined as this
class ListView(QtGui.QWidget):
itemActivated = QtCore.Signal()
def __init__(self, parent):
super(ListView, self).__init__(parent)
self._ui = self._createUi()
self._ui.qListWidget.itemActivated.connect(self._itemActivatedSlot)
def _createUi(self):
ui = UIElems()
hLayout = QtGui.QHBoxLayout(self)
ui.qListWidget = QtGui.QListWidget(self)
hLayout.addWidget(ui.qListWidget)
return ui
def _itemActivatedSlot(self, listitem):
# I CONFIRM THIS LINE IS EXECUTED
# SO THE EMIT IS CORRECTLY PERFORMED
self.itemActivated.emit()
Note how I defined an itemActivated signal for the ListView. I intercept the QListWidget (note the Q) itemActivated, redirect it to a protected slot of ListView, and then emit a ListView signal, which will be listened externally. I do this because I want my ListView to wrap completely its internal controls.
Who is listening to this ListView itemActivated event? The ListController is, which should get this signal, and set the selection accordingly, so that the 3d controller can update the 3d view.
This is the ListController
class ListController(QtCore.QObject):
def __init__(self, listView, listModel, selectionModel):
super(ListController, self).__init__()
self._listView = listView
self._listModel = listModel
self._selectionModel = selectionModel
self._listModel.changed.connect(self._listModelChanged)
# HERE IS THE PROBLEM
self._listView.itemActivated.connect(self._listViewItemActivated)
# PLACEHOLDER
def _listModelChanged(self):
self._listView.setItems(self._listModel.items())
def _listViewItemActivated(self): ### ... AND HERE
identifier = self._listView.currentSelectedId()
self._selectionModel.setSelected(identifier)
As you can see, in the controller, I connect to the ListView's signal itemActivated, and expect to see the proper slot called. This unfortunately does not happen. If I, however, add the following line at the placeholder
self._listView.itemActivated.emit()
that is, I force the emitting right after the connect "from outside" (I am in the controller, and I emit a signal defined in the view), I start receiving the events and everything works as expected: the ListController._listViewItemActivated is called every time I click on the items, and the 3d view is consequently updated.
I tried to replicate the case in a smaller program, but I always got something that works. At this point, you are my only hope, because the behavior I experience makes no sense at all.
EDIT
This code shows the same behavior, except that now even the forced emit does not solve. Try to click the "hello" element in the listview. It will print the message, but the emit will not follow and the "Victory!" line will not be printed.
import sys
from PySide import QtGui, QtCore
class ListView(QtGui.QWidget):
itemActivated = QtCore.Signal()
def __init__(self, parent):
super(ListView, self).__init__(parent)
qlistview = QtGui.QListWidget(self)
qlistview.addItem("hello")
qlistview.itemActivated.connect(self._itemActivatedSlot)
def _itemActivatedSlot(self, listitem):
print "Activated "+ listitem.text() # this is printed.
self.itemActivated.emit() # Why not emitted or received ?!?!
class ListController(QtCore.QObject):
def __init__(self, listView):
super(ListController, self).__init__()
self._listView = listView
self._listView.itemActivated.connect(self._listViewItemActivated)
# self._listView.itemActivated.emit() # uncomment to see that the signal is actually connected and the mechanism works
def _listViewItemActivated(self):
print "_listViewItemActivated activated!!! Victory!"
class MainWindow(QtGui.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
listView = ListView(self)
listController = ListController(listView)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
app.setApplicationName("Test")
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
I might be wrong, but try to add slot decorator like:
#QtCore.Slot()
def _listViewItemActivated(self):
identifier = self._listView.currentSelectedId()
self._selectionModel.setSelected(identifier)
Or maybe just adding some dummy parameter to pass?
#QtCore.Slot(int)
...
itemActivated = QtCore.Signal(int)
Ok, the problem was subtle, and I'm a fool not realizing it, because it's basic python after all. You have to store the controllers somewhere on the MainWindow object, otherwise as soon as init is over, they will go out of scope and disappear, so they won't listen to signals anymore.

Categories

Resources