I am creating some unit tests for a PyQt application with pytest-qt.
And I would like to create open the graphical window, do some tests then close the window, rather than open a new window for every test, ie. use a module fixture for the window itself.
I succeeded to do this part, by calling in a local function a QtBot rather than using the default fixture, and removing the mocker ones. So I am pretty close to my objective.
However, but I am not able to close the window (and test the QMessageBox for closing event).
I red examples like
how to handle modal dialog and its git discussion, or the qmessage question; which seem to be close to my question.
It is suggested to use a timer to wait for the QMessageBox to appear then click on a button choice, but visibly I am not able to apply them correctly.
In my attempt, pytest get the closing demand, but not the click on the dialog box. So, I have to click myself to finish the test.
Here is a small example, with file GUI.py:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
from PyQt5 import QtGui, QtCore, QtWidgets
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QCoreApplication, Qt, QObject
from PyQt5.QtGui import QIcon
class Example(QMainWindow):
def __init__(self, parent = None):
super().__init__()
self.initUI(self)
def initUI(self, MainWindow):
# centralwidget
MainWindow.resize(346, 193)
self.centralwidget = QtWidgets.QWidget(MainWindow)
# The Action to quit
self.toolb_action_Exit = QAction(QIcon('exit.png'), 'Exit', self)
self.toolb_action_Exit.setShortcut('Ctrl+Q')
self.toolb_action_Exit.triggered.connect(self.close)
# The Button
self.btn_prt = QtWidgets.QPushButton(self.centralwidget)
self.btn_prt.setGeometry(QtCore.QRect(120, 20, 89, 25))
self.btn_prt.clicked.connect(lambda: self.doPrint() )
self.btn_quit = QtWidgets.QPushButton(self.centralwidget)
self.btn_quit.setGeometry(QtCore.QRect(220, 20, 89, 25))
self.btn_quit.clicked.connect(lambda: self.close() )
# The textEdit
self.textEdit = QtWidgets.QTextEdit(self.centralwidget)
self.textEdit.setGeometry(QtCore.QRect(10, 60, 321, 81))
# Show the frame
MainWindow.setCentralWidget(self.centralwidget)
self.show()
def doPrint(self):
print('TEST doPrint')
def closeEvent(self, event):
# Ask a question before to quit.
self.replyClosing = QMessageBox.question(self, 'Message',
"Are you sure to quit?", QMessageBox.Yes |
QMessageBox.No, QMessageBox.No)
if self.replyClosing == QMessageBox.Yes:
event.accept()
else:
event.ignore()
def main_GUI():
app = QApplication(sys.argv)
imageViewer = Example()
return app, imageViewer
if __name__ == '__main__':
app, imageViewer =main_GUI()
rc= app.exec_()
print('App end is exit code {}'.format(rc))
sys.exit(rc)
and the pytest file named test_GUI.py:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os, sys
import pytest
from PyQt5 import QtGui, QtCore, QtWidgets, QtTest
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QCoreApplication, Qt, QObject
from pytestqt.plugin import QtBot
GUI = __import__('GUI')
#pytest.yield_fixture(scope="module")
def qtbot_session(qapp, request):
print(" SETUP qtbot")
result = QtBot(qapp)
with capture_exceptions() as exceptions:
yield result
print(" TEARDOWN qtbot")
#pytest.fixture(scope="module")
def Viewer(request):
print(" SETUP GUI")
app, imageViewer = GUI.main_GUI()
qtbotbis = QtBot(app)
# qtbotbis.addWidget(imageViewer)
# qtbotbis.wait_for_window_shown(imageViewer)
QtTest.QTest.qWait(0.5 *1000)
yield app, imageViewer, qtbotbis
# EXIT
# mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes)
# imageViewer.toolb_action_Exit.trigger()
def handle_dialog():
# while not imageViewer.replyClosing.isVisible():
# app.processEvents()
box = QMessageBox()
box.setStandardButtons(QMessageBox.Yes)
button = box.button(QMessageBox.Yes)
qtbotbis.mouseClick(button, QtCore.Qt.LeftButton)
QtCore.QTimer.singleShot(100, handle_dialog)
qtbotbis.mouseClick(imageViewer.btn_quit, QtCore.Qt.LeftButton, delay=1)
assert imageViewer.close()
print(" TEARDOWN GUI")
class Test_GUI() :
def test_interface(self, Viewer):
print(" beginning ")
app, imageViewer, qtbot = Viewer
qtbot.mouseClick( imageViewer.btn_prt, QtCore.Qt.LeftButton )
QtTest.QTest.qWait(0.5 *1000)
assert True
print(" Test passed")
Any idea of what I am missing ? Any other idea or suggestion would also be appreciate.
In your attempt you are creating a new QMessageBox that is different from the one created with the static method QMessageBox::question() so even if you click it will not work.
The idea is to obtain the QMessageBox shown, and in this case we will take advantage of that since it is the active window so we can obtain it using QApplication::activeWindow(). Another way to get the QMessageBox is to use the relationship between imageViewer and the QMessageBox through findChild():
#pytest.fixture(scope="module")
def Viewer(request):
print(" SETUP GUI")
app, imageViewer = GUI.main_GUI()
qtbotbis = QtBot(app)
QtTest.QTest.qWait(0.5 * 1000)
yield app, imageViewer, qtbotbis
def handle_dialog():
messagebox = QtWidgets.QApplication.activeWindow()
# or
# messagebox = imageViewer.findChild(QtWidgets.QMessageBox)
yes_button = messagebox.button(QtWidgets.QMessageBox.Yes)
qtbotbis.mouseClick(yes_button, QtCore.Qt.LeftButton, delay=1)
QtCore.QTimer.singleShot(100, handle_dialog)
qtbotbis.mouseClick(imageViewer.btn_quit, QtCore.Qt.LeftButton, delay=1)
assert imageViewer.isHidden()
Related
Using PyQt5, I want to implement a two windows displaying one after another automatically, without the user interacting with any window. Something like this:
While True:
Show Window1
wait 2 seconds
Close Window1
Show Window2
wait 2 seconds
Close Window2
The problem I am having is that the main UI thread is stuck in app.exec_() function, so it cannot implement the opening and closing logic.
import sys
from PyQt5.QtWidgets import *
from PyQt5 import uic
class Win1(QMainWindow):
def __init__(self):
super(Win1, self).__init__()
uic.loadUi('win1.ui', self)
self.show()
class Win2(QMainWindow):
def __init__(self):
super(Win2, self).__init__()
uic.loadUi('win2.ui', self)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
while True:
win = Win1()
time.sleep(1)
win.close()
win = Win2()
time.sleep(1)
win.close()
app.exec_() # <--------- Program blocks here
I would appreciate if someone can share a minimal example for this working without blocking. Or please point to the mechanism that should be used.
If you are going to work with Qt then you should forget about sequential logic but you have to implement the logic using events. For example, in your case you want one window to be shown every time T and another to be hidden, so that can be implemented with a QTimer and a flag:
import sys
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5 import uic
class Win1(QMainWindow):
def __init__(self):
super(Win1, self).__init__()
uic.loadUi('win1.ui', self)
self.show()
class Win2(QMainWindow):
def __init__(self):
super(Win2, self).__init__()
uic.loadUi('win2.ui', self)
self.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
timer = QTimer()
timer.setProperty("flag", True)
win1 = Win1()
win2 = Win2()
def on_timeout():
flag = timer.property("flag")
if flag:
win1.show()
win2.close()
else:
win2.show()
win1.close()
timer.setProperty("flag", not flag)
timer.timeout.connect(on_timeout)
timer.start(1 * 1000)
on_timeout()
app.exec_()
You should not use while loop or time.sleep since they block the eventloop in which the GUI lives, that is: they freeze the windows
I am new to Python and to PyQt. I have designed GUIs in MATLAB so its been a frustrating new experience. Right now, I have a main window and I want to open another login window on button push. What I have not decided is whether it should return the value of login to the parent window or another window. But either way I need to be able to open the login window through the pushbutton. I have created 2 files declaring the GUI one as a mainwindow and another as Dialog (does not seem like it is inheriting from QDialog though). The other 2 py files are classed calling the UIs separately and work fine. Each have their functions. I am pasting the main.py (mainwindow init and onOpen function code) along with logindialog.py init function code. Kindly help or else I will have to go back to Matlab where I can do this quite easily.`
import sys
from PyQt5 import QtWidgets, QtGui
from LoginDialog import LoginDialog
import UI_MSLDB_Main
from UI_LoginDialog import Ui_LoginDialog
#import Helper
#import Auth
#import TestFeature01
class Main(QtWidgets.QMainWindow, UI_MSLDB_Main.Ui_MSLDatabase):
def _init_(self, parent = None):
QtWidgets.QMainWindow._init_(self)
self.setupUi(self)
self.child = LoginDialog(self)
self.child.setupUi(QtWidgets.QDialog())
# self.createConnections()
self.pushButton_OpenPrimarylist.clicked.connect(self.onOpen)
def onOpen(self):
# window = QtWidgets.QDialog
# self.child = LoginDialog(window, Ui_LoginDialog)
# self.child.show()
# exec('LoginDialog.py')
self.child.show
# self.actionStudy.triggered.connect()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
# LoginDialog(QDialog, Ui_LoginDialog)
# loginDialog = LoginDialog(QDialog, UI_LoginDialog)
# username = loginDialog.username
# password =loginDialog.password
# Helper.dbConnect(username,password)
# isAuth = False
# result = -1
# while not isAuth:
# result = loginDialog.exec()
# if result == LoginDialog.Success or result == LoginDialog.Rejected:
# isAuth = True
# else:
# isAuth = False
result = 1
if result == 1:#LoginDialog.Success:
MSLDatabase = QtWidgets.QMainWindow()
ui = UI_MSLDB_Main.Ui_MSLDatabase()
ui.setupUi(MSLDatabase)
MSLDatabase.show()
# w.show()
app.exec_()
sys.exit(app.exec_())
There were multiple things I tried not knowing which would work. The commented codes are the ones I have tried and failed. LoginDialog class is below.
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QDialog
from PyQt5.QtWidgets import QMessageBox
import Auth
import Helper
from UI_LoginDialog import Ui_LoginDialog
class LoginDialog(QDialog, Ui_LoginDialog):
Success, Failed, Rejected, username, password = range(0,5)
def _init_(self):
QDialog._init_(self)
self.setupUi(self)
QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL
(_fromUtf8("accepted()")),self.onAccept)
QtCore.QObject.connect(self.buttonBox,
QtCore.SIGNAL(_fromUtf8("rejected()")),
self.onReject)
I am currently coding an application using the Qt4 python bindings that requires the user to provide login credentials. At startup of my application I show a modal dialog in which the user can enter their data. Since the application cannot provide useful services when the user is not logged in, I want to close the app if the user clicks the cancel button.
So, if the dialog returns a negative result, I just call the close() method of my QMainWindow. Normally this leads to the exit of the application since there are no interactable windows any more.
However, if the modal Dialog has been shown before, the application simply continues to run and I have to kill it manually.
Below is the code for an minimal example
main.py:
import sys
from PyQt4.QtGui import QApplication
from MyMainWindow import MyMainWindow
app = QApplication(sys.argv)
window = MyMainWindow()
window.show()
app.exec_()
print "Terminated"
MyMainWindow.py:
from PyQt4.QtGui import QMainWindow
from PyQt4 import QtGui
from MyDialog import MyDialog
class MyMainWindow(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
self.setWindowTitle("Close App Test")
self.centralwidget = QtGui.QWidget(self)
self.verticalLayout = QtGui.QVBoxLayout(self.centralwidget)
self.closeButton = QtGui.QPushButton(self.centralwidget)
self.closeButton.setText("Close")
self.closeButton.clicked.connect(self.exit)
def show(self):
QMainWindow.show(self)
myDialog = MyDialog(self)
res = myDialog.exec_()
if res == 0:
self.exit()
else:
print "Continue"
def exit(self):
self.close()
MyDialog.py:
from PyQt4.QtGui import QDialog
from PyQt4 import QtGui
class MyDialog(QDialog):
def __init__(self, parent = None):
QDialog.__init__(self, parent)
self.setWindowTitle("Modal Dialog")
self.resize(200, 50)
self.closeButton = QtGui.QPushButton(self)
self.closeButton.setText("Close")
self.closeButton.move(10, 10)
self.otherButton = QtGui.QPushButton(self)
self.otherButton.setText("Do Nothing")
self.otherButton.move(100, 10)
self.closeButton.clicked.connect(self.reject)
self.otherButton.clicked.connect(self.accept)
If the Close button on the modal dialog is clicked, the application continues to run even if all windows are closed. If the "Do Nothing" button is clicked, and the application is closed with the close button on the main window, everything works as expected and the application terminates.
I cannot really see the difference between both cases, since each time I just call close on the main window. I can just assume that I have errors in handling the modal dialog. I even tried tinkering around with myDialog.close(), myDialog.destroy(), as well as the quitOnCloseand deleteOnClose window attributes without any positive effects.
Any help is appreciated.
self.exit() is called before app.exec_() if the Close button on the modal dialog is clicked, so the application continues to run. self.exit() should be called when the main event loop is running.
The following is a possible solution for the case:
from PyQt4.QtGui import QMainWindow
from PyQt4 import QtGui
from pyQt4 import QtCore
from MyDialog import MyDialog
class MyMainWindow(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
self.setWindowTitle("Close App Test")
self.centralwidget = QtGui.QWidget(self)
self.verticalLayout = QtGui.QVBoxLayout(self.centralwidget)
self.closeButton = QtGui.QPushButton(self.centralwidget)
self.closeButton.setText("Close")
self.closeButton.clicked.connect(self.exit)
def login(self):
myDialog = MyDialog(self)
res = myDialog.exec_()
if res == 0:
self.exit()
else:
print "Continue"
def show(self):
QMainWindow.show(self)
QtCore.QTimer.singleShot(0, self.login)
def exit(self):
self.close()
I am coding a application which needs a custom buttons in QMessageBox. i managed to create an example in QT designer which is given below.
i wanted to do this in a QMessageBox.
I am using python 2.6.4 and PyQt4. please, can any one help.
Here is an example of building a custom message box from the ground up.
import sys
from PyQt4 import QtCore, QtGui
class Example(QtGui.QDialog):
def __init__(self, parent=None):
super(Example, self).__init__(parent)
msgBox = QtGui.QMessageBox()
msgBox.setText('What to do?')
msgBox.addButton(QtGui.QPushButton('Accept'), QtGui.QMessageBox.YesRole)
msgBox.addButton(QtGui.QPushButton('Reject'), QtGui.QMessageBox.NoRole)
msgBox.addButton(QtGui.QPushButton('Cancel'), QtGui.QMessageBox.RejectRole)
ret = msgBox.exec_()
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
ex = Example()
ex.show()
sys.exit(app.exec_())
manuel-gutierrez, why do you inherit from QDilaog? You can inherit from QMessageBox. It's much simpler and less code
import sys
from PyQt4.QtGui import QMessageBox, QPushButton, QApplication
from PyQt4.QtCore import Qt
class ErrorWindow(QMessageBox):
def __init__(self, parent=None):
QMessageBox.__init__(self, parent)
self.setWindowTitle("Example")
self.addButton(QPushButton("Yes"), QMessageBox.YesRole )
self.addButton(QPushButton("No"), QMessageBox.NoRole)
self.addButton(QPushButton("Cancel"), QMessageBox.RejectRole)
if __name__ == "__main__":
app = QApplication(sys.argv)
ex = ErrorWindow()
ex.setText("some error")
ex.show()
sys.exit(app.exec_())
The standard QMessageBox "impose an interpretation of the response", being accepted, rejected or canceled.
Here is a version that allows arbitrary buttons, as much as wanted, and leave the interpreatation up to the user.
And it simplifies the sourcecode a bit.
The argument "buttons" gives a text list, this makes the buttons.
The return value is the text of the clicked botton.
So the user can do what he want's with that.
Note: This might be against UI-Standards and therefore less robust, but hey.
Note2: Since it's 2021, i use PyQt5 and python 3.7
I just posted this in case someone prefer this more generic approach.
#!/usr/bin/python3
# -*- coding: utf-8 -*-
""" A more generic, a bit simplified message box also known as 'popup' """
from PyQt5 import QtWidgets as QW
class Popup(QW.QMessageBox):
def __init__(
self,
title,
text,
buttons = ["Ok"]
):
super(Popup, self).__init__()
self.setWindowTitle(title)
self.setText(text)
self.buttons = buttons
for txt in self.buttons:
b = QW.QPushButton(txt)
self.addButton(b, QW.QMessageBox.NoRole)
def do(self):
answer = self.exec_()
text = self.buttons[answer]
return text
if __name__ == "__main__": # test
class Tester(QW.QWidget):
def __init__(self):
super(Tester, self).__init__()
btn = QW.QPushButton("do it")
btn.clicked.connect(self.klick)
layout = QW.QHBoxLayout(self)
layout.addWidget(btn)
def klick(self, text):
r = Popup(
"choose a letter",
"What is your favorite\nLetter\namong a to e ?",
buttons = "a,b,c,d,e".split(","))
print("result = ",r.do())
import sys
app = QW.QApplication(sys.argv)
widget = Tester()
widget.setGeometry(400,400,100,100)
widget.show()
sys.exit(app.exec_())
I defined a print function using QPrinter and QDialog. However, when I launch the printer dialog and then press cancel, the entire mainwindow goes into not responding mode. I have tried to do QtGui.QPrintDialog.close() but does not work.
Code:
import sys
from PyQt4 import QtCore
from PyQt4 import QtGui
class QButton(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.button = QtGui.QPushButton('Button', self)
self.name='me'
self.button.clicked.connect(self.calluser)
def calluser(self):
Appli=QtGui.QApplication(sys.argv)
printer= QtGui.QPrinter()
doc=QtGui.QTextDocument("Set local variables in this printing slot." )
dialog = QtGui.QPrintDialog(printer)
dialog.setModal(True)
dialog.setWindowTitle("Print Document" )
if dialog.exec_() == True:
doc.print_(printer)
# dialog.addEnabledOption(QAbstractPrintDialog.PrintSelection)
def demo_QButton():
app = QtGui.QApplication(sys.argv)
tb = QButton()
tb.show()
app.exec_()
if __name__=='__main__':
demo_QButton()
You created a new application in the calluser method. Delete or comment the line:
Appli=QtGui.QApplication(sys.argv)
and try again. I think this time your main window will remain responsive.