How to test a custom dialog window called using exec_()? - python

I'm trying to write a system test for my project. I have a controller class which launches the various windows. However, I can't seem to control windows launch using exec with the qtbot.
Here is an MVCE:
from PyQt5.QtWidgets import *
from PyQt5 import QtGui
class Controller:
def __init__(self):
self.name = None
self.a = WindowA(self)
def launchB(self):
self.b = WindowB(self)
if self.b.exec_():
self.name = self.b.getData()
class WindowA(QDialog):
def __init__(self, controller):
super(WindowA, self).__init__()
self.controller = controller
layout = QVBoxLayout()
self.button = QPushButton('Launch B')
self.button.clicked.connect(self.controller.launchB)
layout.addWidget(self.button)
self.setLayout(layout)
self.show()
class WindowB(QDialog):
def __init__(self, controller):
super(WindowB, self).__init__()
self.controller = controller
layout = QVBoxLayout()
self.le = QLineEdit()
self.button = QPushButton('Save')
self.button.clicked.connect(self.save)
layout.addWidget(self.le)
layout.addWidget(self.button)
self.setLayout(layout)
self.show()
def getData(self):
return self.le.text()
def save(self):
if self.le.text():
self.accept()
self.close()
else:
self.reject()
from PyQt5.QtWidgets import QApplication
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = Controller()
sys.exit(app.exec_())
I'd like to test that the user successfully enters data in the lineedit. In my test I'm able to successfully click the button in WindowA to launch WindowB, but am unable to use keyClicks to enter data in the lineedit.
Here is the test:
def test_1(qtbot):
control = Controller()
qtbot.mouseClick(control.a.button, QtCore.Qt.LeftButton)
qtbot.keyClicks(control.b.le, 'Test_Project')
qtbot.mouseClick(control.b.button, QtCore.Qt.LeftButton)
assert control.name == 'Test_Project'

The problem is that using exec_() blocks all synchronous tasks until the window is closed, the solution is to use a QTimer to launch the remaining tasks asynchronously:
def test_1(qtbot):
control = Controller()
def on_timeout():
qtbot.keyClicks(control.b.le, "Test_Project")
qtbot.mouseClick(control.b.button, QtCore.Qt.LeftButton)
QtCore.QTimer.singleShot(0, on_timeout)
qtbot.mouseClick(control.a.button, QtCore.Qt.LeftButton)
assert control.name == "Test_Project"

Related

PYQT5 Gui crashes sometimes when I start a thread [duplicate]

I have the following code but it's complaining that I cannot access the UI data from my thread. In my example code below, What is the best way I can access the userInputString value so my threading can run?
self.nameField is a PyQt QLineEdit.
QObject::setParent: Cannot set parent, new parent is in a different thread
QPixmap: It is not safe to use pixmaps outside the GUI thread
QWidget::repaint: Recursive repaint detected
import myUI
class MainUIClass(QtGui.QMainWindow, myUI.Ui_MainWindow):
def __init__(self, parent=None):
super(MainUIClass, self).__init__(parent)
self.setupUi(self)
self.startbutton.clicked.connect(self.do_work)
self.workerThread = WorkerThread()
self.connect(self.workerThread, SIGNAL("myThreading()"), self.myThreading, Qt.DirectConnection)
def do_work(self):
self.userInputString = self.nameField.Text()
self.workerThread.start()
def myThreading(self):
if userInputString is not None:
#Do something
class WorkerThread(QThread):
def __init__(self, parent=None):
super(WorkerThread, self).__init__(parent)
def run(self):
self.emit(SIGNAL("myThreading()"))
if __name__ == '__main__':
a = QtGui.QApplication(sys.argv)
app = MainUIClass()
app.show()
a.exec_()
Not sure if it's what you need but here is a working QThread exemple using Qt5
import time
import sys
from PyQt5 import QtWidgets, QtGui, QtCore
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.worker_thread = WorkerThread()
self.worker_thread.job_done.connect(self.on_job_done)
self.create_ui()
def create_ui(self):
self.button = QtWidgets.QPushButton('Test', self)
self.button.clicked.connect(self.start_thread)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.button)
def start_thread(self):
self.worker_thread.gui_text = self.button.text()
self.worker_thread.start()
def on_job_done(self, generated_str):
print("Generated string : ", generated_str)
self.button.setText(generated_str)
class WorkerThread(QtCore.QThread):
job_done = QtCore.pyqtSignal('QString')
def __init__(self, parent=None):
super(WorkerThread, self).__init__(parent)
self.gui_text = None
def do_work(self):
for i in range(0, 1000):
print(self.gui_text)
self.job_done.emit(self.gui_text + str(i))
time.sleep(0.5)
def run(self):
self.do_work()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
test = MainWindow()
test.show()
app.exec_()

PyQt: How to get UI data from a QThread

I have the following code but it's complaining that I cannot access the UI data from my thread. In my example code below, What is the best way I can access the userInputString value so my threading can run?
self.nameField is a PyQt QLineEdit.
QObject::setParent: Cannot set parent, new parent is in a different thread
QPixmap: It is not safe to use pixmaps outside the GUI thread
QWidget::repaint: Recursive repaint detected
import myUI
class MainUIClass(QtGui.QMainWindow, myUI.Ui_MainWindow):
def __init__(self, parent=None):
super(MainUIClass, self).__init__(parent)
self.setupUi(self)
self.startbutton.clicked.connect(self.do_work)
self.workerThread = WorkerThread()
self.connect(self.workerThread, SIGNAL("myThreading()"), self.myThreading, Qt.DirectConnection)
def do_work(self):
self.userInputString = self.nameField.Text()
self.workerThread.start()
def myThreading(self):
if userInputString is not None:
#Do something
class WorkerThread(QThread):
def __init__(self, parent=None):
super(WorkerThread, self).__init__(parent)
def run(self):
self.emit(SIGNAL("myThreading()"))
if __name__ == '__main__':
a = QtGui.QApplication(sys.argv)
app = MainUIClass()
app.show()
a.exec_()
Not sure if it's what you need but here is a working QThread exemple using Qt5
import time
import sys
from PyQt5 import QtWidgets, QtGui, QtCore
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.worker_thread = WorkerThread()
self.worker_thread.job_done.connect(self.on_job_done)
self.create_ui()
def create_ui(self):
self.button = QtWidgets.QPushButton('Test', self)
self.button.clicked.connect(self.start_thread)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.button)
def start_thread(self):
self.worker_thread.gui_text = self.button.text()
self.worker_thread.start()
def on_job_done(self, generated_str):
print("Generated string : ", generated_str)
self.button.setText(generated_str)
class WorkerThread(QtCore.QThread):
job_done = QtCore.pyqtSignal('QString')
def __init__(self, parent=None):
super(WorkerThread, self).__init__(parent)
self.gui_text = None
def do_work(self):
for i in range(0, 1000):
print(self.gui_text)
self.job_done.emit(self.gui_text + str(i))
time.sleep(0.5)
def run(self):
self.do_work()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
test = MainWindow()
test.show()
app.exec_()

How to control QProgressBar with Signal

Pushing the button starts 100 rounds cycle. With QLabel.setText() we update self.label from inside of scope of clicked() function.
Aside from updating self.label we would like to update the progressbar as well.
But since progressbar is a local variable we can't update it from inside of onClick() function.
import time
class ProgressBar(QtGui.QProgressBar):
def __init__(self, parent=None, total=20):
super(ProgressBar, self).__init__(parent=parent)
self.setMinimum(1)
self.setMaximum(105)
self.setTextVisible(True)
def set_to_value(self, value):
self.setValue(value)
QtGui.qApp.processEvents()
def closeEvent(self, event):
self._active=False
class Dialog(QtGui.QDialog):
def __init__(self):
super(QtGui.QDialog,self).__init__()
layout = QtGui.QVBoxLayout()
self.setLayout(layout)
self.label = QtGui.QLabel('idle...')
layout.addWidget(self.label)
progressbar = ProgressBar(self)
layout.addWidget(progressbar)
button = QtGui.QPushButton('Process')
button.clicked.connect(self.onClick)
layout.addWidget(button)
def onClick(self):
for i in range(101):
message = '...processing %s of 100'%i
self.label.setText(message)
QtGui.qApp.processEvents()
time.sleep(1)
if __name__ == '__main__':
app = QtGui.QApplication([])
dialog = Dialog()
dialog.resize(300, 100)
dialog.show()
app.exec_()
Declare the progress bar as:
self.progressbar = ProgressBar(self)
The code declares a local progressbar object connected to a custom 'customSignal` using:
QtCore.QObject.connect(self, QtCore.SIGNAL("customSignal(int)"), progressbar, QtCore.SLOT("setValue(int)"))
with four arguments passed to QtCore.QObject.connect().
The first argument self is the object that will be emitting the signal. Since the function that will do the "sleep-every-second-processing" is declared under the main Dialog instance we pass self here.
The second argument is the name of the signal itself: 'customSignal'.
The progressbar object is used as third and its method setValue as fourth last argument.
class ProgressBar(QtGui.QProgressBar):
def __init__(self, parent=None):
super(ProgressBar, self).__init__(parent=parent)
class Dialog(QtGui.QDialog):
def __init__(self):
super(QtGui.QDialog,self).__init__()
layout = QtGui.QVBoxLayout()
self.setLayout(layout)
self.label = QtGui.QLabel('idle...')
layout.addWidget(self.label)
progressbar = ProgressBar(self)
QtCore.QObject.connect(self, QtCore.SIGNAL("customSignal(int)"), progressbar, QtCore.SLOT("setValue(int)"))
layout.addWidget(progressbar)
button = QtGui.QPushButton('Process')
button.clicked.connect(self.clicked)
layout.addWidget(button)
def clicked(self):
for value in range(101):
message = '...processing %s of 100'%value
self.label.setText(message)
self.emit(QtCore.SIGNAL("customSignal(int)"), value)
QtGui.qApp.processEvents()
time.sleep(1)
if __name__ == '__main__':
app = QtGui.QApplication([])
dialog = Dialog()
dialog.resize(300, 100)
dialog.show()
app.exec_()
--------------------
Here is variation of the same solution except linking to the progressbar method.
import time
class ProgressBar(QtGui.QProgressBar):
def __init__(self, parent=None):
super(ProgressBar, self).__init__(parent=parent)
def set_to_value(self, value):
self.setValue(value)
QtGui.qApp.processEvents()
return True
def closeEvent(self, event):
self._active=False
class Dialog(QtGui.QDialog):
def __init__(self):
QtGui.QDialog.__init__(self, parent=None)
layout = QtGui.QVBoxLayout()
self.setLayout(layout)
self.label = QtGui.QLabel('idle...')
layout.addWidget(self.label)
progressbar = ProgressBar(self)
QtCore.QObject.connect(self, QtCore.SIGNAL("customSignal(int)"), progressbar.set_to_value )
layout.addWidget(progressbar)
button = QtGui.QPushButton('Process')
button.clicked.connect(self.clicked)
layout.addWidget(button)
def clicked(self):
for value in range(101):
message = '...processing %s of 100'%value
self.label.setText(message)
self.emit(QtCore.SIGNAL("customSignal(int)"), value)
QtGui.qApp.processEvents()
time.sleep(1)
if __name__ == '__main__':
app = QtGui.QApplication([])
dialog = Dialog()
dialog.resize(300, 100)
dialog.show()
app.exec_()
======================
This code now links a custom signal to a function referred as Slot in Qt.
from PySide import QtCore, QtGui
import time
class ProgressBar(QtGui.QProgressBar):
def __init__(self, parent=None):
super(ProgressBar, self).__init__(parent=parent)
#QtCore.Slot(int)
def set_to_value(self, value):
self.setValue(value)
QtGui.qApp.processEvents()
return True
def closeEvent(self, event):
self._active=False
class Dialog(QtGui.QDialog):
def __init__(self):
QtGui.QDialog.__init__(self, parent=None)
layout = QtGui.QVBoxLayout()
self.setLayout(layout)
self.label = QtGui.QLabel('idle...')
layout.addWidget(self.label)
progressbar = ProgressBar(self)
# QtCore.QObject.connect(self, QtCore.SIGNAL("customSignal(int)"), progressbar.set_to_value )
QtCore.QObject.connect(self, QtCore.SIGNAL("customSignal(int)"), progressbar, QtCore.SLOT("set_to_value(int)"))
layout.addWidget(progressbar)
button = QtGui.QPushButton('Process')
button.clicked.connect(self.clicked)
layout.addWidget(button)
def clicked(self):
for value in range(101):
message = '...processing %s of 100'%value
self.label.setText(message)
self.emit(QtCore.SIGNAL("customSignal(int)"), value)
QtGui.qApp.processEvents()
time.sleep(1)
if __name__ == '__main__':
dialog = Dialog()
dialog.resize(300, 100)
dialog.show()

How to set focus on a widget at app startup?

I'm trying to set the focus on QLineEdit widget at app startup but for some reasons it fails. Calling the method which includes the QLineEdit_object.setFocus() and is bound to a button click, works perfectly. However on startup, it seems like it doesn't execute at all when set to initialize after widget creation.
Using PySide with Python.
# coding=utf-8
import sys
import PySide.QtGui as QG
import PySide.QtCore as QC
class GG(QG.QMainWindow):
def __init__(self):
super(GG, self).__init__()
self.move(0,0)
self.resize(400,300)
self.setWindowTitle('Demo')
self.tabw = QG.QTabWidget()
self.tab1 = Tab1()
self.tab2 = Tab2()
self.tabw.addTab(self.tab1, 'Tab1')
self.tabw.addTab(self.tab2, 'Tab2')
hbox = QG.QHBoxLayout()
hbox.addWidget(self.tabw)
self.setCentralWidget(self.tabw)
self.setLayout(hbox)
self.show()
class Tab1(QG.QWidget):
def __init__(self):
super(Tab1, self).__init__()
self.btns()
self.inputt()
self.layoutz()
self.inp.setFocus() # doesn't set the focus on startup ?
self.show()
def inputt(self):
self.inp = QG.QLineEdit('', self)
def btns(self):
self.btn1 = QG.QPushButton('Button1', self)
self.btn1.clicked.connect(self.focusit) # works just fine
def layoutz(self):
vbox = QG.QVBoxLayout()
vbox.addWidget(self.btn1)
vbox.addStretch(1)
vbox.addWidget(self.inp)
self.setLayout(vbox)
def focusit(self):
self.inp.setFocus() # works just fine
class Tab2(Tab1):
def __init__(self):
super(Tab2, self).__init__()
def main():
app = QG.QApplication(sys.argv)
a = GG()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Well, after some playing I came up with this solution:
import sys
import PySide.QtGui as QG
import PySide.QtCore as QC
class GG(QG.QMainWindow):
def __init__(self):
super(GG, self).__init__()
self.move(0,0)
self.resize(400,300)
self.setWindowTitle('Demo')
self.tabw = QG.QTabWidget()
self.tab1 = Tab1()
self.tab2 = Tab2()
self.tabw.addTab(self.tab1, 'Tab1')
self.tabw.addTab(self.tab2, 'Tab2')
hbox = QG.QHBoxLayout()
hbox.addWidget(self.tabw)
self.setCentralWidget(self.tabw)
self.setLayout(hbox)
self.tab2.inp.setFocus() # setting focus right here
self.tab1.inp.setFocus() # and here; notice the order
self.show()
class Tab1(QG.QWidget):
def __init__(self):
super(Tab1, self).__init__()
self.btns()
self.inputt()
self.layoutz()
self.show()
def inputt(self):
self.inp = QG.QLineEdit('', self)
def btns(self):
self.btn1 = QG.QPushButton('Button1', self)
self.btn1.clicked.connect(self.focusit)
def layoutz(self):
vbox = QG.QVBoxLayout()
vbox.addWidget(self.btn1)
vbox.addStretch(1)
vbox.addWidget(self.inp)
self.setLayout(vbox)
def focusit(self):
self.inp.setFocus()
class Tab2(Tab1):
def __init__(self):
super(Tab2, self).__init__()
def main():
app = QG.QApplication(sys.argv)
a = GG()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

Pyqt: Change icon from button

I'm just started with pyqt and i want to change the icon from a button.
But i'm dowing it from another class and pyqt don't like that.
The error is: QPixmap: It is not safe to use pixmaps outside the GUI thread
I know i must using singal en emits.
But i don't know how i must use it for change the icon from a button.
This my code now:
import sys
import time
from PyQt4 import QtGui, QtCore
from pymodbus.exceptions import ConnectionException
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from data.database import Tags, Query_Tags
class Example(QtGui.QMainWindow):
def __init__(self):
super(Example, self).__init__()
self.db_engine = create_engine('mysql://modbususer:modbususer#localhost/modbus')
self.db_session = sessionmaker(bind=self.db_engine)
self.db_session = self.db_session()
self.status = 0
self.initUI()
def initUI(self):
self.button = QtGui.QPushButton('Aan', self)
self.button.clicked.connect(self.run)
self.button.move(50,50)
Uit = QtGui.QPushButton('Uit', self)
Uit.clicked.connect(self.off)
Uit.move(150,50)
self.setGeometry(300, 300, 500, 150)
self.setWindowTitle('Quit button')
self.show()
self.worker = WorkThread(self)
self.worker.start()
def run(self):
add_tag = Query_Tags('18', '1')
self.db_session.add(add_tag)
self.db_session.commit()
def off(self):
add_tag = Query_Tags('18', '0')
self.db_session.add(add_tag)
self.db_session.commit()
self.status = 0
print self.store
def change(self):
print "test"
#self.button.setIcon(QtGui.QIcon("/home/stijnb/test/icon.png"))
#self.button.setIconSize(QtCore.QSize(16,16))
def database(self, store):
self.store = store
"""
Thread
"""
class WorkThread(QtCore.QThread):
def __init__(self, layout):
QtCore.QThread.__init__(self)
self.layout = layout
def __del__(self):
self.wait()
def run(self):
self.database = {}
while True:
self.db_engine = create_engine('mysql://modbususer:modbususer#localhost/modbus')
self.db_session = sessionmaker(bind=self.db_engine)
self.db_session = self.db_session()
for result in self.db_session.query(Tags):
self.database[int(result.id)] = {'naam': result.name, 'value': result.value}
self.change_icons()
time.sleep(1)
return
self.terminate()
def change_icons(self):
print self.database[21]['value']
if self.database[21]['value'] == '1':
self.layout.button.setIcon(QtGui.QIcon("/home/stijnb/test/aan.png"))
self.layout.button.setIconSize(QtCore.QSize(16,16))
else:
self.layout.button.setIcon(QtGui.QIcon("/home/stijnb/test/uit.png"))
self.layout.button.setIconSize(QtCore.QSize(16,16))
def main():
app = QtGui.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
You cannot touch GUI elements from any other thread other than the main GUI Thread. That's why Qt introduced messaging (Signals and Slots). You can connect to a signal from your worker thread that will be caught in the main thread, and then in the main thread you can change any element you want.
Here is a very simple example where I demonstrate the concept.
import sys
from PyQt4 import QtGui, QtCore
class Example(QtGui.QMainWindow):
def __init__(self):
super(Example, self).__init__()
# Create a button and set it as a central widget
self.button = QtGui.QPushButton('My Button', self)
self.setCentralWidget(self.button)
# Start the thread
self.worker = WorkThread(self)
self.worker.start()
# Connect the thread's signal "change_text" to
# "self.change_thread" function
self.worker.change_text.connect(self.change_text)
def change_text(self):
# Slots are always executed in the GUI thread so it's safe to change
# anything you want in a slot function. Here we just flip the button
# text from foo to bar and back. But you can change the image or do
# whatever you want.
# Slots can also receive parameters so you can emit from your thread
# which text should be set
self.button.setText('bar' if self.button.text() == 'foo' else 'foo')
class WorkThread(QtCore.QThread):
# Define the signal
change_text = QtCore.pyqtSignal()
def __init__(self, layout):
QtCore.QThread.__init__(self)
def run(self):
while True:
# emit the signal every 3 seconds
self.sleep(3)
self.change_text.emit()
app = QtGui.QApplication(sys.argv)
ex = Example()
ex.show()
sys.exit(app.exec_())

Categories