Ir al contenido principal

Ralsina.Me — El sitio web de Roberto Alsina

PyQt by Example (Session 2)

Requirements

If you haven't yet, check Ses­sion 1 first.

All files for this ses­sion are here: Ses­sion 2 at GitHub

Session 2: Plugging Things

In Ses­sion 1 we cre­at­ed our ap­pli­ca­tion's main win­dow, a skele­ton ap­pli­ca­tion that dis­plays said win­dow, and a sim­ple Elixir based back­end.

We did not, how­ev­er, con­nect both things. And con­nect­ing things is the im­por­tant step we are tak­ing in this ses­sion.

We will be work­ing first on our main.py.

Loading Data

The first thing is that we need to use our back­end, so we have to im­port to­do.py.

We do this in line 13

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

Let's ex­am­ine that in de­tail. 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, to­do.­Task.­query().al­l() 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-re­lat­ed code.

First: self­.ui.list is a wid­get. Ev­ery­thing we put on the win­dow us­ing de­sign­er is ac­ces­si­ble us­ing self­.ui.ob­jec­t_­name and the ob­jec­t_­name is set us­ing de­sign­er. If you right-click on the big wid­get in our win­dow you can see that the wid­get's ob­ject name is list:

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

The ob­ject name is list.

You can al­so see it (and change it) in the Ob­ject In­spec­tor and in the Prop­er­ty Ed­i­tor.

That is not just a wid­get. It's a QTreeWid­get, use­ful for show­ing fan­cy mul­ti­-­col­umn lists and trees.

You can read a lot about this wid­get in its man­u­al but let's do the very short ver­sion:

  • We cre­ate QTreeWid­getItem ob­­jec­t­s. These take a list of strings, which are dis­­­played in the wid­get's col­umn­s. In this case, we are adding the task text ("Buy gro­ceries"), the due date, and the tags.

  • We set item.­­task to task. This is so lat­er on we know what task is de­scribed by a spe­­cif­ic item. Not the most el­e­­gant way, per­hap­s, but by far the eas­i­est.

  • We add each item to the wid­get us­ing ad­d­To­­pLevelItem so they are all at the same lev­­el (not hi­er­ar­chi­­cal, as childs and par­ents)

  • 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 sim­ple pro­gram­s, this is all you need to know about QTreeWid­get. It's a rather com­plex wid­get, and very pow­er­ful, but this will do for our task. In­ves­ti­gate!

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 sam­ple 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 be­cause we have al­so im­ple­ment­ed (par­tial) sav­ing of mod­i­fied tasks...

Saving Data

So, what we want is that when the us­er clicks on the check­box, the task should be mod­i­fied ac­cord­ing­ly, and stored in the data­base.

In most tool­kits you would be look­ing for call­back­s. Here things are a bit dif­fer­en­t. When a Qt wid­get wants to make you no­tice some­thing, like "The us­er clicked this but­ton" or "The Help menu is ac­ti­vat­ed", or what­ev­er, they emit sig­nals (go along for a min­ute).

In par­tic­u­lar, here we are in­ter­est­ed in the item­Changed sig­nal of QTreeWid­get:

QTreeWid­get.item­Changed ( item, col­umn ) [sig­nal]

This sig­nal is emit­ted when the con­tents of the col­umn in the spec­i­fied item changes.

And, when­ev­er you click on the check­box, this sig­nal is emit­ted. Why's that im­por­tan­t? Be­cause you can con­nect your own code to a sig­nal, so that when­ev­er that sig­nal is emit­ted, your code is ex­e­cut­ed! Your code is called a slot in Qt slang.

It's like a call­back, ex­cept that:

  1. The sig­­nal does­n't know what is con­nec­t­ed to it (or if there is any­thing con­nec­t­ed at al­l)

  2. You can con­nect as many slots to a sig­­nal as you wan­t.

This helps keep your code loose­ly cou­pled.

We could de­fine a method in the Main class, and con­nect it to the item­Changed sig­nal, but there is no need be­cause we can use Au­to­Con­nec­t. If you add to Main a method with a spe­cif­ic name, it will be con­nect­ed to that sig­nal. The name is on_ob­ject­name_sig­nal­name.

Here is the code (lines 37 to 42):

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 re­flect the item's check­State (whether it's checked or not) in­to the task state?

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

Au­to­Con­nect is what you will use 90% of the time to add be­hav­iour to your ap­pli­ca­tion. Most of the time, you will just cre­ate the win­dow, add the wid­get­s, and hook sig­nals via Au­to­Con­nec­t.

In some oc­ca­sions that will not be enough. But we are not there yet.

This was a rather short ses­sion, but be pre­pared for the next one: we will be do­ing Im­por­tant Stuff with De­sign­er (T­M)!

In the mean­time, you may want to check these pages:


Here you can see what changed be­tween the old and new ver­sion­s:

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
Geert Vancompernolle / 2009-03-08 15:17:

When using Eric4 (on WinXP), I get the following error when running the command "from elixir import *" in the file "todo.py":

"the file buildbdist.win32eggelixir__... could not be opened".

I don't have a directory "build" at all in my project.
What I do have, is the file "Elixir-0.6.1-py2.6.egg" in the directory /Lib/site-packages.

Any idea as to why I get that import issue?

Geert Vancompernolle / 2009-03-08 15:17:

Note: the above mentioned problem does not occur when I run the command line version (so, not using Eric4).

Roberto Alsina / 2009-03-08 15:53:

Looks like an eric problem. I'l see if I can reproduce it.

Geert Vancompernolle / 2009-03-08 16:37:

@Roberto:

It indeed seems to be an Eric4 issue. When I step over instead of step into the Elixir import or the Elixir functions (like using_options() and setup_all()), then it's OK.

Only when I want to step "into" that package/those functions, I get the error message.

I've in the mean time asked the question on the Eric4 forum as to how they cope with .egg packages...

Roberto Alsina / 2009-03-08 16:57:

Please keep me posted if they figure it out, so I can add a note in the tutorial.

Geert Vancompernolle / 2009-03-18 19:34:

@Roberto:

I got the following response on the .egg debugger problem, given by the Eric4 developer himself (Detlev Offenbach):

"
Egg files are problematic for debuggers. As you saw in your original report,
the filename does not give any indication about the filename of the egg file.
If that would be given, I could implement some logic. Maybe somebody on this
list can give hints on how to tackle this issue.

"

See http://thread.gmane.org/gma... for the complete discussion so far.

So, it won't be an easy one to tackle...

Best is to mention in the tutorials to step over and not step into functions that are defined in .egg files.

Best rgds,
--Geert

Roberto Alsina / 2009-03-18 21:08:

There is simply no way to follow that advice. How is the user to know what's an egg and what's not?

I think that just adds noise, the tutorial says nothing about debuggers at all anyway :-(

Alquimista / 2009-11-03 01:33:

I have this error

for task in todo.Task.query().all():
TypeError: 'Query' object is not callable

dubbaluga / 2010-10-11 15:43:

Hi! I got this error as well,
just remove the parentheses from query in the for-loop on line 27 above:
for task in todo.Task.query.all():

Hope this helps! Regards, Rainer

Roberto Alsina / 2009-11-03 03:04:

@Alquimista, could you email me the version numbers of elixir and sqlalchemy you are using?

Alquimista / 2009-11-09 16:20:

I solve the problem in elixir 0.7

todo.Task.query.all()

removing the parentesis

Ximulka / 2011-12-16 16:39:

Thanks for posting solution here

Jeff / 2010-02-06 06:53:

Hi,
great Tutorial so far. But I spent at least 20 minutes to find the cause of a "'Task' has no attribute 'query'", as I typed the code you're talking about by hand. And in the text above you don't mention to call todo.initDB() (line 47). Only comparing my code to your version finally saved me from a bold head... And I know it's a small issue and people can figure it out themselves, but still :)
Thanks

Ximulka / 2011-12-16 16:36:

If I have read the comments before trying to solve the problem myself would save a lot of time :)

Vincent / 2012-05-15 06:28:

I lost about 10 minutes on this as well. Adding the call to todo.initDB() at line 47 should probably be mentioned between "We do this in line 13" and "Then, in line 25 ..." Thanks for this helpful tutorial and comments!

Gary Wakerley / 2010-03-15 13:54:

Hi
Just worked through tutorial. Found it was very useful for python novice.

BxCx / 2011-06-12 15:14:

Hola.

Gracias por este gran aporte!

Sabes, estoy siguiendo el código me tira un error en:

27        for task in todo.Task.query().all():
Lo dejé como:

27        for task in todo.Task.query.all():
Y sigo teniendo el mismo problema.

Tengo la versión 0.7.1 de Elixir ¿A qué se deberá el error?

Saludos!

P.D. Pensaba loguearme con Disqus pero es muy invasivo a la privacidad...

BxCx / 2011-06-13 02:57:

Volví a instalar SQLAlchemy, posteriormente instalé Elixir y ahora funciona con la modificación del código... Que raro!

Alexander Staiger / 2011-09-16 13:44:

I'm trying this tutorial with Pyside. The "connectSlotsByName" function is not working for me. Is anyone aware of a problem with pyside or any other hints?

Mike / 2011-12-02 18:04:

Having the same problem, working through with pyside... seems the autoconnecting doesn't work with pyside(?)

Jadoti / 2011-12-02 18:23:

You have to decorate the handler with a @Slot indicator, so above the "def on_list_itemChanged" line, put "@QtCore:disqus 
.Slot(QTreeWidgetItem,int)" (without quotes)

I got this from inside qt designer, when viewing the slots (Go to slots) for the list object, you scroll to the itemChanged signal, it shows you the call (itemChanged(QTreeWidgetItem*, int))

HTH

Drbluesman / 2011-09-16 21:31:

Python 2.7.1, Elixir 0.7.1, Gedit editor on Ubuntu. I'm receiving the same error as in first comment: "AttributeError: type object 'Task' has no attribute 'query'" I have tried with line 27 as both "for task in todo.Task.query().all(): "and for task in todo.Task.query.all():".  Any other ideas???

employment background check / 2011-12-27 23:22:


Hi very nice article

cell phone lookup / 2012-01-17 05:51:


Your blog has the same post as another author but i like your better

xuansamdinh / 2012-05-25 17:39:

Hi!
Thanks for your tutorial!
I have something which is misunderstood, that is, if I run the main.py standalone, I will get an empty database only. To solve this issue, I have to run the todo.py beforce run the main.py file. So, the widget will display items, if not, that is an empty list.
Something has wrong?

rulolp / 2012-10-20 04:14:

Si al ejecutarlo funciona pero en la terminal sale QSpiAccessible::accessibleEvent not handled: con
sudo apt-get remove qt-at-spi se soluciona,
parece que es un bug en el paquete ubuntu qt-at-spi que viene por defecto al menos en Ubuntu 11.10 y 12.04, desinstalarlo no se si fue una buena idea, ya me enteraré, pero funcionó y en la terminal no aparece ningún error. Ocurre siempre con el elixir? es la primera vez que lo usaba :)
Muy interesante el post, gracias por mantenerlo en línea tanto tiempo!

rulolp / 2012-10-20 04:32:

Sería de ayuda agregar los enlaces a las siguientes secciones al final de cada post ya que pensé que no estaban en linea, si alguien más las está buscando, la sesión 3 está en http://lateral.netmanagers.... la sesión 4 en http://lateral.netmanagers.... y la 5 en http://lateral.netmanagers.... tampoco encontré un índice con todas las sesiones. De más está decir que muchas gracias por el post!