Skip to main content

PyQt by Example (Session 2)

Requirements

If you haven't yet, check Session 1 first.

All files for this session are here: Session 2 at GitHub

Session 2: Plugging Things

In Session 1 we created our application's main window, a skeleton application that displays said window, and a simple Elixir based backend.

We did not, however, connect both things. And connecting things is the important step we are taking in this session.

We will be working first on our main.py.

Loading Data

The first thing is that we need to use our backend, so we have to import todo.py.

We do this in line 13

Then, in line 25 we do the first real new work: we get the list of tasks and put them in our list.

Let's examine that in detail. Here is the code from lines 25 to 35:

# Let's do something interesting: load the database contents
# into our task list widget
for task in todo.Task.query().all():
    tags=','.join([t.name for t in task.tags])
    item=QtGui.QTreeWidgetItem([task.text,str(task.date),tags])
    item.task=task
    if task.done:
        item.setCheckState(0,QtCore.Qt.Checked)
    else:
        item.setCheckState(0,QtCore.Qt.Unchecked)
    self.ui.list.addTopLevelItem(item)

Remember our backend, todo.py? In it we defined Task objects that are stored in a database. If you are not familiar with Elixir, todo.Task.query().all() gets a list of all Task objects from the database.

Then we assign the tags separated by commas to tags, which will look something like "home,important".

And now, we see our first Qt-related code.

First: self.ui.list is a widget. Everything we put on the window using designer is accessible using self.ui.object_name and the object_name is set using designer. If you right-click on the big widget in our window you can see that the widget's object name is list:

http://lateral.netmanagers.com.ar/static/tut2-object_name.png

The object name is list.

You can also see it (and change it) in the Object Inspector and in the Property Editor.

That is not just a widget. It's a QTreeWidget, useful for showing fancy multi-column lists and trees.

You can read a lot about this widget in its manual but let's do the very short version:

  • We create QTreeWidgetItem objects. These take a list of strings, which are displayed in the widget's columns. In this case, we are adding the task text ("Buy groceries"), the due date, and the tags.

  • We set item.task to task. This is so later on we know what task is described by a specific item. Not the most elegant way, perhaps, but by far the easiest.

  • We add each item to the widget using addTopLevelItem so they are all at the same level (not hierarchical, as childs and parents)

  • It will work as a list

Then, if the task is done (task.done==True) we make a checkmark next to the task. If it's not done, we do not.

For many simple programs, this is all you need to know about QTreeWidget. It's a rather complex widget, and very powerful, but this will do for our task. Investigate!

So, what does it do? Let's run python main.py and find out!

http://lateral.netmanagers.com.ar/static/tut2-window3.png

The task list with our sample tasks.

If your window is empty, try running python todo.py first, which will create some sample tasks.

You can even check a task to mark it done! That's because we have also implemented (partial) saving of modified tasks...

Saving Data

So, what we want is that when the user clicks on the checkbox, the task should be modified accordingly, and stored in the database.

In most toolkits you would be looking for callbacks. Here things are a bit different. When a Qt widget wants to make you notice something, like "The user clicked this button" or "The Help menu is activated", or whatever, they emit signals (go along for a minute).

In particular, here we are interested in the itemChanged signal of QTreeWidget:

QTreeWidget.itemChanged ( item, column ) [signal]

This signal is emitted when the contents of the column in the specified item changes.

And, whenever you click on the checkbox, this signal is emitted. Why's that important? Because you can connect your own code to a signal, so that whenever that signal is emitted, your code is executed! Your code is called a slot in Qt slang.

It's like a callback, except that:

  1. The signal doesn't know what is connected to it (or if there is anything connected at all)

  2. You can connect as many slots to a signal as you want.

This helps keep your code loosely coupled.

We could define a method in the Main class, and connect it to the itemChanged signal, but there is no need because we can use AutoConnect. If you add to Main a method with a specific name, it will be connected to that signal. The name is on_objectname_signalname.

Here is the code (lines 37 to 42):

1
2
3
4
5
6
def on_list_itemChanged(self,item,column):
    if item.checkState(0):
        item.task.done=True
    else:
        item.task.done=False
    todo.saveData()

See how we use item.task to reflect the item's checkState (whether it's checked or not) into the task state?

The todo.saveData() at the end makes sure all data is saved in the database via Elixir.

AutoConnect is what you will use 90% of the time to add behaviour to your application. Most of the time, you will just create the window, add the widgets, and hook signals via AutoConnect.

In some occasions that will not be enough. But we are not there yet.

This was a rather short session, but be prepared for the next one: we will be doing Important Stuff with Designer (TM)!

In the meantime, you may want to check these pages:


Here you can see what changed between the old and new versions:

Modified lines:  34
Added line:  13, 14, 15, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 46, 47, 48
Removed line:  None
Generated by diff2html
© Yves Bailly, MandrakeSoft S.A. 2001
diff2html is licensed under the GNU GPL.

  session1/main.py     session2/main.py
  34 lines
754 bytes
Last modified : Mon Mar 2 01:29:24 2009

    60 lines
1557 bytes
Last modified : Thu Mar 5 02:03:03 2009

1 # -*- coding: utf-8 -*-   1 # -*- coding: utf-8 -*-
2   2
3 """The user interface for our app"""   3 """The user interface for our app"""
4   4
5 import os,sys   5 import os,sys
6   6
7 # Import Qt modules   7 # Import Qt modules
8 from PyQt4 import QtCore,QtGui   8 from PyQt4 import QtCore,QtGui
9   9
10 # Import the compiled UI module   10 # Import the compiled UI module
11 from windowUi import Ui_MainWindow   11 from windowUi import Ui_MainWindow
12   12
      13 # Import our backend
      14 import todo
      15
13 # Create a class for our main window   16 # Create a class for our main window
14 class Main(QtGui.QMainWindow):   17 class Main(QtGui.QMainWindow):
15     def __init__(self):   18     def __init__(self):
16         QtGui.QMainWindow.__init__(self)   19         QtGui.QMainWindow.__init__(self)
17   20
18         # This is always the same   21         # This is always the same
19         self.ui=Ui_MainWindow()   22         self.ui=Ui_MainWindow()
20         self.ui.setupUi(self)   23         self.ui.setupUi(self)
21   24
      25         # Let's do something interesting: load the database contents
      26         # into our task list widget
      27         for task in todo.Task.query().all():
      28             tags=','.join([t.name for t in task.tags])
      29             item=QtGui.QTreeWidgetItem([task.text,str(task.date),tags])
      30             item.task=task
      31             if task.done:
      32                 item.setCheckState(0,QtCore.Qt.Checked)
      33             else:
      34                 item.setCheckState(0,QtCore.Qt.Unchecked)
      35             self.ui.list.addTopLevelItem(item)
      36
      37     def on_list_itemChanged(self,item,column):
      38         if item.checkState(0):
      39             item.task.done=True
      40         else:
      41             item.task.done=False
      42         todo.saveData()
      43
      44
22 def main():   45 def main():
      46     # Init the database before doing anything else
      47     todo.initDB()
      48
23     # Again, this is boilerplate, it's going to be the same on   49     # Again, this is boilerplate, it's going to be the same on
24     # almost every app you write   50     # almost every app you write
25     app = QtGui.QApplication(sys.argv)   51     app = QtGui.QApplication(sys.argv)
26     window=Main()   52     window=Main()
27     window.show()   53     window.show()
28     # It's exec_ because exec is a reserved word in Python   54     # It's exec_ because exec is a reserved word in Python
29     sys.exit(app.exec_())   55     sys.exit(app.exec_())
30   56
31   57
32 if __name__ == "__main__":   58 if __name__ == "__main__":
33     main()   59     main()
34   60

Generated by diff2html on Fri Mar 6 00:58:30 2009
Command-line:
/home/ralsina/bin/diff2html session1/main.py session2/main.py