I have created a little pyqt5 project. Here is a printscreen of the application while running:
When the user clicks on the QPushButton from the main window, the dialog window appears and the user writes something in the QlineEdit. Then while clicking on the QPushButton of the dialog window, the dialog window sends a signal to the main window and is deleted. The signal contains the text typed by the user.
Here are the descriptions of my two classes which are very simple:
The MainWindow class.
The DialogWindow class (I want to make my own Dialog Class without using the pre existing Dialog windows).
My main script
I have several questions:
Is it the right way of using signals in order to communicate between windows? I do not think that I violate the class encapsulation. However I do not like to connect the signal on the child class by writing:
self.mySignal.connect(parent.updatelabelAnswer)
In this line I use the attribute parent - is it okay? It seems to me that it is not a good way to use signals.
My second question is:
Am I right to call self.deleteLater() in the on_pushButton_clicked slot of DialogWindow? It seems not, as I have checked with the python interactive shell and the object myDialogWindow is still accessible.
Generally, the parent should always be the one performing the signal connecting. Having the child widget make connections on the parent is problematic because it places limitations on the parent and causes side effects, and completely breaks in cases where parent ownership is transfrerred for the child widget.
In your example, there are two options I would consider "correct". If the dialog was meant to be at least somewhat persistent, and not meant to be run modally, then it should define a signal that the parent class connects to. The dialog should not delete itself, that should be the responsibility of the parent class after the signal is received.
MainWindow
def on_pushbutton_clicked(self):
if not self.dlg:
self.dlg = DialogWindow(self)
self.dlg.mySignal.connect(self.on_mySignal)
self.dlg.show()
def on_mySignal(value):
self.dlg.mySignal.disconnect()
self.dlg.close()
self.dlg.deleteLater()
self.dlg = None
self.updateLabelAnswer(value)
Your dialog seems to be a temporary dialog that exists just to gather input and should probably be run modally. In that case, you don't even have to define any signals. Just create the class and provide an API to get the value of the text box.
DialogWindow
class DialogWindow(...)
...
def on_pushbutton_clicked(self):
self.accept()
def getValue(self):
return self.lineEdit.text()
In MainWindow
def on_pushbutton_clicked(self):
dlg = DialogWindow(self)
if dlg.exec_():
value = dlg.getValue()
Okay so I guess I should post an answer instead of writing bloated comments :P
About the deletion I will quote the Qt documentation:
As with QWidget::close(), done() deletes the dialog if the
Qt::WA_DeleteOnClose flag is set. If the dialog is the application's
main widget, the application terminates. If the dialog is the last
window closed, the QApplication::lastWindowClosed() signal is emitted.
However if you want to handle the closing (and deletion) of the dialog window from your other widget that opens it, slots and signals should be used. Simply connect a button or whatever from your main widget and its clicked() signal to the done() slot of your dialog and you are good to go.
At this point I would also like to point out that deleting a dialog may not be necessary. Based on the memory footprint of the dialog (how much memory is used to create and run it) you may wish to consider creating the dialog at the beginning and leaving it in your memory until the main application is closed. In addition to that you can use hide() and show() to display it on the screen. This is actually a generally good practice for things that are small enough since the deletion and then creation of a window takes more time compared to simply hiding and showing it.
Now about the signals and slots these have pretty straight-forward semantics. As I've posted in the comments and my other answer in order to connect a slot to a signal you need to have them present in the same scope. If that's not the case pass one (or both) to a place where the situation is fixed. In your case you have to have a common place for both. If both are top-level widgets you have to do the connections inside your main(). I would rather add the dialog as an extension to your MainWindow class (as a class member) and to the instantiation plus the connections there - for example in the constructor of your MainWindow:
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setupUi(self)
self.dialog = DialogWindow(self)
# Connect mainwindow's signals to dialog's slots
# Connect dialog's signals to mainwindow's slots
# And even connect dialog's signals to dialog's slots
Related
I'm still trying to figure out how to separate my UI from my programme logic. I'm fairly new to Python and maybe you can help me out.
I was trying to separate my UI from my programme hoping that I might be able to switch from tkinter to some other GUI later on. So I was thinking to have my main programme doing stuff and calling a tk Frame for displaying my data.
So, this is my class for displaying and it works fine:
import tkinter as tk
class TestClass(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.label1 = tk.Label(text="Label1")
self.label1.pack()
self.changeLabel1("change from within") # <-- this works
def changeLabel1(self, changeText):
self.label1.config(text=changeText)
self.update_idletasks()
def writeSomething(self, outputText):
print(outputText)
And I made myself some test programme to instantiate the class:
# starter for TestClass
import TestClass
x = TestClass.TestClass()
x.mainloop()
x.writeSomething("Test") <-- nothing happens
x.changeLabel1("Test") <-- nothing happens
I put those calls to the functions after the mainloop just to show that I'm not able to change something after the mainloop has been called.
When I'm trying to change the label1 with the changeLabel1 function it works from within the class but not from my starter class. I get some output from writeSomething(text) but only when I close the window. And there is an error message which reads:
_tkinter.TclError: invalid command name ".!label"
(It is actually longer, only the last line of the traceback.)
Searching brought me to the conclusion that it might have something to do with the mainloop() but I'm not sure how to handle this problem.
What is best practice? Do I call the mainloop in my tkinter class or in my test file which calls the tkinter class?
Is this a way to separate UI and logic or am I getting something wrong and should it be done the other way around (calling the programme logic from within the UI class)?
I hope I made myself clear enough...
In a typical GUI the main loop is responsible for handling user events (by dispatching them to your event handler callbacks). In your snippet, code after the call to x.mainloop() is only executed when the main window is closed and the loop exited.
So yes, the obvious answer is that it's the UI code (the event handlers callbacks) that is responsible for calling the "logic" code (which would be better named "domain model" code - there is some "logic" in the UI too) - which is mostly obvious when you think about it: your domain model code should know nothing about the UI so you can use it with different UIs (GUI, text based, web, command line script etc).
If you want your UI to update itself in reaction to things happening in your domain model layer (which is a common requirement for any non-trivial GUI app), your best bet is the good old Model-View-Controller pattern (the real thing, not the web derivative), where the "model" is your domain model (or some wrapper around it), the "controler" are your event handlers callbacks and the "view" is your UI components (or some wrapper around them). The key here is the use of the Observer pattern which let the UI subscribe to events sent by the model.
I have a PyQt4 application with a QMainWindow. From within that program I launch another QMainWindow that is used to draw a matplotlib plot. My approach is based on Eli Benderskys way of integrating matplotlib with PyQt.
class QtMatplotlibWindow(QtGui.QMainWindow):
"""Plot window to display data, is created by main application"""
def __init__(self, parent=None):
QtGui.QMainWindow.__init__(self, parent)
If I instantiate QtMatplotlibWindow with parent=None the resulting window will be completely "independent", meaning that it has its own icon in the taskbar and is completely "detached" from the main application. So, for instance, I can bring anther application, say Internet Explorer, to the front and subsequently bring only the Matplotlib window to the front, the actual application staying in the background. However using parent=None results in the matplotlib window being thrown off the stack and closed without my willing to do so at some seemingly random point in time.
If, on the other hand, I pass the instance of the main application as the parent the two windows are "tied together", meaning that I cannot view them independently of each other.
How can I achieve the "best of both worlds"? I'd like to pass the instance of the main application as the parent, so that the generated plots will only be closed if I close the main application, but I would also like the plot windows to be entirely independent in showing and moving. I would expect there to be some property of QMainWindow that would allow me exactly that. I hope I could phrase my question clear enought, I feel like I lack the appropriate terminology.
The fact that your second window disappears at random time indicates that it has been garbage collected. You must keep a python reference to all your windows. For instance append your newly created window to a list somwhere in your application: windowlist.append(QtMatplotlibWindow())
I'm using pyside and sqlalchemy for a database of contact information.
The setup of sqlalchemy is pretty standard, except for the fact that when I created the session using sessionmaker(), I set expire_on_commit = False.
In order to be able to create a new session within a sitting I'm using a sort of refresh button. The problem is every time I use the refresh button, when I try to add a new line/entry, I get 2 or 4 additional entries (with one click), whereas if one gets deleted, all the "copies" are deleted too (they aren't actual copies though because each of them has their own unique id).
def refresh(self):
self.session.close_all()
self.session = CreateSession(self.username, self.password) #custom method defined elsewhere for creating a session
self.TableViews() #initializing the table views for pyside
self.WidgetsToAttributes() #connect widgets of the gui to the sqlalchemy scheme attributes
self.connectSignals() #connects the qt signals to the appropriate functions in code
pass
(there are also some additional initializations but they are irrelevant here because they only initialize some widgets)
All the above functions are used in other places, for example for the first initialization of the GUI, so what doesn't work here is the combination of the methods. From what I gather, it's the connectSignals() method that causes all the trouble. Signals are apparently doubled (sometimes quadrupled) which results in several entries (execution of code) from one click.
What causes that and how can I avoid it?
If you call several time a connect, your signal will be connected several times to the same slot. One signal will then trigger multiple calls to the slot. I think that's why you get multiple entries in your database.
As a simple example, the code bellow would print two "1" every time the button is clicked:
self.button=QtGui.QPushButton("my button")
self.button.clicked.connect(self.on_click)
self.button.clicked.connect(self.on_click)
def on_click(self):
print("1")
Signals are automatically disconnected when an object is destroyed. In your case, you destroy self.session to create a new one. So every signals directly linked to self.session should be destroyed.
But if you have something like this:
class myWidget(QtGui.QWidget):
def __init__( self, parent=None):
super(myWidget, self ).__init__( parent )
self.button=QtGui.QPushButton("add entry")
self.button.clicked.connect(self.on_click)
def on_click(self):
self.session.add_an_entry()
Then destroying self.session is not going to disconnect the signal between the button and the on_click function.
So, you can either not call this kind of connect multiple time, or you can manually delete the connection with disconnect.
I am creating a PyQt4 gui that allows the user in a qmainwindow to input some initial parameters and then click a start button to begin a program. When they click the start button, a new window that displays the parameters appears, and the program begins. I would like the user to be able to initiate multiple instances of the program. However, when the parameters are changed in the qmainwindow and the start button is clicked a second time, the first program window disappears.
Is there a way to have the start button call a second window that runs concurrently with the first window? I imagine this would be something like threading, but from what I have read, PyQt4 doesn't seem to have a method for threading within an application.
Any help would be much appreciated.
I am guessing that you are saving the reference to newly created window in the same variable. If you want to create multiple windows, try to save the reference to that window in a separate variable, i.e each window should have it's own reference variable.
def showWindow(self):
self.child = Window(self)
self.child.show()
If this is your situation, the first window will loose it's reference as soon as the second time showWindow() executes. Because the self.child will contain the reference to second window, resulting in closing the first window, because the first window has no reference reference. As soon as the widget looses reference in Qt, the widget will be destroyed. To overcome this problem maintain a list of variables:
# declare a list in __init__ as self.widgetList = []
def showWindow(self):
win = Window(self):
win.show()
self.widgetList.append(win)
I have a Qt program with many buttons, user-interactable widgets, etc.
At one stage in the program, I would like all the widgets to temporarily 'stop working'; stop behaving to mouse clicks and instead pass the event on to one function.
(This is so the User can select a widget to perform meta operations. Part explanation here: Get variable name of Qt Widget (for use in Stylesheet)? )
The User would pick a widget (to do stuff with) by clicking it, and of course clicking a button must not cause the button's bound function to run.
What is the correct (most abstracted, sensible) method of doing this?
(which doesn't involve too much new code. ie; not subclassing every widget)
Is there anything in Qt designed for this?
So far, I am able to retrieve a list of all the widgets in the program (by calling
QObject.findChildren(QtGui.QWidget)
so the solution can incorporate this.
My current horrible ideas are;
Some how dealing with all the applications events all the time in one
function and not letting through the events when I need the
application to be dormant.
When I need dormancy, make a new transparent widget which recieves
mouse clicks and stretch it over the entire window. Take coordinates
of click and figure out the widget underneath.
Somehow create a new 'shell' instance of the window.
THANKS!
(Sorry for the terrible write-up; in a slight rush)
python 2.7.2
PyQt4
Windows 7
You can intercept events send to specific widgets with QObject::installEventFilter.
graphite answered this one first so give credit where credit is due.
For an actual example in PySide, here's an example you might draw some useful code from:
my_app.py
from KeyPressEater import KeyPressEater
if __name__ == "__main__":
app = QApplication(sys.argv)
eater = KeyPressEater()
app.installEventFilter(eater)
KeyPressEater.py
class KeyPressEater(QObject):
# subclassing for eventFilter
def eventFilter(self, obj, event):
if self.ignore_input:
# swallow events
pass
else:
# bubble events
return QObject.eventFilter(self,obj,event)