PyQt5; problem with threading using QRunnable and QThreadPool - python

It seems that I have a problem with running threads in my application as follows:
in my MainForm class I have:
.
.
self.threadPool = QtCore.QThreadPool()
self.threadPool.setMaxThreadCount(4)
.
.
def openFileFcn(self):
worker = **Worker**(self, **self.ImageViewer.showImages**, self.files)
self.threadPool.start(worker)
where Worker is a wrapper QRunnable defined as:
class Worker(QtCore.QRunnable):
def __init__(self, fn, *args, **kwargs):
QtCore.QRunnable.__init__(self)
self.fn = fn
self.args = args
self.kwargs = kwargs
def run(self) -> None:
self.fn(*self.args, **self.kwargs)
and self.ImageViewer.showImages refers to the below section of the ImageViewer class:
def loadImg(self, file):
tabName = str()
if len(file) > 15: tabName = file[0:13] + "..."
widget = ImageViewer()
localPath = os.path.abspath(file)
localPath = QtCore.QDir().filePath(localPath)
pixmap = QtGui.QPixmap(localPath)
widget.setImage(pixmap)
self.tabwidget.addTab(widget, self.mainFormHandle.sharedData.imgIcon, tabName)
self.tabwidget.setCurrentIndex(self.tabwidget.currentIndex() + 1)
def **showImages**(self, files):
files = [file.lower() for file in files]
for file in files:
self.loadImg(file)
Nothing much happens when I run this code. It just freezes and after a while the application closes with exit code -1073740791 (0xC0000409). What do you suggest to be the cause?

The issue seems to be that you are trying to create widgets from another thread than the main thread. You can only create widgets from the main thread but since loadImg() creates several widgets, calling it from the thread pool crashes your application. One way to get around this is to split loadImg() into two methods, one method that loads the pixmaps, and another one that creates the ImageViewers and adds the tabs. The first one can then be moved to the thread pool, and you can use a signal to automatically call the second one every time a pixmap is loaded. For example:
class TabArea(QtWidgets.QWidget):
pixmap_loaded = QtCore.pyqtSignal(str, QtGui.QPixmap)
def __init__(self, parent=None):
...
self.pixmap_loaded.connect(self.addTab)
def loadImg(self, file):
localPath = os.path.abspath(file)
localPath = QtCore.QDir().filePath(localPath)
pixmap = QtGui.QPixmap(localPath)
# emit pixmap and file name
self.pixmap_loaded.emit(file, pixmap)
def addTab(self, file, pixmap):
tabName = str()
if len(file) > 15: tabName = file[0:13] + "..."
widget = ImageViewer()
widget.setImage(pixmap)
self.tabwidget.addTab(widget, self.mainFormHandle.sharedData.imgIcon, tabName)
self.tabwidget.setCurrentIndex(self.tabwidget.currentIndex() + 1)
def showImages(self, files):
files = [file.lower() for file in files]
for file in files:
self.loadImg(file)

Related

How do I get events for QListWidgetItem becoming visible in PyQT5

I'm using PyQT5 and currently have a QListWidgetItem that has a custom icon of an image thumbnail.
class ThumbWidget(QListWidgetItem):
def __init__(self, filename: str, database):
QListWidgetItem.__init__(self)
self.filename = filename
self.database = database
self.setText(basename(self.filename))
standard_file_icon = QWidget().style().standardIcon(QStyle.SP_FileIcon)
self.setIcon(standard_file_icon)
self.setSizeHint(QSize(THUMB_WIDTH, THUMB_HEIGHT + FILENAME_MARGIN))
def __str__(self):
return f'Thumbnail for {self.filename}'
def load_thumb(self):
metadata = self.database.file_metadata(self.filename)
img_thumb = metadata['thumb']
if img_thumb:
img = QPixmap()
img.loadFromData(img_thumb, 'JPEG')
self.setIcon(QIcon(img))
As you can see, it starts off using a SP_FileIcon by default and loads in a Thumbnail. Currently this happens in the QListWidget using a thread to load all the thumbnails at once.
class MediaBrowser(QListWidget):
def __init__(self, database: Database, viewtab, dir_path):
QListWidget.__init__(self)
self.log = logging.getLogger('mediahug')
self.database = database
self.viewtab = viewtab
self.current_directory = dir_path
# self.current_file_widgets = []
self.thumb_loader_thread = None
...
...
...
def thumb_load_progress(self, msg):
self.log.debug(msg)
if msg == 'thumb-load':
self.viewtab.scan_label_action.setVisible(True)
self.viewtab.scan_thumb_progress_action.setVisible(True)
elif msg == 'thumb-finish':
self.log.debug('Thumb Scan Finished. Hiding Progressbar')
self.viewtab.scan_label_action.setVisible(False)
self.viewtab.scan_thumb_progress_action.setVisible(False)
elif type(msg) == float:
self.viewtab.scan_thumb_progress.setValue(int(msg * 100))
else:
self.log.warning(f'Unknown thumb loader progress message {msg}')
def load_files(self, dir_path):
if self.thumb_loader_thread and self.thumb_loader_thread.isRunning():
self.log.info('Killing Previous Thumbnail Loading Thread')
self.thumb_loader_thread.requestInterruption()
self.thumb_loader_thread.wait(sys.maxsize)
self.log.info('Previous Thumbnail Thread Done')
self.clear()
# Load New File widgets
onlyfiles = [f for f in listdir(dir_path) if isfile(join(dir_path, f))]
for f in onlyfiles:
vid = join(dir_path, f)
self.log.debug(f"Creating File/Thumb Widget {vid}")
self.addItem(ThumbWidget(vid, self.database))
self.thumb_loader_thread = ThumbLoaderThread(self.all_files(), dir_path)
self.thumb_loader_thread.signal.connect(self.thumb_load_progress)
self.thumb_loader_thread.start()
This can take a long time and load a lot of thumbnails for large collections. I would like to ditch that thread entirely and instead, load the thumbnails lazily/more efficiently. The QListWidget gets wrapped inside of a QScrollArea. Is there any means, either via the QListWidget widget or within the QListWidgetItem, to receive events when a particular list item comes into view and opportunistically load the thumbnail at that point? I've looked through the API and can't seem to find anything.

PyQt5: QProgressBar will not update

I have an application where one of its functions is to use pytube to download YouTube videos. Everything works perfectly except for updating the QProgressBar with the download progress. Please see below code:
import pytube
import PyQt5.QtWidgets as QtWidgets
import PyQt5.QtCore as QtCore
class Worker(QtCore.QObject):
finished = QtCore.pyqtSignal()
progress = QtCore.pyqtSignal(str, int, int)
def __init__(self, url, save_path, fname):
super(Worker, self).__init__()
self.url = url
self.save_path = save_path
self.fname = fname
self.perc_dl = 0
self.download_video()
def run(self):
if self.perc_dl < 100:
self.progress.emit('Downloading: {}%'.format(str(self.perc_dl)), int(self.perc_dl), 100)
print(self.perc_dl)
else:
self.progress.emit('Done!', 100, 100)
self.finished.emit()
def download_video(self):
yt = pytube.YouTube(self.url, on_progress_callback=self.progress_function)
stream = yt.streams.filter(progressive=True, file_extension='mp4').get_highest_resolution()
stream.download(output_path=self.save_path, filename=self.fname)
def progress_function(self, stream, chunk, bytes_remaining):
curr = stream.filesize - bytes_remaining
per_downloaded = round((curr / stream.filesize) * 100, 1)
self.perc_dl = per_downloaded
self.run()
class DownloadFromYT(QtWidgets.QDialog):
def __init__(self, url, save_path, fname):
super(DownloadFromYT, self).__init__()
self.url = url
self.save_path = save_path
self.fname = fname
self.vLayoutMaster = QtWidgets.QVBoxLayout()
self.hLayout = QtWidgets.QHBoxLayout()
self.hLayout.setAlignment(QtCore.Qt.AlignRight)
self.label = QtWidgets.QLabel()
self.label.setText('Downloading video')
self.pBar = QtWidgets.QProgressBar()
self.pBar.setInvertedAppearance(True)
self.pBar.setTextVisible(True)
self.pBar.setFixedWidth(200)
self.pBar.setAlignment(QtCore.Qt.AlignCenter)
self.pBar.setMaximum(100)
self.pBar.setFormat('Downloading: ')
self.noButton = QtWidgets.QPushButton('No')
self.noButton.setFixedWidth(50)
self.noButton.hide()
self.yesButton = QtWidgets.QPushButton('Yes')
self.yesButton.setFixedWidth(50)
self.yesButton.hide()
self.vLayoutMaster.addWidget(self.label)
self.vLayoutMaster.addSpacing(10)
self.vLayoutMaster.addWidget(self.pBar)
self.vLayoutMaster.addSpacing(20)
self.hLayout.addWidget(self.noButton)
self.hLayout.addWidget(self.yesButton)
self.vLayoutMaster.addLayout(self.hLayout)
# Signals / slots
self.noButton.clicked.connect(self.reject)
self.yesButton.clicked.connect(self.accept)
# Widget
self.setLayout(self.vLayoutMaster)
self.setWindowTitle('Downloading')
self.setFixedSize(250, 100)
self.show()
# Init download
self.run_thread(url, save_path, fname)
def run_thread(self, url, save_path, fname):
self.thrd = QtCore.QThread()
self.worker = Worker(url, save_path, fname)
self.worker.moveToThread(self.thrd)
self.thrd.start()
self.thrd.started.connect(self.worker.run)
self.worker.finished.connect(self.thrd.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.worker.progress.connect(self.show_progress)
self.thrd.finished.connect(self.thrd.deleteLater)
def show_progress(self, label, n, total):
self.pBar.setFormat(label)
self.pBar.setMaximum(total)
self.pBar.setValue(n)
QtCore.QCoreApplication.processEvents()
if label == 'Done!':
self.thrd.quit()
self.noButton.show()
self.yesButton.show()
self.label.setText('Video has been downloaded. Would you like to set the chosen\n'
'file path as the "Local file" path?')
I have used this basic structure in other parts of my application to display and update a progress bar with no issues, however for some reason here, self.pBar refuses to update when the progress signal is emitted from the Worker class. As you can see, I have a print() statement in the run method to check that the code is actually entering that if block and periodically printing out the download progress to the console, which it is. The application updates when the thread has finished running, so I get the final, completely filled progress bar, but in between the start and end the progress bar does not update as expected.
Any help would be greatly appreciated!
The problem seems to be that despite all your effort to wire up all that thread-worker components correctly, you are still running self.download_video() in the main (UI) thread. And therefore it is blocking refreshing of the UI. If you want to run in in the secondary thread, you must start it in Worker.run() function and not in the Worker constructor. If you wire up all threading correctly, then you should not need to call processEvents() at all to refresh UI.
If you are unsure about the thread in which the download function runs, print the thread ID in your print function and compare it to thread id of the main UI thread. Have a look at https://doc.qt.io/qt-5/qthread.html#currentThread

Terminate a python thread that calls gdal functions

i have a program that uses gdal to process pictures. That is done in a thread that is started with a button click.
Now, i need a way to terminate this thread at any given point, also with a button click. So i defined a function that i call when the button is clicked:
def terminateThread(self):
self.change_button_caption.emit('Start')
self.print_log.emit('Vorgang abgebrochen')
self.show_progress.emit(0)
self.terminate()
When i replace the actual code with a sleep order, this works. But it doesn't when the thread called a gdal function.
How can i achieve the termination of this thread at any time?
this is the thread:
class MapThread(QThread):
print_log = Signal(str)
show_progress = Signal(int)
change_button_caption = Signal(str)
def __init__(self, path, tab_filelist, targetpath):
QThread.__init__(self)
self.path = path
self.tab_filelist = tab_filelist
self.targetpath = targetpath
def run(self):
self.change_button_caption.emit('Stop')
print('MapThread run', flush=True)
# try:
from osgeo import gdal
from osgeo import osr
self.show_progress.emit(0)
filename = self.tab_filelist[0].rsplit('\\', 1)[1].rsplit('.', 1)[0]
path2 = self.targetpath + "\\" + filename + ".vrt"
pathout = self.targetpath + "\\" + filename + ".mbtiles"
d = gdal.Open(self.path)
proj = osr.SpatialReference(wkt=d.GetProjection())
self.print_log.emit('EPSG Code der Bilddaten: ' + proj.GetAttrValue('AUTHORITY', 1))
# Vituelles Raster erzeugen
vrt_options = gdal.BuildVRTOptions(resampleAlg='cubic', outputSRS=proj)
gdal.BuildVRT(path2, self.tab_filelist, options=vrt_options)
print('VRT Datei erzeugt', flush=True)
self.print_log.emit('VRT Datei erzeugt')
self.show_progress.emit(10)
# mbtiles generieren
creation_options = ["TILE_FORMAT=JPEG", "QUALITY=90"]
src_ds = gdal.Open(path2)
gdal.Translate(pathout, src_ds, format='mbtiles', creationOptions=creation_options)
print('MB Tiles generiert', flush=True)
self.print_log.emit('MB Tiles generiert')
self.show_progress.emit(75)
# Overviews erzeugen
Image = gdal.Open(pathout, 1)
gdal.SetConfigOption("COMPRESS_OVERVIEW", "DEFLATE")
Image.BuildOverviews("AVERAGE", [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048])
self.show_progress.emit(100)
print('Overviews erfolgreich berechnet', flush=True)
self.print_log.emit('Overviews erfolgreich berechnet')
self.change_button_caption.emit('Start')
# except Exception as err:
# print(err)
def terminateThread(self):
self.change_button_caption.emit('Start')
self.print_log.emit('Vorgang abgebrochen')
self.show_progress.emit(0)
self.terminate()
and the functions for comunicating with the ui:
#Slot(str)
def printLog(self, msg):
self.ui.list_status.addItem(msg)
#Slot(int)
def showProgress(self, value):
self.ui.progressBar.setValue(value)
#Slot(str)
def changeCaption(self, txt):
self.ui.btn_start_mbtiles.setText(txt)
Looks like gdal library is a binding - non-python code is called from python. So python's terminate() just can't stop it until execution returned to the python part. If you need to terminate your function immediatly, you may use processes instead of threads.
This may be helpful:
Is it possible to run function in a subprocess without threading or writing a separate file/script.

Python PyQt Pyside - setNameFilters in QFileDialog does not work

(Windows 7 64 Bit, PyCharm 3.4.1 Pro, Python 3.4.0, PySide 1.2.2)
I want to make a file dialog with filters and preselect one filter.
If i use the static method, it works, i can use filters and preselect one filter.
dir = self.sourceDir
filters = "Text files (*.txt);;Images (*.png *.xpm *.jpg)"
selected_filter = "Images (*.png *.xpm *.jpg)"
fileObj = QFileDialog.getOpenFileName(self, " File dialog ", dir, filters, selected_filter)
If i use an object it does not work, my filters are not there.
file_dialog = QFileDialog(self)
file_dialog.setNameFilters("Text files (*.txt);;Images (*.png *.jpg)")
file_dialog.selectNameFilter("Images (*.png *.jpg)")
file_dialog.getOpenFileName()
Why does this not work?
You have misunderstood how QFileDialog works.
The functions getOpenFileName, getSaveFileName, etc are static. They create an internal file-dialog object, and the arguments to the function are used to set properties on it.
But when you use the QFileDialog constructor, it creates an external instance, and so setting properties on it have no effect on the internal file-dialog object created by the static functions.
What you have to do instead, is show the external instance you created:
file_dialog = QFileDialog(self)
# the name filters must be a list
file_dialog.setNameFilters(["Text files (*.txt)", "Images (*.png *.jpg)"])
file_dialog.selectNameFilter("Images (*.png *.jpg)")
# show the dialog
file_dialog.exec_()
Global solution is here. You can get directory, filename and file extension. You can use that:
d = Dialog(self)
d.filters = ['A dosyaları (*.aaa)', 'B Dosyaları (*.bbb)', 'C Dosyaları (*.ccc)', 'Tüm Dosyalar (*.*)']
d.default_filter_index = 3
d.exec()
print(d.filename)
print(d.path)
and class is here!
from PyQt5.QtWidgets import QFileDialog, QDialog
import os
class Dialog():
def __init__(self, mainform):
self.__mainform = mainform
self.__dialog = QFileDialog()
self.__directory = ''
self.__filename = ['', '', '']
self.__filters = []
self.__default_filter_index = 0
self.__path = ''
#property
def path(self):
return self.__path
#property
def filename(self):
return self.__filename
#property
def directory(self):
return self.__directory
#directory.setter
def directory(self, value):
self.__directory = value
#property
def filters(self):
return self.__filters
#filters.setter
def filters(self, value):
self.__filters = value
#property
def default_filter_index(self):
return self.__default_filter_index
#default_filter_index.setter
def default_filter_index(self, value):
self.__default_filter_index = value
def exec(self, load =True):
self.__dialog.setNameFilters(self.__filters)
self.__dialog.selectNameFilter(self.__filters[self.__default_filter_index])
self.__dialog.setDirectory(self.__directory)
if load == True:
self.__dialog.setLabelText(QFileDialog.Accept, 'Aç')
self.__dialog.setWindowTitle('Aç')
else:
self.__dialog.setLabelText(QFileDialog.Accept, 'Kaydet')
self.__dialog.setWindowTitle('Kaydet')
if self.__dialog.exec() == QDialog.Accepted:
self.__path = self.__dialog.selectedFiles()[0]
fn = os.path.split(self.__path)
ex = os.path.splitext(self.__path)[1]
self.__filename = [fn[0], fn[1], ex[1:len(ex)]]

Refresh QTextEdit in PyQt

Im writing a PyQt app that takes some input in one widget, and then processes some text files.
What ive got at the moment is when the user clicks the "process" button a seperate window with a QTextEdit in it pops up, and ouputs some logging messages.
On Mac OS X this window is refreshed automatically and you cna see the process.
On Windows, the window reports (Not Responding) and then once all the proccessing is done, the log output is shown. Im assuming I need to refresh the window after each write into the log, and ive had a look around at using a timer. etc, but havnt had much luck in getting it working.
Below is the source code. It has two files, GUI.py which does all the GUI stuff and MOVtoMXF that does all the processing.
GUI.py
import os
import sys
import MOVtoMXF
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class Form(QDialog):
def process(self):
path = str(self.pathBox.displayText())
if(path == ''):
QMessageBox.warning(self, "Empty Path", "You didnt fill something out.")
return
xmlFile = str(self.xmlFileBox.displayText())
if(xmlFile == ''):
QMessageBox.warning(self, "No XML file", "You didnt fill something.")
return
outFileName = str(self.outfileNameBox.displayText())
if(outFileName == ''):
QMessageBox.warning(self, "No Output File", "You didnt do something")
return
print path + " " + xmlFile + " " + outFileName
mov1 = MOVtoMXF.MOVtoMXF(path, xmlFile, outFileName, self.log)
self.log.show()
rc = mov1.ScanFile()
if( rc < 0):
print "something happened"
#self.done(0)
def __init__(self, parent=None):
super(Form, self).__init__(parent)
self.log = Log()
self.pathLabel = QLabel("P2 Path:")
self.pathBox = QLineEdit("")
self.pathBrowseB = QPushButton("Browse")
self.pathLayout = QHBoxLayout()
self.pathLayout.addStretch()
self.pathLayout.addWidget(self.pathLabel)
self.pathLayout.addWidget(self.pathBox)
self.pathLayout.addWidget(self.pathBrowseB)
self.xmlLabel = QLabel("FCP XML File:")
self.xmlFileBox = QLineEdit("")
self.xmlFileBrowseB = QPushButton("Browse")
self.xmlLayout = QHBoxLayout()
self.xmlLayout.addStretch()
self.xmlLayout.addWidget(self.xmlLabel)
self.xmlLayout.addWidget(self.xmlFileBox)
self.xmlLayout.addWidget(self.xmlFileBrowseB)
self.outFileLabel = QLabel("Save to:")
self.outfileNameBox = QLineEdit("")
self.outputFileBrowseB = QPushButton("Browse")
self.outputLayout = QHBoxLayout()
self.outputLayout.addStretch()
self.outputLayout.addWidget(self.outFileLabel)
self.outputLayout.addWidget(self.outfileNameBox)
self.outputLayout.addWidget(self.outputFileBrowseB)
self.exitButton = QPushButton("Exit")
self.processButton = QPushButton("Process")
self.buttonLayout = QHBoxLayout()
#self.buttonLayout.addStretch()
self.buttonLayout.addWidget(self.exitButton)
self.buttonLayout.addWidget(self.processButton)
self.layout = QVBoxLayout()
self.layout.addLayout(self.pathLayout)
self.layout.addLayout(self.xmlLayout)
self.layout.addLayout(self.outputLayout)
self.layout.addLayout(self.buttonLayout)
self.setLayout(self.layout)
self.pathBox.setFocus()
self.setWindowTitle("MOVtoMXF")
self.connect(self.processButton, SIGNAL("clicked()"), self.process)
self.connect(self.exitButton, SIGNAL("clicked()"), self, SLOT("reject()"))
self.ConnectButtons()
class Log(QTextEdit):
def __init__(self, parent=None):
super(Log, self).__init__(parent)
self.timer = QTimer()
self.connect(self.timer, SIGNAL("timeout()"), self.updateText())
self.timer.start(2000)
def updateText(self):
print "update Called"
AND MOVtoMXF.py
import os
import sys
import time
import string
import FileUtils
import shutil
import re
class MOVtoMXF:
#Class to do the MOVtoMXF stuff.
def __init__(self, path, xmlFile, outputFile, edit):
self.MXFdict = {}
self.MOVDict = {}
self.path = path
self.xmlFile = xmlFile
self.outputFile = outputFile
self.outputDirectory = outputFile.rsplit('/',1)
self.outputDirectory = self.outputDirectory[0]
sys.stdout = OutLog( edit, sys.stdout)
class OutLog():
def __init__(self, edit, out=None, color=None):
"""(edit, out=None, color=None) -> can write stdout, stderr to a
QTextEdit.
edit = QTextEdit
out = alternate stream ( can be the original sys.stdout )
color = alternate color (i.e. color stderr a different color)
"""
self.edit = edit
self.out = None
self.color = color
def write(self, m):
if self.color:
tc = self.edit.textColor()
self.edit.setTextColor(self.color)
#self.edit.moveCursor(QtGui.QTextCursor.End)
self.edit.insertPlainText( m )
if self.color:
self.edit.setTextColor(tc)
if self.out:
self.out.write(m)
self.edit.show()
If any other code is needed (i think this is all that is needed) then just let me know.
Any Help would be great.
Mark
It looks like your are running an external program, capturing its output into a QTextEdit. I didn't see the code of Form.process, but I am guessing on windows your function waits for the external program to finish, then quickly dumps everything to the QTextEdit.
If your interface really is waiting for the other process to finish, then it will hang in the manner you describe. You'll need to look at subprocess or perhaps even popen to get the program's output in a "non-blocking" manner.
The key to avoiding "(Not Responding)" is to call QApplication.processEvents a few times every few seconds. The QTimer is not going to help in this case, because if Qt cannot process its events, it cannot call any signal handlers.

Categories

Resources