Updating QML content from Python threads

This PySide tutorial shows you how to use native Python threads (non-QThread, i.e. threading.Thread) to carry out work in the background (e.g. downloading files). In the special case of downloading, you might want to use QNetworkAccessManager and friends, but in this case, we assume that you can’t use it for whatever reason (e.g. because you want to use Twisted or because you already have your special download code that you want to reuse).

WorkingOnIt.py

Importing required modules

We will be using standard Python modules for threading (threading) and for downloading (urllib). From PySide, we need the standard module QtCore, QtGui and QtDeclarative:

  1. import os
  2. import sys
  3. import threading
  4. import urllib
  5.  
  6. from PySide import QtCore, QtGui, QtDeclarative

The Downloader object

We now subclass QObject (so we can have Signals, Slots and Properties in our downloader) and implement all the properties that we need for downloading the file and also for displaying the current status in the UI:

  1. class Downloader(QtCore.QObject):
  2.     def __init__(self, url, filename=None):
  3.         QtCore.QObject.__init__(self)
  4.         self._url = url
  5.         if filename is None:
  6.             filename = os.path.basename(self._url)
  7.         self._filename = filename
  8.         self._progress = 0.
  9.         self._running = False
  10.         self._size = -1
  11.  
  12.     def _download(self):
  13.         def reporthook(pos, block, total):
  14.             if self.size != total:
  15.                 self._size = total
  16.                 self.on_size.emit()
  17.             self.progress = float(pos*block)/float(total)
  18.         urllib.urlretrieve(self._url, self._filename, reporthook)
  19.         self.running = False
  20.  
  21.     @QtCore.Slot()
  22.     def start_download(self):
  23.         if not self.running:
  24.             self.running = True
  25.             thread = threading.Thread(target=self._download)
  26.             thread.start()
  27.  
  28.     def _get_progress(self):
  29.         return self._progress
  30.  
  31.     def _set_progress(self, progress):
  32.         self._progress = progress
  33.         self.on_progress.emit()
  34.  
  35.     def _get_running(self):
  36.         return self._running
  37.  
  38.     def _set_running(self, running):
  39.         self._running = running
  40.         self.on_running.emit()
  41.  
  42.     def _get_filename(self):
  43.         return self._filename
  44.  
  45.     def _get_size(self):
  46.         return self._size
  47.  
  48.     on_progress = QtCore.Signal()
  49.     on_running = QtCore.Signal()
  50.     on_filename = QtCore.Signal()
  51.     on_size = QtCore.Signal()
  52.  
  53.     progress = QtCore.Property(float, _get_progress, _set_progress,
  54.             notify=on_progress)
  55.     running = QtCore.Property(bool, _get_running, _set_running,
  56.             notify=on_running)
  57.     filename = QtCore.Property(str, _get_filename, notify=on_filename)
  58.     size = QtCore.Property(int, _get_size, notify=on_size)

Creating a new Downloader instance

As an example, we create a new Downloader here that downloads a kernel image for the N900 from the MeeGo repository:

  1. downloader = Downloader('http://repo.meego.com/MeeGo/builds/trunk/1.1.80.8.20101130.1/handset/images/meego-handset-armv7l-n900/meego-handset-armv7l-n900-1.1.80.8.20101130.1-vmlinuz-2.6.35.3-13.6-n900')

QApplication, QDeclarativeView and context properties

As usual, we simply instantiate a new QApplication and a QDeclarativeView. Our Downloader is exposed to the QML context by setting it as context property downloader on the rootContext of our view. We then simply load the QML file via setSource, show the view and execute the application:

  1. app = QtGui.QApplication(sys.argv)
  2. view = QtDeclarative.QDeclarativeView()
  3. view.rootContext().setContextProperty('downloader', downloader)
  4. view.setSource(__file__.replace('.py', '.qml'))
  5. view.show()
  6. app.exec_()

WorkingOnIt.qml

This is the QML UI for our downloader example. The interesting parts here are:

  • When the button is clicked, downloader.start_download() (a PySide Slot) is called, which starts the thread
  • The UI elements use properties of the downloader to determine visiblity and content – they are automatically updated when the properties are notified to be updated

  1. import Qt 4.7
  2.  
  3. Rectangle {
  4.     width: 200; height: 160
  5.  
  6.     function formatProgress(size, progress) {
  7.         return "" + parseInt(progress*size/1024) +
  8.             " KiB ("+parseInt(progress*100.) + "%)";
  9.     }
  10.  
  11.     Text {
  12.         x: progressBar.x; y: 20
  13.         width: progressBar.width
  14.         font.pixelSize: 8
  15.         text: downloader.filename
  16.         elide: Text.ElideRight
  17.     }
  18.  
  19.     Rectangle {
  20.         id: progressBar
  21.         color: "#aaa"
  22.  
  23.         x: 20; y: 60
  24.         width: parent.width-40
  25.         height: 20
  26.  
  27.         Rectangle {
  28.             color: downloader.progress<1?"#ee8":"#8e8"
  29.             clip: true
  30.  
  31.             anchors {
  32.                 top: parent.top
  33.                 bottom: parent.bottom
  34.                 left: parent.left
  35.             }
  36.  
  37.             width: parent.width*downloader.progress
  38.  
  39.             Text {
  40.                 anchors {
  41.                     fill: parent
  42.                     rightMargin: 5
  43.                 }
  44.                 color: "black"
  45.                 text: formatProgress(downloader.size, downloader.progress)
  46.                 verticalAlignment: Text.AlignVCenter
  47.                 horizontalAlignment: Text.AlignRight
  48.             }
  49.         }
  50.     }
  51.  
  52.     Rectangle {
  53.         anchors.left: progressBar.left
  54.         anchors.right: progressBar.right
  55.  
  56.         color: "#aad"
  57.         y: progressBar.y + progressBar.height + 20
  58.         height: 40
  59.  
  60.         Text {
  61.             anchors.fill: parent
  62.             color: "#003"
  63.             text: downloader.running?"Please wait...":"Start download"
  64.  
  65.             verticalAlignment: Text.AlignVCenter
  66.             horizontalAlignment: Text.AlignHCenter
  67.         }
  68.  
  69.         MouseArea {
  70.             anchors.fill: parent
  71.             onClicked: { downloader.start_download() }
  72.         }
  73.     }
  74. }

How the example app looks

Save the files WorkingOnIt.py and WorkingOnIt.qml in the same folder and start the app using python WorkingOnIt.py:

Screenshot of the application in action

Categories:

  • Tutorial
  • LanguageBindings
  • snippets
  •