Search code examples
pythonpyside2qlistview

Background worker populate QListView thumbnails


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_())
    

Solution

  • 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_())