I have created a UI with PyQt5. I can use it on Windows and it works perfectly, but when I try to use it on MacOS I get stuck trying to close it (with self.close()). Using the PyCharm debugger I found out that after self.close() it jumps to app.exec_() and the function that was entered to close it is executed again (for example on_later_button_clicked(self)). I have also already tried sys.exit(app.exec_()).
Here is my code:
import os
import sys
from PyQt5 import QtGui, QtWidgets
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import QApplication, QDialog
from PyQt5.uic import loadUi
from Modules.database import addNeverID
from Modules.supportedWebsites import getWebsites
def Start():
m = askForPartnerUrl()
# m.setFixedSize(500,500)
m.show()
return m
class askForPartnerUrl(QDialog):
def __init__(self):
super(askForPartnerUrl, self).__init__()
loadUi('lib/askForPartnerURL.ui', self)
self.setWindowTitle('Upload')
current_id = getFromFile("id.txt")
self.show_id.setText(current_id)
self.show_origin_url.setText(
'' + getFromFile("origin_url.txt") + '')
self.show_origin_url.setOpenExternalLinks(True)
id_beginns = ["1"]
website_eq = ["1"]
website_guess_str = "Nicht verfügbar!"
for i in range(len(id_beginns)):
if id_beginns[i] in current_id:
website_guess_str = '' + website_eq[i] + ''
self.website_guess.setOpenExternalLinks(True)
break
self.website_guess.setText(website_guess_str)
self.save_button.clicked.connect(self.on_save_button_clicked)
self.later_button.clicked.connect(self.on_later_button_clicked)
self.never_button.clicked.connect(self.on_never_button_clicked)
try:
os.remove('temp/currentObject/partner_url.txt')
except:
pass
#pyqtSlot()
def on_never_button_clicked(self):
addNeverID(getFromFile("id.txt"))
saveToFile("Never-ID", "partner_url.txt")
self.close()
def on_later_button_clicked(self):
saveToFile("Later-ID", "partner_url.txt")
self.close()
def on_save_button_clicked(self):
url_is_valid = False
for i in getWebsites():
if i in self.partner_url_input.text():
url_is_valid = True
break
if url_is_valid:
saveToFile(self.partner_url_input.text(), "partner_url.txt")
self.close()
else:
error_dialog = QtWidgets.QErrorMessage(self)
error_dialog.setWindowTitle("Eingabe nicht verwertbar")
error_dialog.showMessage('Die eingegebene URL ist nicht verwendbar! Bitte prüfe deine Eingabe.')
def showGUI():
app = QApplication(sys.argv)
app.setStyle('Fusion')
app.setWindowIcon(QtGui.QIcon('lib/icon.png'))
window = Start()
app.exec_()
def saveToFile(content, filename):
file = open("temp/currentObject/" + filename, "w+")
file.write(content)
file.close()
def getFromFile(filename):
file = open("temp/currentObject/" + filename)
content = file.read()
file.close()
return content
Many thanks in advance
The reason is that since you're using uic, it automatically enables the auto-connection feature, which automatically detects function names based on object/signals names and connects them, even if the functions do not have Qt slots decorators.
The result is that your slot will be actually called thrice:
without any argument (clicked());
with the checked argument (clicked(bool)): the argument is ignored by Qt since the function doesn't take any, but the function will be called anyway because no slot signature has been specified for it;
again with the checked argument, because you manually connected it in your code;
If you want to keep using the auto connection, use a unique slot decorator for that specific function, otherwise manually connect to a function (possibly with a slot, if you need a specific signature) that does not use the auto connection naming, but don't use both.
class askForPartnerUrl(QDialog):
def __init__(self):
super(askForPartnerUrl, self).__init__()
loadUi('askForPartnerURL.ui', self)
# ...
# remove the following lines:
# self.save_button.clicked.connect(self.on_save_button_clicked)
# self.later_button.clicked.connect(self.on_later_button_clicked)
# self.never_button.clicked.connect(self.on_never_button_clicked)
# manual connection
self.later_button.clicked.connect(self.saveLater)
# using the auto connection; the function doesn't need arguments, so
# you can ignore the argument type signature
#pyqtSlot()
def on_never_button_clicked(self):
addNeverID(getFromFile("id.txt"))
# ...
# with a normal function; in this case no slot decorator is required since
# you don't have arguments
def saveLater(self):
url_is_valid = False
# ...
PS: The reason for which it gets "stuck" is probably due to the way Python deals with the end of the program (which by default happens as soon as the last window is closed in Qt) on MacOS: after the first call to close() PyQt tries to quit the QApplication (free up memory, etc...), but while doing so the original click event is still in the process of firing the signals to the remaining second and third slot, hence the "loop" (but it's not an actual loop, and the third slot never gets called because it's the second one that blocks everything).
Note that this is a big oversimplification, I'm not an expert in memory usage and low level programming, but this is fundamentally what's happening.
Related
I am writing a package to send/receive frames over a CAN bus monitored by a GUI, using pyqt5.
Here is the process :
Create A new QThread object (called tx_thread)separate from main window thread.
Create a TxWorker (class inheriting from QObject), called tx_worker.
Move it to tx_thread
Setup signal/slot connexion
Start tx_thread
Create a TxJob object (class inheriting from QObject)
Move it to tx_thread
Pass it to tx_worker through signal/slot connexion, so that tx_worker handles the TxJob by itself afterwards.
I have used the above process many times for other cases and it usually works perfectly.
However, in this case I get an error : it seems that the TxJob object "loses" its type during the call through signal/slot and is reverted to a mere QObject without the additional TxJob attributes. That is very weird to me because I specified the type of signal parameter and slot signature.
Does anyone understand what the issue is and what I should do to fix it ?
-- EDIT --
I have found a workaround to avoid the error : I send constructor parameters instead of the object in the signal emit method, then I create the TxJob object from within the receiving thread.
Nevertheless, I want to understand why my first approach did not work, for the sake of my knowledge in python and/or Qt. I would be glad to get any leads on this.
--
I condensed my code into the following files to reproduce the issue.
When executing test_main.py, I get :
Before emitting signal, type is : <class 'test_tx_job.TxJob'>
After entering slot, type is : <class 'PyQt5.QtCore.QObject'>
'QObject' object has no attribute 'set_active'
The error arises when executing job.set_active(True) in method add_tx_job() of class TxWorker
test_main.py
import sys
from test_iocan import IoCan
from PyQt5.QtWidgets import QApplication
def main():
app = QApplication(sys.argv)
io_can = IoCan()
io_can.new_tx_task()
io_can.show()
sys.exit(app.exec_()) # Start event loop
if __name__ == '__main__':
main()
test_iocan.py
from test_tx_job import TxJob
from test_tx_worker import TxWorker
from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtWidgets import QMainWindow
class IoCan(QMainWindow):
sig_add_tx_job = pyqtSignal(TxJob)
def __init__(self):
super(IoCan, self).__init__()
self.tx_thread = QThread(parent=self) # Step 0
self.tx_worker = TxWorker() # Step 1
self.tx_worker.moveToThread(self.tx_thread) # Step 2
self.sig_add_tx_job.connect(self.tx_worker.add_tx_job) # Step 3
self.tx_thread.start(priority=QThread.TimeCriticalPriority) # Step 4
def closeEvent(self, event):
"""Override of the close event default method."""
# Needed to cleanly terminate threads
self.threads_stop()
super(IoCan, self).closeEvent(event)
def new_tx_task(self):
new_job = TxJob() # Step 5
new_job.moveToThread(self.tx_thread) # Step 6
print('Before emitting signal, type is : ' + str(type(new_job)))
self.sig_add_tx_job.emit(new_job) # Step 7
def threads_stop(self):
self.tx_thread.quit()
self.tx_thread.wait()
test_tx_worker.py
from test_tx_job import TxJob
from PyQt5.QtCore import QObject, pyqtSlot
class TxWorker(QObject):
def __init__(self):
super(TxWorker, self).__init__()
self.tx_jobs = []
#pyqtSlot(TxJob)
def add_tx_job(self, job: TxJob): # Called at step 7 in tx_thread
print('After entering slot, type is : ' + str(type(job)))
try:
job.set_active(True)
except AttributeError as err:
print(err.args[0])
else:
print(type(job))
self.tx_jobs.append(job)
test_tx_job.py
from PyQt5.QtCore import QObject
class TxJob(QObject):
def __init__(self):
super(TxJob, self).__init__()
self._is_active = False
def set_active(self, value: bool):
self._is_active = value
I am developing an application that reads MS Access DBs to produce extracts and data manipulation into SharePoint. One of my challenges is that I will need to write a lengthy GUI interface with many simple events. To keep the code organized I will be writing subroutines and import into the main program. As a result there will be code that needs to run outside the class and access objects inside the class. I am having a problem determining the proper syntax.
Or is there a way to import directly into the class to avoid the redefining issue?
In the InitUI there is a call to subroutine called TestIt() in that sub routine I print a message to verify the code is getting called. Then I try to set the text value of a Single Line Edit object from the main window. The question what is the proper way to call it? Do I need to redefine the variable.
The main program is this:
from PyQt5 import QtCore, QtGui, QtWidgets
from Main import Ui_MainWin
import sys, time, os, configparser
import pyodbc
from ServerQueue import *
class MainWindow_EXEC():
def __init__(self): # This section has to be laid out this way
app = QtWidgets.QApplication(sys.argv)
win = QtWidgets.QMainWindow()
self.ui = Ui_MainWin()
self.ui.setupUi(win)
self.initUI() # Inits that need to happen before start up
win.setWindowTitle("This is the Title!")
#win.resize(800,600)
win.show()
sys.exit(app.exec_())
def initUI(self):
TestIt()
self.ui.cmbAppCodes.currentIndexChanged.connect(self.selectionchange)
try:
self.conn = pyodbc.connect(r'Driver={Microsoft Access Driver
(*.mdb,*.accdb)};DBQ='+ dbPath + ';',autocommit=True)
self.cursor = self.conn.cursor()
except:
print("Error opening the database!")
qApp.quit()
print(dbPath)
self.loadAppCodes()
def loadAppCodes(self):
self.ui.cmbAppCodes.clear()
sql = "SELECT [App Code], [App Name] from [App List] ORDER BY [App Code];"
self.cursor.execute(sql)
res = self.cursor.fetchall()
for code in res:
self.ui.cmbAppCodes.addItem("{} - {}".format(code[0], code[1]))
def selectionchange(self,i):
App = self.ui.cmbAppCodes.itemText(i)
sql = "SELECT * FROM [LCM Application List] WHERE [App Code] = '" + App[0:3] + "';"
self.cursor.execute(sql)
res = self.cursor.fetchone()
self.ui.lneAPM.setText(res[3])
self.ui.lneTPM.setText(res[21])
self.ui.lneAPO.setText(res[4])
#-----------------------------------------------------------------------------
# Load up the init file
#-----------------------------------------------------------------------------
def iniSettings():
if sys.platform == "win32":
os.system('cls')
Home = os.path.expanduser('~') + "\\"
D = "W"
else:
os.system('clear')
Home = os.path.expanduser('~') + "/"
D = "O"
config = configparser.ConfigParser()
config.read(Home + 'LCM.ini') # Path to LCM.ini file
Configs = config['DEFAULT'] # Read the DEFAULT section
return [Home, Configs['LCMDB'], Configs['CR1'], D]
##########################################################################################
# Main Body
###########################################################################################
if __name__ == "__main__":
Paths = iniSettings() # This is to pull the ini data for paths and what not.
hmPath = Paths[0]
dbPath = Paths[1]
cr1Path = Paths[2]
myOS = Paths[3]
print("Home: {}\nCR1: {}\nDatabase: {}\n".format(hmPath, cr1Path, dbPath))
MainWindow_EXEC() # Becuase the init is laid out this way this is all that is needed
print("Application completed")enter code here
#This file is imported and is called ServerQueue.py
def TestIt():
print("TestIt")
self.ui.lneAPM.setText("Testing")
#-----------------------------------------------------------------------------
# Main Processing Loop
#-----------------------------------------------------------------------------
if __name__ == "__main__":
print("\n ********* \n This file is not meant to be run directly! \n ********* \n")
You are doing everything backwards. You are trying to get the elements of business logic to consume the GUI, but the correct logic is the reverse: The GUI consumes the information from the elements of business logic. Considering the above, the Test () function should not modify the GUI directly, but provide the information to the GUI, one way to implement that is to return the text.
def TestIt():
print("TestIt")
return "Testing"
# ...
def initUI(self):
self.ui.lneAPM.setText(TestIt())
# ...
If the above does not solve the problem then you probably have an XY problem so I recommend you to rewrite your question better providing a better MRE
[edit] This is not a pure duplicate of the PySide emit signal causes python to crash question. This question relates specifically to a (now) known bug in PySide preventing None from being passed across threads. The other question relates to hooking up signals to a spinner box. I've updated the title of this question to better reflect the problem I was facing. [/edit]
I've banged my head against a situation where PySide behaves subtly different from PyQt. Well, I say subtly but actually PySide crashes Python whereas PyQt works as I expect.
I'm completely new to PySide and still fairly new to PyQt so maybe I'm making some basic mistake, but damned if I can figure it out... really hoping one of you fine folks can give some pointers!
The full app is a batch processing tool and much too cumbersome to describe here, but I've stripped the problem down to its bare essentials in the code-sample below:
import threading
try:
# raise ImportError() # Uncomment this line to show PyQt works correctly
from PySide import QtCore, QtGui
except ImportError:
from PyQt4 import QtCore, QtGui
QtCore.Signal = QtCore.pyqtSignal
QtCore.Slot = QtCore.pyqtSlot
class _ThreadsafeCallbackHelper(QtCore.QObject):
finished = QtCore.Signal(object)
def Dummy():
print "Ran Dummy"
# return '' # Uncomment this to show PySide *not* crashing
return None
class BatchProcessingWindow(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self, None)
btn = QtGui.QPushButton('do it', self)
btn.clicked.connect(lambda: self._BatchProcess())
def _BatchProcess(self):
def postbatch():
pass
helper = _ThreadsafeCallbackHelper()
helper.finished.connect(postbatch)
def cb():
res = Dummy()
helper.finished.emit(res) # `None` crashes Python under PySide??!
t = threading.Thread(target=cb)
t.start()
if __name__ == '__main__': # pragma: no cover
app = QtGui.QApplication([])
BatchProcessingWindow().show()
app.exec_()
Running this displays a window with a "do it" button. Clicking it crashes Python if running under PySide. Uncomment the ImportError on line 4 to see PyQt* correctly run the Dummy function. Or uncomment the return statement on line 20 to see PySide correctly run.
I don't understand why emitting None makes Python/PySide fail so badly?
The goal is to offload the processing (whatever Dummy does) to another thread, keeping the main GUI thread responsive. Again this has worked fine with PyQt but clearly not so much with PySide.
Any and all advice will be super appreciated.
This is under:
Python 2.7 (r27:82525, Jul 4 2010, 09:01:59) [MSC v.1500 32 bit (Intel)] on win32
>>> import PySide
>>> PySide.__version_info__
(1, 1, 0, 'final', 1)
>>> from PyQt4 import Qt
>>> Qt.qVersion()
'4.8.2'
So, if the argument is that PySide is neglected and this really is a bug, we might as well come up with a workaround, right?
By introducing a sentinel to replace None, and emitting it the problem can be circumvented, then the sentinel just has to be swapped back to None in the callbacks and the problem is bypassed.
Good grief though. I'll post the code I've ended up with to invite further comments, but if you got better alternatives or actual solutions then do give a shout. In the meantime I guess this'll do:
_PYSIDE_NONE_SENTINEL = object()
def pyside_none_wrap(var):
"""None -> sentinel. Wrap this around out-of-thread emitting."""
if var is None:
return _PYSIDE_NONE_SENTINEL
return var
def pyside_none_deco(func):
"""sentinel -> None. Decorate callbacks that react to out-of-thread
signal emitting.
Modifies the function such that any sentinels passed in
are transformed into None.
"""
def sentinel_guard(arg):
if arg is _PYSIDE_NONE_SENTINEL:
return None
return arg
def inner(*args, **kwargs):
newargs = map(sentinel_guard, args)
newkwargs = {k: sentinel_guard(v) for k, v in kwargs.iteritems()}
return func(*newargs, **newkwargs)
return inner
Modifying my original code we arrive at this solution:
class _ThreadsafeCallbackHelper(QtCore.QObject):
finished = QtCore.Signal(object)
def Dummy():
print "Ran Dummy"
return None
def _BatchProcess():
#pyside_none_deco
def postbatch(result):
print "Post batch result: %s" % result
helper = _ThreadsafeCallbackHelper()
helper.finished.connect(postbatch)
def cb():
res = Dummy()
helper.finished.emit(pyside_none_wrap(res))
t = threading.Thread(target=cb)
t.start()
class BatchProcessingWindow(QtGui.QDialog):
def __init__(self):
super(BatchProcessingWindow, self).__init__(None)
btn = QtGui.QPushButton('do it', self)
btn.clicked.connect(_BatchProcess)
if __name__ == '__main__': # pragma: no cover
app = QtGui.QApplication([])
window = BatchProcessingWindow()
window.show()
sys.exit(app.exec_())
I doubt that'll win any awards, but it does seem to fix the issue.
In my PyQt4 application, there is a functionality that allows users to save a avi file.
To this aim, a saveMovie method has been implemented in the main window:
def saveMovie(self):
""" Let the user make a movie out of the current experiment. """
filename = QtGui.QFileDialog.getSaveFileName(self, "Export Movie", "",
'AVI Movie File (*.avi)')
if filename != "":
dialog = QtGui.QProgressDialog('',
QtCore.QString(),
0, 100,
self,
QtCore.Qt.Dialog |
QtCore.Qt.WindowTitleHint)
dialog.setWindowModality(QtCore.Qt.WindowModal)
dialog.setWindowTitle('Exporting Movie')
dialog.setLabelText('Resampling...')
dialog.show()
make_movie(self.appStatus, filename, dialog)
dialog.close()
My idea is to use a QProgressDialog to show how the video encoding work is proceeding.
Nevertheless, after the selection of the filename, the QFileDialog won't disappear and the entire application stays unresponsive until the make_movie function has completed.
What should I do to avoid this?
Lesson learned: if you have some long-running operations to do -- for example, reading or writing a big file, move them to another thread or they will freeze the UI.
Therefore, I created a subclass of QThread, MovieMaker, whose run method encapsulates the functionality previosly implemented by make_movie:
class MovieMaker(QThread):
def __init__(self, uAppStatus, uFilename):
QtCore.QThread.__init__(self, parent=None)
self.appStatus = uAppStatus
self.filename = uFilename
def run(self):
## make the movie and save it on file
Let's move back to the saveMovie method. Here, I replaced the original call to make_movie with the following code:
self.mm = MovieMaker(self.appStatus,
filename)
self.connect(self.mm, QtCore.SIGNAL("Progress(int)"),
self.updateProgressDialog)
self.mm.start()
Note how I defined a new signal, Progress(int).
Such a signal is emitted by the MovieMaker thread to update the QProgressDialog used to show the user how the movie encoding work is progressing.
I don't know the first thing about Qt, but I'm trying to be cheeky and borrow code from elsewhere (http://lateral.netmanagers.com.ar/weblog/posts/BB901.html#disqus_thread). ;)
I have a problem. When I run test() the first time, everything works swimmingly. However, when I run it the second time, I get nasty segfaults. I suspect that the problem is that I'm not ending the qt stuff correctly. What should I change about this program to make it work multiple times? Thanks in advance!
from PyQt4 import QtCore, QtGui, QtWebKit
import logging
logging.basicConfig(level=logging.DEBUG)
class Capturer(object):
"""A class to capture webpages as images"""
def __init__(self, url, filename, app):
self.url = url
self.app = app
self.filename = filename
self.saw_initial_layout = False
self.saw_document_complete = False
def loadFinishedSlot(self):
self.saw_document_complete = True
if self.saw_initial_layout and self.saw_document_complete:
self.doCapture()
def initialLayoutSlot(self):
self.saw_initial_layout = True
if self.saw_initial_layout and self.saw_document_complete:
self.doCapture()
def capture(self):
"""Captures url as an image to the file specified"""
self.wb = QtWebKit.QWebPage()
self.wb.mainFrame().setScrollBarPolicy(
QtCore.Qt.Horizontal, QtCore.Qt.ScrollBarAlwaysOff)
self.wb.mainFrame().setScrollBarPolicy(
QtCore.Qt.Vertical, QtCore.Qt.ScrollBarAlwaysOff)
self.wb.loadFinished.connect(self.loadFinishedSlot)
self.wb.mainFrame().initialLayoutCompleted.connect(
self.initialLayoutSlot)
logging.debug("Load %s", self.url)
self.wb.mainFrame().load(QtCore.QUrl(self.url))
def doCapture(self):
logging.debug("Beginning capture")
self.wb.setViewportSize(self.wb.mainFrame().contentsSize())
img = QtGui.QImage(self.wb.viewportSize(), QtGui.QImage.Format_ARGB32)
painter = QtGui.QPainter(img)
self.wb.mainFrame().render(painter)
painter.end()
img.save(self.filename)
self.app.quit()
def test():
"""Run a simple capture"""
app = QtGui.QApplication([])
c = Capturer("http://www.google.com", "google.png", app)
c.capture()
logging.debug("About to run exec_")
app.exec_()
DEBUG:root:Load http://www.google.com
QObject::connect: Cannot connect (null)::configurationAdded(QNetworkConfiguration) to QNetworkConfigurationManager::configurationAdded(QNetworkConfiguration)
QObject::connect: Cannot connect (null)::configurationRemoved(QNetworkConfiguration) to QNetworkConfigurationManager::configurationRemoved(QNetworkConfiguration)
QObject::connect: Cannot connect (null)::configurationUpdateComplete() to QNetworkConfigurationManager::updateCompleted()
QObject::connect: Cannot connect (null)::onlineStateChanged(bool) to QNetworkConfigurationManager::onlineStateChanged(bool)
QObject::connect: Cannot connect (null)::configurationChanged(QNetworkConfiguration) to QNetworkConfigurationManager::configurationChanged(QNetworkConfiguration)
Process Python segmentation fault (this last line is comes from emacs)
You need to handle the QApplication outside of the test functions, sort of like a singleton (it's actually appropriate here).
What you can do is to check if QtCore.qApp is something (or if QApplication.instance() returns None or something else) and only then create your qApp, otherwise, use the global one.
It will not be destroyed after your test() function since PyQt stores the app somewhere.
If you want to be sure it's handled correctly, just setup a lazily initialized singleton for it.
A QApplication should only be initialized once!
It can be used by as many Capture instances as you like, but you should start them in the mainloop.
See: https://doc.qt.io/qt-4.8/qapplication.html
You could also try "del app" after "app.exec_", but I am unsure about the results.
(Your original code runs fine on my system)
I would use urllib instead of webkit:
import urllib
class Capturer:
def capture(self, s_url, s_filename):
s_file_out, httpmessage = urllib.urlretrieve(s_url, s_filename, self.report)
def report(self, i_count, i_chunk, i_size):
print('retrived %5d of %5d bytes' % (i_count * i_chunk, i_size))
def test():
c = Capturer()
c.capture("http://www.google.com/google.png", "google1.png")
c.capture("http://www.google.com/google.png", "google2.png")
if __name__ == '__main__':
test()