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:
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:
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!
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:
The signal doesn't know what is connected to it (or if there is anything connected at all)
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):
|
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:
Learn about widgets available in Qt
Learn about signals and slots
Here you can see what changed between the old and new versions:
|
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
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?
Note: the above mentioned problem does not occur when I run the command line version (so, not using Eric4).
Looks like an eric problem. I'l see if I can reproduce it.
@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...
Please keep me posted if they figure it out, so I can add a note in the tutorial.
@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
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 :-(
I have this error
for task in todo.Task.query().all():
TypeError: 'Query' object is not callable
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
@Alquimista, could you email me the version numbers of elixir and sqlalchemy you are using?
I solve the problem in elixir 0.7
todo.Task.query.all()
removing the parentesis
Thanks for posting solution here
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
If I have read the comments before trying to solve the problem myself would save a lot of time :)
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!
Hi
Just worked through tutorial. Found it was very useful for python novice.
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...
Volví a instalar SQLAlchemy, posteriormente instalé Elixir y ahora funciona con la modificación del código... Que raro!
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?
Having the same problem, working through with pyside... seems the autoconnecting doesn't work with pyside(?)
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
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???
Hi very nice article
Your blog has the same post as another author but i like your better
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?
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!
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!