The context
I am creating my own version of the board game Battleship using PyQt. A PyQt main window contains both own and enemy boards. Boards are made up of clickable tiles that players 'fire at'. The implementation supports HUMANvsAI and AIvsAI games. I would like to test the strength of my AI algorithms through simulation. For example, I would like to run in a loop 1,000 AIvsAI games and get stats on % victory, average accuracy, etc.
The main issue
I am struggling to run the PyQt app multiple times to gather game data, e.g. in a for loop. Specifically, I cannot find a way to run the application, exit it, and re-run it again. Conceptually speaking I am looking for something like this:
# conceptual snippet
for i in range(n):
app = QApplication([])
window = MainWindow(b_size, boat_dict, players)
app.exec_()
With the call to exit the app somewhere else and called each time the game is over:
# conceptual snippet
if is_game_over():
sys.exit(app.exec_())
But this simple solution breaks the for loop. Any feedback would be welcome on how to run and exit a PyQt application multiple times, either sequentially (e.g. for loop approach) or in parallel threads.
You should not use sys.exit() since that instruction serves to terminate the execution of the program, if you want to terminate the Qt application you must use QCoreApplication::quit() (or QCoreApplication::exit(0)). In addition, another improvement would be to use multiprocessing:
import random
from multiprocessing import Pool
from PyQt5 import QtCore, QtWidgets
def create_app(i):
app = QtWidgets.QApplication([])
w = QtWidgets.QMainWindow()
w.setWindowTitle("Game-{}".format(i))
w.show()
# emulate end-game
def end_game():
QtCore.QCoreApplication.quit()
timeout = random.randint(1000, 2000) # 1000-2000 milliseconds
QtCore.QTimer.singleShot(timeout, end_game)
app.exec_()
# emulate results
o = {"victory": random.randint(0, 101), "average": random.randint(0, 101)}
return o
def main():
results = []
pool = Pool(processes=8)
for i in range(1000):
r = pool.apply_async(create_app, args=(i,))
results.append(r)
pool.close()
pool.join()
print([result.get() for result in results])
if __name__ == "__main__":
main()
I've to tell you, your question is dangerously near the level of a "too-broad" flag. The real problem is: how do you want to keep track of the gathered data, and what do you want to do with that data? This question may have a lot of answers.
As you already found out, you can't use sys.exit, but you could gather your data in different ways.
If you're going to run your application within a controlled environment, a possible solution is to serialize your gathered data while controlling the whole "gathering" process from the application
Semi-pseudo-code:
from PyQt5 import QtCore, QtWidgets
import json
class BattleShipWindow(QtWidgets.QMainWindow):
restart = QtCore.pyqtSignal()
# ...
def storeData(self, data):
# get the app data location
appDir = QtWidgets.QStandardPath.standardLocations(
QtCore.QStandardaPaths.AppDataLocation)[0]
# if it doesn't exists, create it
if not QtCore.QFile.exists(appDir):
QtCore.QDir().mkpath(appDir)
now = QtCore.QDateTime.currentDateTime()
fileName = str(now.toMSecsSinceEpoch() // 1000)
# write down the data
with open(os.path.join(appDir, fileName), 'w') as df:
df.write(json.dumps(data))
def getGameData(self):
# gather your own data here, this is just an example
return {'gameData': [1, 2, 3]}
def closeEvent(self, event):
if QtWidgets.QMessageBox.question(
self, 'Play again?',
'Play another game?',
QtWidgets.QMessageBox.Yes|QtWidgets.QMessageBox.No
) == QtWidgets.QMessageBox.Yes:
self.storeData(self.getGameData())
self.restart.emit()
class BattleShipApp(QtWidgets.QApplication):
def restart(self):
self.currentGame = BattleShipWindow()
self.currentGame.restart.connect(self.restart)
self.currentGame.show()
def exec_(self):
self.currentGame = BattleShipWindow()
self.currentGame.restart.connect(self.restart)
self.currentGame.show()
super(BattleShipApp, self).exec_()
if __name__ == '__main__':
import sys
app = BattleShipApp(sys.argv)
sys.exit(app.exec_())
Note: this is a [semi]pseudo code, I obviously didn't test it. Its purpose is to show how it could behave, so don't expect it to work as it is.
Related
I have rewritten the question so that it is more clear.
In my code I created a QApplication, connected a slot to the application using QTimer.singleShot(), then executed my application.
Now in this slot I want to create another QApplication in another process, I used multiprocessing.Process Class and from inside the process I try to start another QApplication and execute it, but I have an error because an event loop is already running!, I know I can't run two event loops but I am running the new QApplication in another process so it should run.
I know this is not a common implementation but it would be much easier to get this running in my case.
Here is a code example:
The error I get is "QCoreApplication::exec: The event loop is already running"
import multiprocessing
from PyQt4 import QtCore,QtGui
def first_app_slot():
mProcess = multiprocessing.Process(target = run_another_app)
mProcess.start()
mProcess.join()
def run_another_app():
second_app = QtGui.QApplication([])
second_app.exec_()
if __name__ == "__main__":
first_app = QtGui.QApplication([])
QtCore.QTimer.singleShot(0,first_app_slot)
first_app.exec_()
A few problems
In your code, you're not calling any of the multiprocessing code (maybe a typo)?
Don't create the first QApplication in the global scope, put it inside a function. Before creating a new process, multiprocessing will copy the global state to the new process, which includes first_app in this case.
Ex.
def main():
first_app = QtGui.QApplication(sys.argv)
...
if __name__ == '__main__':
main()
I'd appreciate some help on using the QtHelpEngine. I've tried a few approaches with various results. Below is the code that seems most intuitive to me but I run in to threading problems. The problem I'm having now is to use QT signals to perform the necessary actions in the correct sequence.
The error I get is this:
QObject: Cannot create children for a parent that is in a different
thread. (Parent is QHelpEngine(0x226f6143780), parent's thread is
QmlSearch(0x226f61382a0), current thread is QThread(0x226f61bda80)
I'd appreciate if someone could give me some advice on how to solve this or how the help engine is intended to be used.
Thanks!
from PyQt5 import QtHelp, QtCore
import sys
import time
class QmlSearch(QtCore.QThread):
def run(self):
# setup help engine
qhcPath = 'C:/Users/jonoa/Documents/TEST.qhc' # just a temporary test file
self.qmlHelp = QtHelp.QHelpEngine(qhcPath)
self.qmlHelp.setupFinished.connect(self.onSetupFinished)
self.qmlHelp.setupData()
def onSetupFinished(self):
print('setup finished')
# when setup is finished register documentation
path2 = 'C:/Program Files/QT/Docs/Qt-5.7/qtquick.qch' # an example test file
self.qmlHelp.registerDocumentation(path2)
# Then setup the search engine
self.qmlSearch = self.qmlHelp.searchEngine() # This is where the script breaks.
self.qmlSearch.reindexDocumentation()
def onIndexFinished(self):
print('indexing finished')
# create search query and perform search
query = QtHelp.QHelpSearchQuery(0, ['rectangle'])
self.qmlSearch.searchingFinished.connect(self.onSearchFinished)
self.qmlSearch.search([query])
def onSearchFinished(self):
print('search finished')
# print some of the hits and finish
print([a for a in self.qmlSearch.hits(0, self.qmlSearch.hitCount()) if 'rectangle' in a[1].lower()])
self.finished.emit()
if __name__ == '__main__':
app = QtCore.QCoreApplication(sys.argv)
thread = QmlSearch()
thread.finished.connect(app.exit)
thread.start()
sys.exit(app.exec_())
I have a GUI in PyQt with a function addImage(image_path). Easy to imagine, it is called when a new image should be added into a QListWidget. For the detection of new images in a folder, I use a threading.Thread with watchdog to detect file changes in the folder, and this thread then calls addImage directly.
This yields the warning that QPixmap shouldn't be called outside the gui thread, for reasons of thread safety.
What is the best and most simple way to make this threadsafe? QThread? Signal / Slot? QMetaObject.invokeMethod? I only need to pass a string from the thread to addImage.
You should use the built in QThread provided by Qt. You can place your file monitoring code inside a worker class that inherits from QObject so that it can use the Qt Signal/Slot system to pass messages between threads.
class FileMonitor(QObject):
image_signal = QtCore.pyqtSignal(str)
#QtCore.pyqtSlot()
def monitor_images(self):
# I'm guessing this is an infinite while loop that monitors files
while True:
if file_has_changed:
self.image_signal.emit('/path/to/image/file.jpg')
class MyWidget(QtGui.QWidget):
def __init__(self, ...)
...
self.file_monitor = FileMonitor()
self.thread = QtCore.QThread(self)
self.file_monitor.image_signal.connect(self.image_callback)
self.file_monitor.moveToThread(self.thread)
self.thread.started.connect(self.file_monitor.monitor_images)
self.thread.start()
#QtCore.pyqtSlot(str)
def image_callback(self, filepath):
pixmap = QtGui.QPixmap(filepath)
...
I believe the best approach is using the signal/slot mechanism. Here is an example. (Note: see the EDIT below that points out a possible weakness in my approach).
from PyQt4 import QtGui
from PyQt4 import QtCore
# Create the class 'Communicate'. The instance
# from this class shall be used later on for the
# signal/slot mechanism.
class Communicate(QtCore.QObject):
myGUI_signal = QtCore.pyqtSignal(str)
''' End class '''
# Define the function 'myThread'. This function is the so-called
# 'target function' when you create and start your new Thread.
# In other words, this is the function that will run in your new thread.
# 'myThread' expects one argument: the callback function name. That should
# be a function inside your GUI.
def myThread(callbackFunc):
# Setup the signal-slot mechanism.
mySrc = Communicate()
mySrc.myGUI_signal.connect(callbackFunc)
# Endless loop. You typically want the thread
# to run forever.
while(True):
# Do something useful here.
msgForGui = 'This is a message to send to the GUI'
mySrc.myGUI_signal.emit(msgForGui)
# So now the 'callbackFunc' is called, and is fed with 'msgForGui'
# as parameter. That is what you want. You just sent a message to
# your GUI application! - Note: I suppose here that 'callbackFunc'
# is one of the functions in your GUI.
# This procedure is thread safe.
''' End while '''
''' End myThread '''
In your GUI application code, you should create the new Thread, give it the right callback function, and make it run.
from PyQt4 import QtGui
from PyQt4 import QtCore
import sys
import os
# This is the main window from my GUI
class CustomMainWindow(QtGui.QMainWindow):
def __init__(self):
super(CustomMainWindow, self).__init__()
self.setGeometry(300, 300, 2500, 1500)
self.setWindowTitle("my first window")
# ...
self.startTheThread()
''''''
def theCallbackFunc(self, msg):
print('the thread has sent this message to the GUI:')
print(msg)
print('---------')
''''''
def startTheThread(self):
# Create the new thread. The target function is 'myThread'. The
# function we created in the beginning.
t = threading.Thread(name = 'myThread', target = myThread, args = (self.theCallbackFunc))
t.start()
''''''
''' End CustomMainWindow '''
# This is the startup code.
if __name__== '__main__':
app = QtGui.QApplication(sys.argv)
QtGui.QApplication.setStyle(QtGui.QStyleFactory.create('Plastique'))
myGUI = CustomMainWindow()
sys.exit(app.exec_())
''' End Main '''
EDIT
Mr. three_pineapples and Mr. Brendan Abel pointed out a weakness in my approach. Indeed, the approach works fine for this particular case, because you generate / emit the signal directly. When you deal with built-in Qt signals on buttons and widgets, you should take another approach (as specified in the answer of Mr. Brendan Abel).
Mr. three_pineapples adviced me to start a new topic in StackOverflow to make a comparison between the several approaches of thread-safe communication with a GUI. I will dig into the matter, and do that tomorrow :-)
My PyQt program has 2 widgets (selecting files etc) and then a Main Window which displays the results of the parsed files.
The program works great for small sample files, but when trying to parse larger files it will hang (display "Not Responding") and then show the results after about 30 seconds or so.
I would like to implement a QDialog before the Main Window opens. The QDialog will have a progress bar to let the user know when the Main Window will open.
This progress bar needs to be set to the length of time before the Main Window pops up.
What is the best way to implement this? I have seen some examples, but the progress bar is just set to a standardised time, not when the processing(parsing) is complete.
I currently have the following code which opens the Main Window.
def openWidgetMain(self):
self.WidgetMain = WidgetMain()
self.WidgetMain.show()
self.close()
All the processing for this window is done when it opens. So how do I connect the QProgressBar?
Put your long lasting process in some kind of thread. Read this: http://qt-project.org/doc/qt-5/threads-technologies.html
Emit a signal from that thread to update your progress bar. This way your application will not hang and the user sees the progress.
However it is up to your loading routine to decide which percentage to show in the progress bar. If you can't calculate an exact percentage try some kind of estimation (e.g. based on the size of the file vs. processed amount of the file).
First, best way to implement this, Your must estimate your load progress file. Next, implement it with QtCore.QThread to create background process. Last, put your call back progress into your QtGui.QMainWindow.
Little example;
import sys
import time
from PyQt4 import QtGui
from PyQt4 import QtCore
class QCustomThread (QtCore.QThread):
startLoad = QtCore.pyqtSignal(int)
progressLoad = QtCore.pyqtSignal(int)
statusLoad = QtCore.pyqtSignal(bool)
def __init__ (self, parentQWidget = None):
super(QCustomThread, self).__init__(parentQWidget)
self.wasCanceled = False
def run (self):
# Simulate data load estimation
numberOfprogress = 100
self.startLoad.emit(numberOfprogress)
for progress in range(numberOfprogress + 1):
# Delay
time.sleep(0.1)
if not self.wasCanceled:
self.progressLoad.emit(progress)
else:
break
self.statusLoad.emit(True if progress == numberOfprogress else False)
self.exit(0)
def cancel (self):
self.wasCanceled = True
class QCustomMainWindow (QtGui.QMainWindow):
def __init__ (self):
super(QCustomMainWindow, self).__init__()
# Create action with QPushButton
self.startQPushButton = QtGui.QPushButton('START')
self.startQPushButton.released.connect(self.startWork)
self.setCentralWidget(self.startQPushButton)
# Create QProgressDialog
self.loadingQProgressDialog = QtGui.QProgressDialog(self)
self.loadingQProgressDialog.setLabelText('Loading')
self.loadingQProgressDialog.setCancelButtonText('Cancel')
self.loadingQProgressDialog.setWindowModality(QtCore.Qt.WindowModal)
def startWork (self):
myQCustomThread = QCustomThread(self)
def startLoadCallBack (numberOfprogress):
self.loadingQProgressDialog.setMinimum(0)
self.loadingQProgressDialog.setMaximum(numberOfprogress)
self.loadingQProgressDialog.show()
def progressLoadCallBack (progress):
self.loadingQProgressDialog.setValue(progress)
def statusLoadCallBack (flag):
print 'SUCCESSFUL' if flag else 'FAILED'
myQCustomThread.startLoad.connect(startLoadCallBack)
myQCustomThread.progressLoad.connect(progressLoadCallBack)
myQCustomThread.statusLoad.connect(statusLoadCallBack)
self.loadingQProgressDialog.canceled.connect(myQCustomThread.cancel)
myQCustomThread.start()
myQApplication = QtGui.QApplication(sys.argv)
myQCustomMainWindow = QCustomMainWindow()
myQCustomMainWindow.show()
sys.exit(myQApplication.exec_())
More infomation of QtCore.QThread (Recommend read to understand behavior)
I am trying to understand how to use signaling from a Qthread back to the Gui interface that started.
Setup: I have a process (a simulation) that needs to run almost indefinitely (or at least for very long stretches of time)., While it runs, it carries out various computations, amd some of the results must be sent back to the GUI, which will display them appropriately in real time.
I am using PyQt for the GUI. I originally tried using python's threading module, then switched to QThreads after reading several posts both here on SO and elsewhere.
According to this post on the Qt Blog You're doing it wrong, the preferred way to use QThread is by creating a QObject and then moving it to a Qthread. So I followed the advice inBackground thread with QThread in PyQt"> this SO question and tried a simple test app (code below): it opens up a simple GUI, let you start the background process, and it issupposed to update the step value in a spinbox.
But it does not work. The GUI is never updated. What am I doing wrong?
import time, sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class SimulRunner(QObject):
'Object managing the simulation'
stepIncreased = pyqtSignal(int, name = 'stepIncreased')
def __init__(self):
super(SimulRunner, self).__init__()
self._step = 0
self._isRunning = True
self._maxSteps = 20
def longRunning(self):
while self._step < self._maxSteps and self._isRunning == True:
self._step += 1
self.stepIncreased.emit(self._step)
time.sleep(0.1)
def stop(self):
self._isRunning = False
class SimulationUi(QDialog):
'PyQt interface'
def __init__(self):
super(SimulationUi, self).__init__()
self.goButton = QPushButton('Go')
self.stopButton = QPushButton('Stop')
self.currentStep = QSpinBox()
self.layout = QHBoxLayout()
self.layout.addWidget(self.goButton)
self.layout.addWidget(self.stopButton)
self.layout.addWidget(self.currentStep)
self.setLayout(self.layout)
self.simulRunner = SimulRunner()
self.simulThread = QThread()
self.simulRunner.moveToThread(self.simulThread)
self.simulRunner.stepIncreased.connect(self.currentStep.setValue)
self.connect(self.stopButton, SIGNAL('clicked()'), self.simulRunner.stop)
self.connect(self.goButton, SIGNAL('clicked()'), self.simulThread.start)
self.connect(self.simulRunner,SIGNAL('stepIncreased'), self.currentStep.setValue)
if __name__ == '__main__':
app = QApplication(sys.argv)
simul = SimulationUi()
simul.show()
sys.exit(app.exec_())
The problem here is simple: your SimulRunner never gets sent a signal that causes it to start its work. One way of doing that would be to connect it to the started signal of the Thread.
Also, in python you should use the new-style way of connecting signals:
...
self.simulRunner = SimulRunner()
self.simulThread = QThread()
self.simulRunner.moveToThread(self.simulThread)
self.simulRunner.stepIncreased.connect(self.currentStep.setValue)
self.stopButton.clicked.connect(self.simulRunner.stop)
self.goButton.clicked.connect(self.simulThread.start)
# start the execution loop with the thread:
self.simulThread.started.connect(self.simulRunner.longRunning)
...