QPushButton shortcut does not work properly - python

In this simple pyqt5 app, I have a QPushButton and I define a shortcut for it. I want to change its text every time it is triggered. Problem is that the shortcut works only for the first time. How can I fix it?
from PyQt5.QtWidgets import QPushButton, QMainWindow, QApplication
import sys
class window(QMainWindow):
def __init__(self):
super(window, self).__init__()
self.btn = QPushButton('&Connect', self)
self.btn.setShortcut('Ctrl+C')
self.btn.pressed.connect(self.btn_func)
def btn_func(self):
if self.btn.text() == '&Connect':
self.btn.setText('Dis&connect')
else:
self.btn.setText('&Connect')
def main():
app = QApplication(sys.argv)
win = window()
win.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

As explained in the text property documentation [emphasis mine]:
If the text contains an ampersand character ('&'), a shortcut is automatically created for it. [...] Any previous shortcut will be overwritten or cleared if no shortcut is defined by the text
I know that the above might seem confusing, as it seems that the shortcut is overwritten or cleared only if no shortcut is defined by the text, consider it like this:
Any previous shortcut will be overwritten, or it is cleared if no shortcut is defined by the text
The solution is to always reset the shortcut after setting the new text:
def btn_func(self):
if self.btn.text() == '&Connect':
self.btn.setText('Dis&connect')
else:
self.btn.setText('&Connect')
self.btn.setShortcut('Ctrl+C')
Note that using the button's text for comparison is considered bad practice, for three reasons:
the text of a button could (should) be localized;
you could easily forget to correctly update all the & texts, making the function behave in the wrong way;
some QStyles override existing mnemonics and change them by themselves, which also causes the text to change without any warning;
The most preferred way to achieve what you want would be to use an internal flag for the current state, and also a QAction with its own shortcut.
class Window(QMainWindow):
def __init__(self):
super(Window, self).__init__()
self.btn = QPushButton('&Connect', self)
self.btn.clicked.connect(self.btn_func)
self.connectAction = QAction('Toggle connection', self)
self.connectAction.setShortcut('Ctrl+c')
self.connectAction.triggered.connect(self.btn.animateClick)
self.addAction(self.connectAction)
self.connected = False
def btn_func(self):
self.connected = not self.connected
if self.connected:
self.btn.setText('Dis&connect')
else:
self.btn.setText('&Connect')
Also note that:
you should not use the pressed() signal, as it's standard convention to consider a button clicked when the user presses and releases the mouse button while inside the button area (so that the pressed action could be "undone" by moving the mouse away if the mouse button was pressed by mistake on the button); use the clicked() signal instead;
I changed the class name using a capitalized Window, as classes should always use capitalized names and lower cased names should only be used for functions and variables;
I used the animateClick slot to show that the button was clicked (as a visual feedback is always preferable), but you can directly connect to the function: self.connectAction.triggered.connect(self.btn_func);

Related

PyQt5: Closing / relaunching application causes seg fault [duplicate]

The following is a loop that I created:
import mainui
import loginui
from PyQt5 import QtWidgets
import sys
while True:
print('test')
app = QtWidgets.QApplication(sys.argv)
ui = loginui.Ui_MainWindow()
ui.setupUi()
ui.MainWindow.show()
app.exec_()
username=ui.username
app2 = QtWidgets.QApplication(sys.argv)
ui2 = mainui.Ui_MainWindow(username)
ui2.setupUi()
ui2.MainWindow.show()
app2.exec_()
if ui2.exitFlag=='repeat':#Repeat Condition
continue
else: #Exit Condition
sys.exit()
This is a loop containing a couple of PyQt5 windows, which are displayed in order. The windows run normally when they are not contained within a loop, and they also run pretty well in the first iteration of the loop.
But, when the repeat condition is satisfied, even though the loop does iterate (prints 'test' again) - the ui and ui2 windows do not get displayed again, and subsequently the program hits the exit condition and stops.
Any suggestions about why the windows do not get displayed, and how I can get them displayed would be very much appreciated.
An important premise: usually you need only one QApplication instance.
Proposed solutions
In the following examples I'm using a single QApplication instance, and switch between windows using signals.
Since you probably need to wait for the window to be closed in some way, you might prefer to use a QDialog instead of a QMainWindow, but if for some reason you need the features provided by QMainWindow (menus, dockbars, etc) this is a possible solution:
class First(QtWidgets.QMainWindow):
closed = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
central = QtWidgets.QWidget()
self.setCentralWidget(central)
layout = QtWidgets.QHBoxLayout(central)
button = QtWidgets.QPushButton('Continue')
layout.addWidget(button)
button.clicked.connect(self.close)
def closeEvent(self, event):
self.closed.emit()
class Last(QtWidgets.QMainWindow):
shouldRestart = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
central = QtWidgets.QWidget()
self.setCentralWidget(central)
layout = QtWidgets.QHBoxLayout(central)
restartButton = QtWidgets.QPushButton('Restart')
layout.addWidget(restartButton)
closeButton = QtWidgets.QPushButton('Quit')
layout.addWidget(closeButton)
restartButton.clicked.connect(self.restart)
closeButton.clicked.connect(self.close)
def restart(self):
self.exitFlag = True
self.close()
def showEvent(self, event):
# ensure that the flag is always false as soon as the window is shown
self.exitFlag = False
def closeEvent(self, event):
if self.exitFlag:
self.shouldRestart.emit()
app = QtWidgets.QApplication(sys.argv)
first = First()
last = Last()
first.closed.connect(last.show)
last.shouldRestart.connect(first.show)
first.show()
sys.exit(app.exec_())
Note that you can add menubars to a QWidget too, by using setMenuBar(menuBar) on their layout.
On the other hand, QDialogs are more indicated for these cases, as they provide their exec_() method which has its own event loop and blocks everything else until the dialog is closed.
class First(QtWidgets.QDialog):
def __init__(self):
super().__init__()
layout = QtWidgets.QHBoxLayout(self)
button = QtWidgets.QPushButton('Continue')
layout.addWidget(button)
button.clicked.connect(self.accept)
class Last(QtWidgets.QDialog):
def __init__(self):
super().__init__()
layout = QtWidgets.QHBoxLayout(self)
restartButton = QtWidgets.QPushButton('Restart')
layout.addWidget(restartButton)
closeButton = QtWidgets.QPushButton('Quit')
layout.addWidget(closeButton)
restartButton.clicked.connect(self.accept)
closeButton.clicked.connect(self.reject)
def start():
QtCore.QTimer.singleShot(0, first.exec_)
app = QtWidgets.QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)
first = First()
last = Last()
first.finished.connect(last.exec_)
last.accepted.connect(start)
last.rejected.connect(app.quit)
start()
sys.exit(app.exec_())
Note that in this case I had to use a QTimer to launch the first dialog. This is due to the fact that in normal conditions signals wait for theirs slot to be completed before returning control to the emitter (the dialog). Since we're constantly recalling the same dialog, this leads to recursion:
First is executed
First is closed, emitting the finished signal, which causes the following:
Second is executed
at this point the finished signal has not returned yet
Second is accepted, emitting the accepted signal, which causes:
First hasn't returned its exec_() yet, but we're trying to exec it again
Qt crashes showing the error StdErr: QDialog::exec: Recursive call detected
Using a QTimer.singleShot ensures that the signal returns instantly, avoiding any recursion for exec_().
Ok, but why doesn't it work?
As said, only one Q[*]Application instance should usually exists for each process. This doesn't actually prevent to create more instances subsequently: in fact, your code works while it's in the first cycle of the loop.
The problem is related to python garbage collection and how PyQt and Qt deals with memory access to the C++ Qt objects, most importantly the application instance.
When you create the second QApplication, you're assigning it to a new variable (app2). At that point, the first one still exists, and will be finally deleted (by Qt) as soon as the process is completed with sys.exit.
When the cycle restarts, instead, you're overwriting app, which would normally cause python to garbage collect the previous object as soon as possible.
This represents a problem, as Python and Qt need to do "their stuff" to correctly delete an existing QApplication object and the python reference.
If you put the following line at the beginning, you'll see that the first time the instance is returned correctly, while the second returns None:
app = QtWidgets.QApplication(sys.argv)
print('Instance: ', QtWidgets.QApplication.instance())
There's a related question here on StackOverflow, and an important comment to its answer:
In principle, I don't see any reason why multiple instances of QApplication cannot be created, so long as no more than one exists at the same time. In fact, it may often be a requirement in unit-testing that a new application instance is created for each test. The important thing is to ensure that each instance gets deleted properly, and, perhaps more importantly, that it gets deleted at the right time.
A workaround to avoid the garbage collection is to add a persistent reference to the app:
apps = []
while True:
print('test')
app = QtWidgets.QApplication(sys.argv)
apps.append(app)
# ...
app2 = QtWidgets.QApplication(sys.argv)
apps.append(app2)
But, as said, you should not create a new QApplication instance if you don't really need that (which is almost never the case).
As already noted in the comments to the question, you should never modify the files generated with pyuic (nor try to mimic their behavior). Read more about using Designer.

How do I open (and close) a PyQt5 application inside a loop, and get that loop running multiple times

The following is a loop that I created:
import mainui
import loginui
from PyQt5 import QtWidgets
import sys
while True:
print('test')
app = QtWidgets.QApplication(sys.argv)
ui = loginui.Ui_MainWindow()
ui.setupUi()
ui.MainWindow.show()
app.exec_()
username=ui.username
app2 = QtWidgets.QApplication(sys.argv)
ui2 = mainui.Ui_MainWindow(username)
ui2.setupUi()
ui2.MainWindow.show()
app2.exec_()
if ui2.exitFlag=='repeat':#Repeat Condition
continue
else: #Exit Condition
sys.exit()
This is a loop containing a couple of PyQt5 windows, which are displayed in order. The windows run normally when they are not contained within a loop, and they also run pretty well in the first iteration of the loop.
But, when the repeat condition is satisfied, even though the loop does iterate (prints 'test' again) - the ui and ui2 windows do not get displayed again, and subsequently the program hits the exit condition and stops.
Any suggestions about why the windows do not get displayed, and how I can get them displayed would be very much appreciated.
An important premise: usually you need only one QApplication instance.
Proposed solutions
In the following examples I'm using a single QApplication instance, and switch between windows using signals.
Since you probably need to wait for the window to be closed in some way, you might prefer to use a QDialog instead of a QMainWindow, but if for some reason you need the features provided by QMainWindow (menus, dockbars, etc) this is a possible solution:
class First(QtWidgets.QMainWindow):
closed = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
central = QtWidgets.QWidget()
self.setCentralWidget(central)
layout = QtWidgets.QHBoxLayout(central)
button = QtWidgets.QPushButton('Continue')
layout.addWidget(button)
button.clicked.connect(self.close)
def closeEvent(self, event):
self.closed.emit()
class Last(QtWidgets.QMainWindow):
shouldRestart = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
central = QtWidgets.QWidget()
self.setCentralWidget(central)
layout = QtWidgets.QHBoxLayout(central)
restartButton = QtWidgets.QPushButton('Restart')
layout.addWidget(restartButton)
closeButton = QtWidgets.QPushButton('Quit')
layout.addWidget(closeButton)
restartButton.clicked.connect(self.restart)
closeButton.clicked.connect(self.close)
def restart(self):
self.exitFlag = True
self.close()
def showEvent(self, event):
# ensure that the flag is always false as soon as the window is shown
self.exitFlag = False
def closeEvent(self, event):
if self.exitFlag:
self.shouldRestart.emit()
app = QtWidgets.QApplication(sys.argv)
first = First()
last = Last()
first.closed.connect(last.show)
last.shouldRestart.connect(first.show)
first.show()
sys.exit(app.exec_())
Note that you can add menubars to a QWidget too, by using setMenuBar(menuBar) on their layout.
On the other hand, QDialogs are more indicated for these cases, as they provide their exec_() method which has its own event loop and blocks everything else until the dialog is closed.
class First(QtWidgets.QDialog):
def __init__(self):
super().__init__()
layout = QtWidgets.QHBoxLayout(self)
button = QtWidgets.QPushButton('Continue')
layout.addWidget(button)
button.clicked.connect(self.accept)
class Last(QtWidgets.QDialog):
def __init__(self):
super().__init__()
layout = QtWidgets.QHBoxLayout(self)
restartButton = QtWidgets.QPushButton('Restart')
layout.addWidget(restartButton)
closeButton = QtWidgets.QPushButton('Quit')
layout.addWidget(closeButton)
restartButton.clicked.connect(self.accept)
closeButton.clicked.connect(self.reject)
def start():
QtCore.QTimer.singleShot(0, first.exec_)
app = QtWidgets.QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)
first = First()
last = Last()
first.finished.connect(last.exec_)
last.accepted.connect(start)
last.rejected.connect(app.quit)
start()
sys.exit(app.exec_())
Note that in this case I had to use a QTimer to launch the first dialog. This is due to the fact that in normal conditions signals wait for theirs slot to be completed before returning control to the emitter (the dialog). Since we're constantly recalling the same dialog, this leads to recursion:
First is executed
First is closed, emitting the finished signal, which causes the following:
Second is executed
at this point the finished signal has not returned yet
Second is accepted, emitting the accepted signal, which causes:
First hasn't returned its exec_() yet, but we're trying to exec it again
Qt crashes showing the error StdErr: QDialog::exec: Recursive call detected
Using a QTimer.singleShot ensures that the signal returns instantly, avoiding any recursion for exec_().
Ok, but why doesn't it work?
As said, only one Q[*]Application instance should usually exists for each process. This doesn't actually prevent to create more instances subsequently: in fact, your code works while it's in the first cycle of the loop.
The problem is related to python garbage collection and how PyQt and Qt deals with memory access to the C++ Qt objects, most importantly the application instance.
When you create the second QApplication, you're assigning it to a new variable (app2). At that point, the first one still exists, and will be finally deleted (by Qt) as soon as the process is completed with sys.exit.
When the cycle restarts, instead, you're overwriting app, which would normally cause python to garbage collect the previous object as soon as possible.
This represents a problem, as Python and Qt need to do "their stuff" to correctly delete an existing QApplication object and the python reference.
If you put the following line at the beginning, you'll see that the first time the instance is returned correctly, while the second returns None:
app = QtWidgets.QApplication(sys.argv)
print('Instance: ', QtWidgets.QApplication.instance())
There's a related question here on StackOverflow, and an important comment to its answer:
In principle, I don't see any reason why multiple instances of QApplication cannot be created, so long as no more than one exists at the same time. In fact, it may often be a requirement in unit-testing that a new application instance is created for each test. The important thing is to ensure that each instance gets deleted properly, and, perhaps more importantly, that it gets deleted at the right time.
A workaround to avoid the garbage collection is to add a persistent reference to the app:
apps = []
while True:
print('test')
app = QtWidgets.QApplication(sys.argv)
apps.append(app)
# ...
app2 = QtWidgets.QApplication(sys.argv)
apps.append(app2)
But, as said, you should not create a new QApplication instance if you don't really need that (which is almost never the case).
As already noted in the comments to the question, you should never modify the files generated with pyuic (nor try to mimic their behavior). Read more about using Designer.

What effect will exist when using any 'return' statement inside Qt's slot

For instance, I connect the 'clicked' signal of QPushButton to a function named 'func_with_return'. Assumes that there are just three statements in this function: The first one is 'print('start')', the second one is 'return 1' and the last one is 'print('end')'. There is my python code based on PyQt5.
import sys
from PyQt5.QtWidgets import QApplication, QFrame, QPushButton
class MyWindow(QFrame):
def __init__(self):
super(MyWindow, self).__init__()
self.layout_init()
self.layout_manage()
def layout_init(self):
self.setFixedSize(800, 600)
self.button01 = QPushButton('click!', self)
self.button01.setFixedSize(100, 100)
self.button01.clicked.connect(self.func_with_return)
def layout_manage(self):
pass
def func_with_return(self):
print('---------func_with_return starts---------')
return 1
print('---------func_with_return ends---------')
if __name__ == '__main__':
app = QApplication(sys.argv)
mywindow = MyWindow()
mywindow.show()
sys.exit(app.exec_())
Basically, there is no error after clicking on this button. What I am curious about is the interruption caused by 'return' inside a 'slot'. Will this interruption have collision with the signal&slot mechanism?
None. The signals only invoke the function, if the function returns Qt will not use it.
On the other hand in Qt/PyQt it is said that a function is a slot if you use the decorator #QtCore.pyqtSlot(). In your case it is a simple function. Even so for a signal will not serve the data that returns the slot or function invoked.
Will this interruption have collision with the signal&slot mechanism?
No, it does not have a collision. Returning to the beginning, middle or end is irrelevant, remember every function returns something (if you do not use return the function will implicitly return None at the end).
On the other hand in a GUI the tasks of the functions must be light, if they are heavy you must execute it in another thread.

PyQt5 QWidget code uses un-initialized variable

I am following a tutorial on zetcode.com and I seem to have run into trouble. This code is supposed to be very straight forward and so I pasted it below.
This is all a part of an instance of the QWidget class (the base class of all UI objects). I have realized that this is one of the fundamental classes I need to understand in order to write a GUI, and I am simply puzzled on what is going on in the program.
The program is simple enough: PyQt opens a window, you are then able to exit out of that window via the 'x' button. And upon clicking the 'x' a message inquiring "Are you sure to quit?" allows you to continue to exit or cancel.
import sys
from PyQt5.QtWidgets import QWidget, QMessageBox, QApplication
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Message box')
self.show()
def closeEvent(self, event):
reply = QMessageBox.question(self, 'Message',
"Are you sure to quit?", QMessageBox.Yes |
QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
event.accept()
else:
event.ignore()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
So what doesn't make sense?
The closeEvent() method that QWidget calls. The method seemingly accepts a variable "event" that is never initialized but somehow passed into the function. The methods "event.accept()" and "event.ignore()" are then called on an object that previously was never initialized.
I am a noob in PyQt/Qt and maybe it's a misunderstanding of Python too. Here is documentation on the function http://doc.qt.io/qt-5/qwidget.html#closeEvent, that may clarify things.
The method seemingly accepts a variable "event" that is never
initialized but somehow passed into the function.
That is how a method works. Consider this simple function:
def print_a_word(word):
print(word)
It takes an argument word, that we did not initialized. When you call the function, then you need to define what word is:
word = "unicorn"
print_a_word(word)
If you look at the doc in more details, you'll see that event is a QCloseEvent, and it is "initialized" somewhere else in QWidget
The QCloseEvent class contains parameters that describe a close event.
Close events are sent to widgets that the user wants to close, usually
by choosing "Close" from the window menu, or by clicking the X title
bar button. They are also sent when you call QWidget::close() to close
a widget programmatically.

Ctrl+Alt key modifier behavior with Qt on windows when a textbox has focus

I made a quick sample program to demonstrate the problem
import sys
from PyQt4 import QtGui
from PyQt4.QtCore import Qt
class AWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(AWindow, self).__init__(parent=parent)
self.setCentralWidget(QtGui.QWidget())
self.centralWidget().setLayout(QtGui.QFormLayout())
self.centralWidget().layout().addRow(
QtGui.QLabel('some text'),
QtGui.QLineEdit()
)
self.centralWidget().layout().addRow(
QtGui.QLabel('some text'),
QtGui.QCheckBox('this is checkbox')
)
def keyPressEvent(self, e):
if int(e.modifiers()) == (Qt.ControlModifier+Qt.AltModifier):
if e.key() == Qt.Key_K:
#when ctrl+alt+k is pressed, a message box should open
msg = QtGui.QMessageBox(
QtGui.QMessageBox.Information,
'w00t',
'You pressed ctrl+alt+k'
)
msg.exec_()
def main():
app = QtGui.QApplication(sys.argv)
w = AWindow()
w.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
The problem
In this sample I'm capturing the ctrl+alt+k keyboard shortcut. The problem is that if a text entry widget has keyboard focused when those keys are pressed, it is not captured by the keyPressEevent handler, instead an upper case K is typed into the box. (this is the same with all ctrl+alt keypresses).
If another kind of widget is focused, one that does not accept text input (e.g. a checkbox, button) the key press is registered as it should and in the sample, the messagebox is shown.
Also, keyboard shortcuts that use only a Ctrl modifier work fine.
This problem only presents it self on Windows, but not on Linux, so this leads me to believe this has something to do with how windows handles the ctrl+alt modifier, or perhaps I'm not capturing the modifiers properly.
Is there any way to fix this?
Edit
I derived this solution from Spidey's comment.
I sublassed the QLineEdit and handled it's keyPressEvent like this.
#EDIT: this doesn't work, see below
def keyPressEvent(self, e):
if e.modifiers() and Qt.ControlModifier and Qt.AltModifier:
e.ignore()
super(CoolLineEdit, self).keyPressEvent(e)
Edit again
Adding up the modifiers as ints and returning after e.ignore() (maybe ignore isn't needed) seems to be very important.
This is the real fix.
def keyPressEvent(self, e):
if int(e.modifiers()) == (QtCore.Qt.ControlModifier+QtCore.Qt.AltModifier):
e.ignore()
return
super(SaveLineEdit, self).keyPressEvent(e)
The QLineEdit, when it has focus, is handling the `QKeyEvent. If you need this functionality on Windows, I would suggest:
1) Derive a class from QLineEdit (or whatever QTextEdit-derived class you want).
2) Override virtual void keyPressEvent(QKeyEvent* e) and check for your specific key-combo.
3) If you find it, return without handling it, and it will fall to the next level (your window, which will catch it and print your message).
4) If you don't find it, explicitly call the base-class version of the function: BaseClass::keyPressEvent(e); so that normal key presses are handled correctly.
Sorry for c++ syntax, I haven't used PyQt.

Categories

Resources