-1

I have a simple QListview that displays a list of items Names. I would like to display the thumbnail of each item once it has been downloaded. How can i do the following as I'm new to using something like a background worker and I'm not sure how to achieve this.

This explains what i think would be the best approach...

  • Use a custom QStyledItemDelegate that overrides the initStyleOption() function.

  • Detects the lack of an icon and issues an asynchronous request to load it.

  • In the meantime, display default empty icon so user sees placeholder

  • When the asynchronous request to download the icon is done, it signals my widget which updates the items icon.

  • When i create all of my QStandardModelItems, I give them a custom piece of data (a custom role) that holds the path of the thumbnail for each item

    import os
    import sys
    from PySide2 import QtCore, QtGui, QtWidgets
    
    try:
        # python 2
        from urllib import urlretrieve
        from urllib2 import urlopen
    except Exception as e:
        # python 3
        from urllib.request import urlretrieve, urlopen
    
    import time
    from urllib.parse import urlparse
    
    def getThumbnail(url, output):
        if os.path.exists(output):
            return output
        # # download 1
        # # urlretrieve(url, output)
        # # return os.path.abspath(output)
    
        # download 2
        response = urlopen(url, timeout=5000)
        f = open(output, "wb")
        try:
            f.write(response.read())
        finally:
            f.close()
        return output
    
    class ExampleDialog(QtWidgets.QDialog):
    
        def __init__(self):
            super(ExampleDialog, self).__init__()
    
            self.itemModel = QtGui.QStandardItemModel()
    
            self.uiListView = QtWidgets.QListView()
            # self.uiListView.setViewMode(QtWidgets.QListView.IconMode)
            self.uiListView.setIconSize(QtCore.QSize(80, 60))  #set icon size
            self.uiListView.setGridSize(QtCore.QSize(90, 70)) #set icon grid display
            self.uiListView.setModel(self.itemModel)
    
            self.mainLayout = QtWidgets.QVBoxLayout(self)
            self.mainLayout.addWidget(self.uiListView)
    
            self.populateImages()
    
    
        def populateImages(self):
            root = os.path.join(os.getenv('APPDATA'), 'MyApp\\cache')
            if not os.path.exists(root):
                os.makedirs(root)
    
            print('IMAGES:', root)
    
            for x in range(20):
                url = 'https://picsum.photos/id/{}/80/60.jpg'.format(x)
    
                p = urlparse(url).path
                ext = os.path.splitext(p)[-1]
                output = os.path.join(root, '{}{}'.format(x, ext))
    
                # get thumbnail
                getThumbnail(url, output)
    
                # Item
                item = QtGui.QStandardItem('{}'.format(x))
                item.setData(QtGui.QPixmap(output), QtCore.Qt.DecorationRole)
                item.setData(output, QtCore.Qt.UserRole)
                self.itemModel.appendRow(item)
    
    if __name__ == '__main__':
        app = QtWidgets.QApplication(sys.argv)
        window =  ExampleDialog()
        window.show()
        window.raise_()
        sys.exit(app.exec_())
    
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
JokerMartini
  • 5,674
  • 9
  • 83
  • 193

1 Answers1

1

Instead of using a background worker you can use QNetworkAccessManager for asynchronous downloading.

from dataclasses import dataclass
from functools import cached_property
import sys

from PySide2 import QtCore, QtGui, QtWidgets, QtNetwork


@dataclass
class IconDownloader(QtCore.QObject):
    url: QtCore.QUrl
    index: QtCore.QPersistentModelIndex
    _parent: QtCore.QObject = None

    def __post_init__(self):
        super().__init__()
        self.setParent(self._parent)

    @cached_property
    def network_manager(self):
        manager = QtNetwork.QNetworkAccessManager()
        manager.finished.connect(self._handle_finished)
        return manager

    def start(self):
        if self.index.isValid():
            request = QtNetwork.QNetworkRequest(self.url)
            request.setAttribute(
                QtNetwork.QNetworkRequest.FollowRedirectsAttribute, True
            )
            self.network_manager.get(request)

    def _handle_finished(self, reply):
        if reply.error() == QtNetwork.QNetworkReply.NoError:
            pixmap = QtGui.QPixmap()
            ok = pixmap.loadFromData(reply.readAll())
            if ok and self.index.isValid():
                model = self.index.model()
                model.setData(
                    QtCore.QModelIndex(self.index), pixmap, QtCore.Qt.DecorationRole
                )
        else:
            print(reply.error(), reply.errorString())
        reply.deleteLater()
        self.deleteLater()


class ExampleDialog(QtWidgets.QDialog):
    def __init__(self):
        super(ExampleDialog, self).__init__()

        self.itemModel = QtGui.QStandardItemModel()

        self.uiListView = QtWidgets.QListView()
        # self.uiListView.setViewMode(QtWidgets.QListView.IconMode)
        self.uiListView.setIconSize(QtCore.QSize(80, 60))  # set icon size
        self.uiListView.setGridSize(QtCore.QSize(90, 70))  # set icon grid display
        self.uiListView.setModel(self.itemModel)

        self.mainLayout = QtWidgets.QVBoxLayout(self)
        self.mainLayout.addWidget(self.uiListView)

        self.populateImages()

    def populateImages(self):
        for x in range(20):
            url = f"https://picsum.photos/id/{x}/80/60.jpg"
            item = QtGui.QStandardItem(f"x")
            self.itemModel.appendRow(item)
            downloader = IconDownloader(
                QtCore.QUrl(url),
                QtCore.QPersistentModelIndex(self.itemModel.indexFromItem(item)),
                self,
            )
            downloader.start()


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = ExampleDialog()
    window.show()
    window.raise_()
    sys.exit(app.exec_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thanks for the OP code block fix. I would not fix no matter what I did. – JokerMartini Oct 16 '21 at 15:32
  • Thanks for showing this new option of using qnetwork. Could you please show the background option also if that is possible? Just for those who may also want to see that option including myself. – JokerMartini Oct 16 '21 at 15:33
  • @JokerMartini I think it is unnecessary since that option implies using threads that I prefer to avoid. This option is more elegant and simple. – eyllanesc Oct 16 '21 at 15:36
  • @JokerMartini But you can check this post for inspiration https://stackoverflow.com/a/59537535/6622587 – eyllanesc Oct 16 '21 at 15:38
  • I appreciate the help a ton. I do get errors that show as the following `qt.network.ssl: QSslSocket::connectToHostEncrypted: TLS initialization failed PySide2.QtNetwork.QNetworkReply.NetworkError.UnknownNetworkError TLS initialization failed` – JokerMartini Oct 16 '21 at 18:39
  • @JokerMartini Download the openssl .dll and add the directory to the PATH variable environment if you are using windows – eyllanesc Oct 16 '21 at 18:43
  • good to know. i've never used it before. by chance could you share a link as to where to download what i would need. there appears to be a ton of locations.... – JokerMartini Oct 16 '21 at 18:53
  • No luck so far, i've been downloading all kinds of openSSL plugins and have no idea really what im doing but none of them worked. So if you could help that would be great. – JokerMartini Oct 16 '21 at 22:22