Saturday, 20 February 2010

PySide tutorial: model view programming, part one

The third PySide tutorial here (part two: signals and slots available at http://blog.rburchell.com/2010/02/simple-pyside-tutorial-2-signals-and.html and part one: basic introduction/hello world: http://blog.rburchell.com/2010/01/simple-pyside-pyqt-tutorial-aimed-at.html), we're going to delve into something a little more involved and unique than the last two tutorials.

You've already seen by now that Qt has some aspects that are very similar to other programming libraries/toolkits, well, here's an idea which may not be so familiar to you, but, harnessed correctly, is very powerful.

If you've programmed much with GUI stuff before in C# or other languages, you'll be very familiar with widgets like QListWidget in Qt 4. Qt does, however, provide a more powerful alternative to standard widgets where you have to manually manage your data.

For those of you who have done much computer science studies, the pattern in this tutorial will be familiar to you: it's known as 'model view controller', but Qt doesn't actually have a controller component: most of that is handled by the view. So we have two main components: the model (the data) and the view (the presentation and logic of that data).

To get our feet wet, we're going to implement a simple, read-only list of data, with the data maintained separately from the actual widget.

On to the actual code!

# Written by Robin Burchell 
# No licence specified or required, but please give credit where it's due, and please let me know if this helped you.
# Feel free to contact with corrections or suggestions.
#
# We're using PySide, Nokia's official LGPL bindings.
# You can however easily use PyQt (Riverside Computing's GPL bindings) by commenting these and fixing the appropriate imports.
from PySide.QtCore import *
from PySide.QtGui import *
#from PyQt4 import *
#from PyQt4.QtCore import *
#from PyQt4.QtGui import *
import sys

# This is our model. It will maintain, modify, and present data to our view(s).
# As this is read-only, it's pretty straightforward, but it can get pretty complex.
# This is something that Qt Development Frameworks/Nokia are aware of and working on, in terms of
# better documentation, as well as a better implementation of all this, but both of those aren't
# really within the scope of this tutorial. ;)
#
# For more information on list models, take a look at:
# http://doc.trolltech.com/4.6/qabstractitemmodel.html
# but do bear in mind there are other models (like tables) available, depending on your data needs.
# Again, beyond the scope of this tutorial for now. :)
class SimpleListModel(QAbstractListModel):
 def __init__(self, mlist):
  QAbstractListModel.__init__(self)

  # Cache the passed data list as a class member.
  self._items = mlist

 # We need to tell the view how many rows we have present in our data.
 # For us, at least, it's fairly straightforward, as we have a python list of data,
 # so we can just return the length of that list.
 def rowCount(self, parent = QModelIndex()):
  return len(self._items)

 # Here, it's a little more complex.
 # data() is where the view asks us for all sorts of information about our data:
 # this can be purely informational (the data itself), as well as all sorts of 'extras'
 # such as how the data should be presented.
 #
 # For the sake of keeping it simple, I'm only going to show you the data, and one presentational
 # aspect.
 #
 # For more information on what kind of data the views can ask for, take a look at:
 # http://doc.trolltech.com/4.6/qabstractitemmodel.html#data
 #
 # Oh, and just  to clarify: when it says 'invalid QVariant', it means a null QVariant.
 # i.e. QVariant().
 #
 # 'index' is of type QModelIndex, which actually has a whole host of stuff, but we
 # only really care about the row number for the sake of this tutorial.
 # For more information, see:
 # http://doc.trolltech.com/4.6/qmodelindex.html
 def data(self, index, role = Qt.DisplayRole):
  if role == Qt.DisplayRole:
   # The view is asking for the actual data, so, just return the item it's asking for.
   return QVariant(self._items[index.row()])
  elif role == Qt.BackgroundRole:
   # Here, it's asking for some background decoration.
   # Let's mix it up a bit: mod the row number to get even or odd, and return different
   # colours depending.
   # (you can, and should, more easily do this using this:
   # http://doc.trolltech.com/4.6/qabstractitemview.html#alternatingRowColors-prop
   # but I deliberately chose to show that you can put your own logic/processing here.)
   #
   # Exercise for the reader: make it print different colours for each row.
   # Implementation is up to you.
   if index.row() % 2 == 0:
    return QVariant(QColor(Qt.gray))
   else:
    return QVariant(QColor(Qt.lightGray))
  else:
   # We don't care about anything else, so make sure to return an empty QVariant.
   return QVariant()

# This widget is our view of the readonly list.
# Obviously, in a real application, this will be more complex, with signals/etc usage, but
# for the scope of this tutorial, let's keep it simple, as always.
#
# For more information, see:
# http://doc.trolltech.com/4.6/qlistview.html
class SimpleListView(QListView):
 def __init__(self, parent = None):
  QListView.__init__(self, parent)

# Our main application window.
# You should be used to this from previous tutorials.
class MyMainWindow(QWidget):
 def __init__(self):
  QWidget.__init__(self, None)

  # main section of the window
  vbox = QVBoxLayout()

  # create a data source:
  m = SimpleListModel(["test", "tes1t", "t3est", "t5est", "t3est"])

  # let's add two views of the same data source we just created:
  v = SimpleListView()
  v.setModel(m)
  vbox.addWidget(v)

  # second view..
  v = SimpleListView()
  v.setModel(m)
  vbox.addWidget(v)

  # bottom section of the window
  hbox = QHBoxLayout()

  # add bottom to main window layout
  vbox.addLayout(hbox)

  # set layout on the window
  self.setLayout(vbox)

# set things up, and run it. :)
if __name__ == '__main__':
 app = QApplication(sys.argv)
 w = MyMainWindow()
 w.show()
 app.exec_()
 sys.exit()

22 comments:

  1. Excellent tutorial! Do you know how to add a picture or an icon to items in the list? Much like contacts' avatars in maemo 5 contacts. Also in conversations, in each item there is date in small font, how do we get this effect in Qt. I'm pretty sure I can find out with a little search, but asking won't hurt to :)
    ReplyDelete
  2. @jadb: hi there! and thanks for the feedback.

    the concept of 'roles' in the model data() method that I briefly skip over should be helpful in adding an icon. take a look at http://doc.trolltech.com/4.6/qt.html#ItemDataRole-enum - In particular, you want DecorationRole. ;)

    As for small fonts and other things, that will need something a bit outside the discussion of this tutorial: model view delegates - a delegate is responsible for the painting and interaction of an item in the model.

    Take a look at: http://doc.trolltech.com/4.6/model-view-delegate.html
    ReplyDelete
  3. (I'll probably do a separate tutorial with delegates sometime in the future. modelview is a *big* subject, so I will be doing at least 1-2 more tutorials on it. :))
    ReplyDelete
  4. Thanks a lot! I'll be waiting for those. And I've just converted my QListWidgets into QListViews after reading this tutorial ;)
    ReplyDelete
  5. @jadb: cool! what project are you working on?
    ReplyDelete
  6. Well it's not something really BIG, I wanted to learn programming for maemo, so as I always say the best way to accomplish that is to complete a project on it. There you go full description: http://jadb.wordpress.com/2010/02/15/project-retro-conversations/

    If it happens by chance that you're familiar with osso-abook API please let me know..
    ReplyDelete
  7. Hey,

    You should surely either put these on techbase.kde.org or at least put a link there to these blogs!
    ReplyDelete
  8. @jospoortvliet: thanks for the feedback! do you know somewhere on IRC (or someone) I can contact about how they want this structured?
    ReplyDelete
  9. Hmmm.... this script produced only an empty window for me... if it works for everyone else I guess I have corrupted something on my device (like some library/package might be missing...)
    ReplyDelete
  10. @hartti: that's strange, it should produce two lists, one below another.

    can you make the exact code you tried available?
    ReplyDelete
  11. This code also produced only an empty window for me.
    I tried upgrade PySide from 0.2.1 to 0.2.3, but i does not help.

    But when i change first lines to import PyQt instead of PySide - it works
    ReplyDelete
  12. Hi Robin,

    I'm trying developing my own model-view-delegate structure, but in a particular point of my code, data is inserted in the table directly using method setData of my model. So I expected that signal 'dataChanged' should warn view and the latter would update the modified cells. However, nothing happens. Do you know what I'm forgetting or what's wrong?

    Thanks,

    Thiago J.
    ReplyDelete
  13. @Thiago: you probably want to look at QAbstractListModel::beginInsertRows() and QAbstractListModel::endInsertRows().

    Hope this helps, if it doesn't, hit me up via email: viroteck@viroteck.net - and I'll see if I can help you out. ;)
    ReplyDelete
  14. This tutorial segfaults with pyside 0.3.2-3 (installed from debian sid package).
    It works with PyQt4.
    ReplyDelete
  15. Hi shi,

    It didn't when I originally wrote it, so I suspect they've introduced bugs since then.

    Please do report them to the PySide people so they can take a look. :)

    Best,
    Robin
    ReplyDelete
  16. Same here, it shows two empty lists with PySide (ver 0.4.1), but works fine with PyQt4.

    Any idea what has changed in PySide, since you wrote it, that might be causing this?

    P.S: Does it still work fine at your end?
    ReplyDelete
  17. I don't use PySide anymore. I gave up on it because of the huge amount of brokenness I had to keep fighting. By the sounds of this, it hasn't improved. :)

    If you have problems with it, please talk to the PySide people yourself :)
    ReplyDelete
  18. PySide does not use QVariants, see http://developer.qt.nokia.com/wiki/Differences_Between_PySide_and_PyQt .

    So just use QColor(Qt.lightGray) instead of QVariant(QColor(Qt.lightGray)) and everything works.
    ReplyDelete
  19. And Robin -- no need to fight, yield instead (i.e. do it the PySide way) and happiness prevails :). Things seem to be mostly working in 1.0.0~beta2 here.
    ReplyDelete
  20. I am just trying PySide (1.0Beta3) out and came accross this, works all nicely after removing the QVariant(...) stuff.
    ReplyDelete
  21. Thanks for your post, it is very useful.
    One note regarding the QVariant problem addressed in other comments here: Just using PyQt4 in place of PySide makes all work with no problems.
    Also, since this is a tutorial, it could be useful to point out where the dependencies are coming from, e.g.:
    instead of

    from PySide.QtCore import *
    from PySide.QtGui import *

    it could be instructive to use:

    from PyQt4.QtCore import QAbstractListModel, QModelIndex, Qt, QVariant
    from PyQt4.QtGui import QListView, QVBoxLayout, QHBoxLayout, QWidget, QApplication, QColor
    ReplyDelete